Browse Source

Merged vcmi/beta with vcmi/develop

Ivan Savenko 2 years ago
parent
commit
246281e62a
100 changed files with 10211 additions and 7215 deletions
  1. 40 0
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 14 0
      .github/ISSUE_TEMPLATE/feature_request.md
  3. 1 0
      .github/workflows/github.yml
  4. 3 0
      .gitignore
  5. 0 1
      AI/BattleAI/AttackPossibility.cpp
  6. 1 2
      AI/BattleAI/BattleAI.cpp
  7. 2 2
      AI/BattleAI/BattleAI.h
  8. 0 2
      AI/BattleAI/BattleExchangeVariant.cpp
  9. 3 1
      AI/CMakeLists.txt
  10. 1 1
      AI/EmptyAI/CEmptyAI.cpp
  11. 1 1
      AI/EmptyAI/CEmptyAI.h
  12. 3 1
      AI/EmptyAI/StdInc.h
  13. 14 14
      AI/Nullkiller/AIGateway.cpp
  14. 1 1
      AI/Nullkiller/AIGateway.h
  15. 1 1
      AI/Nullkiller/AIUtility.h
  16. 1 1
      AI/Nullkiller/Engine/FuzzyEngines.cpp
  17. 1 3
      AI/Nullkiller/Engine/FuzzyHelper.cpp
  18. 1 1
      AI/Nullkiller/Engine/Nullkiller.cpp
  19. 2 2
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  20. 8 2
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  21. 3 3
      AI/StupidAI/StupidAI.cpp
  22. 3 3
      AI/StupidAI/StupidAI.h
  23. 1 1
      AI/VCAI/FuzzyEngines.cpp
  24. 1 1
      AI/VCAI/Goals/Explore.cpp
  25. 2 2
      AI/VCAI/Pathfinding/AINodeStorage.cpp
  26. 13 11
      AI/VCAI/VCAI.cpp
  27. 1 1
      AI/VCAI/VCAI.h
  28. 1 6
      CCallback.cpp
  29. 0 2
      CCallback.h
  30. 1 1
      CI/linux/before_install.sh
  31. 9 0
      CI/mxe/before_install.sh
  32. 92 35
      CMakeLists.txt
  33. 1 0
      CMakePresets.json
  34. 29 65
      Global.h
  35. 2 1
      README.md
  36. 29 15
      client/CBitmapHandler.cpp
  37. 1 1
      client/CBitmapHandler.h
  38. 66 87
      client/CMT.cpp
  39. 38 10
      client/CMakeLists.txt
  40. 1 1
      client/CMessage.cpp
  41. 1 1
      client/CMusicHandler.cpp
  42. 87 164
      client/CPlayerInterface.cpp
  43. 6 7
      client/CPlayerInterface.h
  44. 9 6
      client/CServerHandler.cpp
  45. 0 2
      client/CServerHandler.h
  46. 7 16
      client/Client.cpp
  47. 3 3
      client/CreatureCostBox.cpp
  48. 0 1
      client/Graphics.cpp
  49. 1 1
      client/Graphics.h
  50. 15 12
      client/NetPacksClient.cpp
  51. 791 0
      client/battle/BattleActionsController.cpp
  52. 103 0
      client/battle/BattleActionsController.h
  53. 1121 0
      client/battle/BattleAnimationClasses.cpp
  54. 367 0
      client/battle/BattleAnimationClasses.h
  55. 92 0
      client/battle/BattleConstants.h
  56. 168 0
      client/battle/BattleEffectsController.cpp
  57. 67 0
      client/battle/BattleEffectsController.h
  58. 556 0
      client/battle/BattleFieldController.cpp
  59. 101 0
      client/battle/BattleFieldController.h
  60. 768 0
      client/battle/BattleInterface.cpp
  61. 226 0
      client/battle/BattleInterface.h
  62. 323 288
      client/battle/BattleInterfaceClasses.cpp
  63. 87 48
      client/battle/BattleInterfaceClasses.h
  64. 178 0
      client/battle/BattleObstacleController.cpp
  65. 60 0
      client/battle/BattleObstacleController.h
  66. 370 0
      client/battle/BattleProjectileController.cpp
  67. 118 0
      client/battle/BattleProjectileController.h
  68. 76 0
      client/battle/BattleRenderer.cpp
  69. 54 0
      client/battle/BattleRenderer.h
  70. 371 0
      client/battle/BattleSiegeController.cpp
  71. 110 0
      client/battle/BattleSiegeController.h
  72. 941 0
      client/battle/BattleStacksController.cpp
  73. 154 0
      client/battle/BattleStacksController.h
  74. 572 0
      client/battle/BattleWindow.cpp
  75. 94 0
      client/battle/BattleWindow.h
  76. 0 1229
      client/battle/CBattleAnimations.cpp
  77. 0 262
      client/battle/CBattleAnimations.h
  78. 0 3807
      client/battle/CBattleInterface.cpp
  79. 0 414
      client/battle/CBattleInterface.h
  80. 0 365
      client/battle/CCreatureAnimation.cpp
  81. 0 124
      client/battle/CCreatureAnimation.h
  82. 412 0
      client/battle/CreatureAnimation.cpp
  83. 149 0
      client/battle/CreatureAnimation.h
  84. 51 53
      client/gui/CAnimation.cpp
  85. 17 12
      client/gui/CAnimation.h
  86. 78 32
      client/gui/CCursorHandler.cpp
  87. 119 18
      client/gui/CCursorHandler.h
  88. 7 7
      client/gui/CGuiHandler.cpp
  89. 3 2
      client/gui/CGuiHandler.h
  90. 5 2
      client/gui/CIntObject.cpp
  91. 22 2
      client/gui/CIntObject.h
  92. 102 0
      client/gui/Canvas.cpp
  93. 65 0
      client/gui/Canvas.h
  94. 162 0
      client/gui/ColorFilter.cpp
  95. 66 0
      client/gui/ColorFilter.h
  96. 7 1
      client/gui/Geometries.cpp
  97. 42 53
      client/gui/Geometries.h
  98. 425 0
      client/gui/InterfaceObjectConfigurable.cpp
  99. 89 0
      client/gui/InterfaceObjectConfigurable.h
  100. 1 1
      client/gui/NotificationHandler.h

+ 40 - 0
.github/ISSUE_TEMPLATE/bug_report.md

@@ -0,0 +1,40 @@
+---
+name: Bug report
+about: Report an issue to help us improve
+title: ''
+labels: ["bug"]
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**Game logs**
+Please attach game logs: `VCMI_client.txt`, `VCMI_server.txt` etc.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Actual behavior**
+A clear description what is currently happening 
+
+**Did it work earlier?**
+If this something which worked well some time ago, please let us know about version where it works or at date when it worked.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Version**
+ - OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
+ - Version: [VCMI version]
+
+**Additional context**
+Add any other context about the problem here.

+ 14 - 0
.github/ISSUE_TEMPLATE/feature_request.md

@@ -0,0 +1,14 @@
+---
+name: Feature request
+about: Suggest an improvement
+title: ''
+labels: ["enhancement"]
+assignees: ''
+
+---
+
+**Describe your proposal**
+Give us as many as possible details about your idea.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.

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

@@ -173,6 +173,7 @@ jobs:
             ../.. -GNinja \
             ${{matrix.cmake_args}} -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} \
             -DENABLE_TEST=${{matrix.test}} \
+            -DENABLE_STRICT_COMPILATION=ON \
             -DPACKAGE_NAME_SUFFIX:STRING="$VCMI_PACKAGE_NAME_SUFFIX" \
             -DPACKAGE_FILE_NAME:STRING="$VCMI_PACKAGE_FILE_NAME" \
             -DENABLE_GITVERSION="$VCMI_PACKAGE_GITVERSION"

+ 3 - 0
.gitignore

@@ -60,3 +60,6 @@ CMakeUserPresets.json
 /AI/FuzzyLite.lib
 /deps
 .vs/
+
+# CLion
+.idea/

+ 0 - 1
AI/BattleAI/AttackPossibility.cpp

@@ -156,7 +156,6 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
 
 				TDmgRange retaliation(0, 0);
 				auto attackDmg = state.battleEstimateDamage(ap.attack, &retaliation);
-				TDmgRange defenderDamageBeforeAttack = state.battleEstimateDamage(BattleAttackInfo(u, attacker, u->canShoot()));
 
 				vstd::amin(attackDmg.first, defenderState->getAvailableHealth());
 				vstd::amin(attackDmg.second, defenderState->getAvailableHealth());

+ 1 - 2
AI/BattleAI/BattleAI.cpp

@@ -79,7 +79,7 @@ CBattleAI::~CBattleAI()
 	}
 }
 
-void CBattleAI::init(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
+void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
 {
 	setCbc(CB);
 	env = ENV;
@@ -186,7 +186,6 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 
 			if(evaluationResult.score > score)
 			{
-				auto & target = bestAttack;
 				score = evaluationResult.score;
 				std::string action;
 

+ 2 - 2
AI/BattleAI/BattleAI.h

@@ -65,7 +65,7 @@ public:
 	CBattleAI();
 	~CBattleAI();
 
-	void init(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) override;
+	void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) override;
 	void attemptCastingSpell();
 
 	void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
@@ -80,7 +80,7 @@ public:
 	//void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
 	//void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
 	//void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack
-	//void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa) override; //called when stack receives damage (after battleAttack())
+	//void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override; //called when stack receives damage (after battleAttack())
 	//void battleEnd(const BattleResult *br) override;
 	//void battleResultsApplied() override; //called when all effects of last battle are applied
 	//void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied;

+ 0 - 2
AI/BattleAI/BattleExchangeVariant.cpp

@@ -215,8 +215,6 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(const battle::Uni
 
 	for(const battle::Unit * enemy : targets.unreachableEnemies)
 	{
-		int64_t stackScore = EvaluationResult::INEFFECTIVE_SCORE;
-
 		std::vector<const battle::Unit *> adjacentStacks = getAdjacentUnits(enemy);
 		auto closestStack = *vstd::minElementByFun(adjacentStacks, [&](const battle::Unit * u) -> int64_t
 			{

+ 3 - 1
AI/CMakeLists.txt

@@ -40,4 +40,6 @@ add_subdirectory(BattleAI)
 add_subdirectory(StupidAI)
 add_subdirectory(EmptyAI)
 add_subdirectory(VCAI)
-add_subdirectory(Nullkiller)
+if(ENABLE_NULLKILLER_AI)
+	add_subdirectory(Nullkiller)
+endif()

+ 1 - 1
AI/EmptyAI/CEmptyAI.cpp

@@ -20,7 +20,7 @@ void CEmptyAI::loadGame(BinaryDeserializer & h, const int version)
 {
 }
 
-void CEmptyAI::init(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
+void CEmptyAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
 {
 	cb = CB;
 	env = ENV;

+ 1 - 1
AI/EmptyAI/CEmptyAI.h

@@ -22,7 +22,7 @@ public:
 	virtual void saveGame(BinarySerializer & h, const int version) override;
 	virtual void loadGame(BinaryDeserializer & h, const int version) override;
 
-	void init(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
+	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
 	void yourTurn() override;
 	void heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID) override;
 	void commanderGotLevel (const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID) override;

+ 3 - 1
AI/EmptyAI/StdInc.h

@@ -4,4 +4,6 @@
 
 // This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
 
-// Here you can add specific libraries and macros which are specific to this project.
+// Here you can add specific libraries and macros which are specific to this project.
+
+VCMI_LIB_USING_NAMESPACE

+ 14 - 14
AI/Nullkiller/AIGateway.cpp

@@ -28,8 +28,8 @@ namespace NKAI
 {
 
 // our to enemy strength ratio constants
-const float SAFE_ATTACK_CONSTANT = 1.2;
-const float RETREAT_THRESHOLD = 0.3;
+const float SAFE_ATTACK_CONSTANT = 1.2f;
+const float RETREAT_THRESHOLD = 0.3f;
 const double RETREAT_ABSOLUTE_THRESHOLD = 10000.;
 
 //one thread may be turn of AI and another will be handling a side effect for AI2
@@ -92,8 +92,9 @@ void AIGateway::heroMoved(const TryMoveHero & details, bool verbose)
 	validateObject(details.id); //enemy hero may have left visible area
 	auto hero = cb->getHero(details.id);
 
-	const int3 from = CGHeroInstance::convertPosition(details.start, false);
-	const int3 to = CGHeroInstance::convertPosition(details.end, false);
+	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));;
+	const int3 to   = hero ? hero->convertToVisitablePos(details.end)   : (details.end   - int3(0,1,0));
+
 	const CGObjectInstance * o1 = vstd::frontOrNull(cb->getVisitableObjs(from, verbose));
 	const CGObjectInstance * o2 = vstd::frontOrNull(cb->getVisitableObjs(to, verbose));
 
@@ -514,7 +515,7 @@ boost::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(
 }
 
 
-void AIGateway::init(std::shared_ptr<Environment> env, std::shared_ptr<CCallback> CB)
+void AIGateway::initGameInterface(std::shared_ptr<Environment> env, std::shared_ptr<CCallback> CB)
 {
 	LOG_TRACE(logAi);
 	myCb = CB;
@@ -535,8 +536,7 @@ void AIGateway::yourTurn()
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
 	status.startedTurn();
-
-	makingTurn = make_unique<boost::thread>(&AIGateway::makeTurn, this);
+	makingTurn = std::make_unique<boost::thread>(&AIGateway::makeTurn, this);
 }
 
 void AIGateway::heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID)
@@ -595,7 +595,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 
 					logAi->trace("Guarded object query hook: %s by %s danger ratio %f", target.toString(), hero.name, ratio);
 
-					if(text.find("guarded") >= 0 && (dangerUnknown || dangerTooHigh))
+					if(text.find("guarded") != std::string::npos && (dangerUnknown || dangerTooHigh))
 						answer = 0; // no
 				}
 			}
@@ -732,7 +732,7 @@ bool AIGateway::makePossibleUpgrades(const CArmedInstance * obj)
 		if(const CStackInstance * s = obj->getStackPtr(SlotID(i)))
 		{
 			UpgradeInfo ui;
-			myCb->getUpgradeInfo(obj, SlotID(i), ui);
+			myCb->fillUpgradeInfo(obj, SlotID(i), ui);
 			if(ui.oldID >= 0 && nullkiller->getFreeResources().canAfford(ui.cost[0] * s->count))
 			{
 				myCb->upgradeCreature(obj, SlotID(i), ui.newID[0]);
@@ -1179,7 +1179,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 	{
 		//FIXME: this assertion fails also if AI moves onto defeated guarded object
 		assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object
-		cb->moveHero(*h, CGHeroInstance::convertPosition(dst, true));
+		cb->moveHero(*h, h->convertFromVisitablePos(dst));
 		afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly?
 		// If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared
 		teleportChannelProbingList.clear();
@@ -1233,14 +1233,14 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 
 		auto doMovement = [&](int3 dst, bool transit)
 		{
-			cb->moveHero(*h, CGHeroInstance::convertPosition(dst, true), transit);
+			cb->moveHero(*h, h->convertFromVisitablePos(dst), transit);
 		};
 
 		auto doTeleportMovement = [&](ObjectInstanceID exitId, int3 exitPos)
 		{
 			destinationTeleport = exitId;
 			if(exitPos.valid())
-				destinationTeleportPos = CGHeroInstance::convertPosition(exitPos, true);
+				destinationTeleportPos = h->convertFromVisitablePos(exitPos);
 			cb->moveHero(*h, h->pos);
 			destinationTeleport = ObjectInstanceID();
 			destinationTeleportPos = int3(-1);
@@ -1249,7 +1249,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 
 		auto doChannelProbing = [&]() -> void
 		{
-			auto currentPos = CGHeroInstance::convertPosition(h->pos, false);
+			auto currentPos = h->visitablePos();
 			auto currentExit = getObj(currentPos, true)->id;
 
 			status.setChannelProbing(true);
@@ -1266,7 +1266,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 			int3 currentCoord = path.nodes[i].coord;
 			int3 nextCoord = path.nodes[i - 1].coord;
 
-			auto currentObject = getObj(currentCoord, currentCoord == CGHeroInstance::convertPosition(h->pos, false));
+			auto currentObject = getObj(currentCoord, currentCoord == h->visitablePos());
 			auto nextObjectTop = getObj(nextCoord, false);
 			auto nextObject = getObj(nextCoord, true);
 			auto destTeleportObj = getDestTeleportObj(currentObject, nextObjectTop, nextObject);

+ 1 - 1
AI/Nullkiller/AIGateway.h

@@ -110,7 +110,7 @@ public:
 
 	std::string getBattleAIName() const override;
 
-	void init(std::shared_ptr<Environment> env, std::shared_ptr<CCallback> CB) override;
+	void initGameInterface(std::shared_ptr<Environment> env, std::shared_ptr<CCallback> CB) override;
 	void yourTurn() override;
 
 	void heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id

+ 1 - 1
AI/Nullkiller/AIUtility.h

@@ -329,7 +329,7 @@ public:
 
 		if(!poolIsEmpty) pool.pop_back();
 
-		return std::move(tmp);
+		return tmp;
 	}
 
 	bool empty() const

+ 1 - 1
AI/Nullkiller/Engine/FuzzyEngines.cpp

@@ -31,7 +31,7 @@ engineBase::engineBase()
 void engineBase::configure()
 {
 	engine.configure("Minimum", "Maximum", "Minimum", "AlgebraicSum", "Centroid", "Proportional");
-	logAi->info(engine.toString());
+	logAi->trace(engine.toString());
 }
 
 void engineBase::addRule(const std::string & txt)

+ 1 - 3
AI/Nullkiller/Engine/FuzzyHelper.cpp

@@ -122,10 +122,8 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
 	case Obj::RESOURCE:
 	{
 		if(!vstd::contains(ai->memory->alreadyVisited, obj))
-		{
 			return 0;
-		}
-		// passthrough
+		FALLTHROUGH;
 	}
 	case Obj::MONSTER:
 	case Obj::HERO:

+ 1 - 1
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -50,7 +50,7 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, PlayerColor playerID)
 		new SharedPool<PriorityEvaluator>(
 			[&]()->std::unique_ptr<PriorityEvaluator>
 			{
-				return make_unique<PriorityEvaluator>(this);
+				return std::make_unique<PriorityEvaluator>(this);
 			}));
 
 	dangerHitMap.reset(new DangerHitMapAnalyzer(this));

+ 2 - 2
AI/Nullkiller/Goals/ExecuteHeroChain.cpp

@@ -127,7 +127,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 							continue;
 						}
 					}
-					catch(cannotFulfillGoalException)
+					catch(const cannotFulfillGoalException &)
 					{
 						if(!heroPtr.validAndSet())
 						{
@@ -173,7 +173,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 			ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
 			blockedIndexes.insert(node.parentIndex);
 		}
-		catch(goalFulfilledException)
+		catch(const goalFulfilledException &)
 		{
 			if(!heroPtr.validAndSet())
 			{

+ 8 - 2
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -401,6 +401,9 @@ public:
 
 	void execute(const blocked_range<size_t>& r)
 	{
+		std::random_device randomDevice;
+		std::mt19937 randomEngine(randomDevice());
+
 		for(int i = r.begin(); i != r.end(); i++)
 		{
 			auto & pos = tiles[i];
@@ -422,7 +425,7 @@ public:
 						existingChains.push_back(&node);
 				}
 
-				std::random_shuffle(existingChains.begin(), existingChains.end());
+				std::shuffle(existingChains.begin(), existingChains.end(), randomEngine);
 
 				for(AIPathNode * node : existingChains)
 				{
@@ -480,6 +483,9 @@ public:
 
 bool AINodeStorage::calculateHeroChain()
 {
+	std::random_device randomDevice;
+	std::mt19937 randomEngine(randomDevice());
+
 	heroChainPass = EHeroChainPass::CHAIN;
 	heroChain.clear();
 
@@ -489,7 +495,7 @@ bool AINodeStorage::calculateHeroChain()
 	{
 		boost::mutex resultMutex;
 
-		std::random_shuffle(data.begin(), data.end());
+		std::shuffle(data.begin(), data.end(), randomEngine);
 
 		parallel_for(blocked_range<size_t>(0, data.size()), [&](const blocked_range<size_t>& r)
 		{

+ 3 - 3
AI/StupidAI/StupidAI.cpp

@@ -28,7 +28,7 @@ CStupidAI::~CStupidAI()
 	print("destroyed");
 }
 
-void CStupidAI::init(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
+void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
 {
 	print("init called, saving ptr to IBattleCallback");
 	env = ENV;
@@ -177,7 +177,7 @@ void CStupidAI::battleAttack(const BattleAttack *ba)
 	print("battleAttack called");
 }
 
-void CStupidAI::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa)
+void CStupidAI::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged)
 {
 	print("battleStacksAttacked called");
 }
@@ -202,7 +202,7 @@ void CStupidAI::battleNewRound(int round)
 	print("battleNewRound called");
 }
 
-void CStupidAI::battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance)
+void CStupidAI::battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
 {
 	print("battleStackMoved called");
 }

+ 3 - 3
AI/StupidAI/StupidAI.h

@@ -25,18 +25,18 @@ public:
 	CStupidAI();
 	~CStupidAI();
 
-	void init(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) override;
+	void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) override;
 	void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
 	void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
 	BattleAction activeStack(const CStack * stack) override; //called when it's turn of that stack
 
 	void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack
-	void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa) override; //called when stack receives damage (after battleAttack())
+	void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override; //called when stack receives damage (after battleAttack())
 	void battleEnd(const BattleResult *br) override;
 	//void battleResultsApplied() override; //called when all effects of last battle are applied
 	void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied;
 	void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
-	void battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance) override;
+	void battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override;
 	void battleSpellCast(const BattleSpellCast *sc) override;
 	void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks
 	//void battleTriggerEffect(const BattleTriggerEffect & bte) override;

+ 1 - 1
AI/VCAI/FuzzyEngines.cpp

@@ -30,7 +30,7 @@ engineBase::engineBase()
 void engineBase::configure()
 {
 	engine.configure("Minimum", "Maximum", "Minimum", "AlgebraicSum", "Centroid", "Proportional");
-	logAi->info(engine.toString());
+	logAi->trace(engine.toString());
 }
 
 void engineBase::addRule(const std::string & txt)

+ 1 - 1
AI/VCAI/Goals/Explore.cpp

@@ -50,7 +50,7 @@ namespace Goals
 			sightRadius = hero->getSightRadius();
 			bestGoal = sptr(Goals::Invalid());
 			bestValue = 0;
-			ourPos = h->convertPosition(h->pos, false);
+			ourPos = h->visitablePos();
 			allowDeadEndCancellation = true;
 			allowGatherArmy = gatherArmy;
 		}

+ 2 - 2
AI/VCAI/Pathfinding/AINodeStorage.cpp

@@ -107,7 +107,7 @@ boost::optional<AIPathNode *> AINodeStorage::getOrCreateNode(const int3 & pos, c
 
 std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
 {
-	auto hpos = hero->getPosition(false);
+	auto hpos = hero->visitablePos();
 	auto initialNode =
 		getOrCreateNode(hpos, hero->boat ? EPathfindingLayer::SAIL : EPathfindingLayer::LAND, NORMAL_CHAIN)
 		.get();
@@ -211,7 +211,7 @@ std::vector<CGPathNode *> AINodeStorage::calculateTeleportations(
 		}
 	}
 
-	if(hero->getPosition(false) == source.coord)
+	if(hero->visitablePos() == source.coord)
 	{
 		calculateTownPortalTeleportations(source, neighbours);
 	}

+ 13 - 11
AI/VCAI/VCAI.cpp

@@ -98,11 +98,13 @@ void VCAI::heroMoved(const TryMoveHero & details, bool verbose)
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
 
-	validateObject(details.id); //enemy hero may have left visible area
+	//enemy hero may have left visible area
+	validateObject(details.id);
 	auto hero = cb->getHero(details.id);
 
-	const int3 from = CGHeroInstance::convertPosition(details.start, false);
-	const int3 to = CGHeroInstance::convertPosition(details.end, false);
+	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));;
+	const int3 to   = hero ? hero->convertToVisitablePos(details.end)   : (details.end   - int3(0,1,0));
+
 	const CGObjectInstance * o1 = vstd::frontOrNull(cb->getVisitableObjs(from, verbose));
 	const CGObjectInstance * o2 = vstd::frontOrNull(cb->getVisitableObjs(to, verbose));
 
@@ -583,7 +585,7 @@ void VCAI::showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions)
 	NET_EVENT_HANDLER;
 }
 
-void VCAI::init(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
+void VCAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
 {
 	LOG_TRACE(logAi);
 	env = ENV;
@@ -608,7 +610,7 @@ void VCAI::yourTurn()
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
 	status.startedTurn();
-	makingTurn = make_unique<boost::thread>(&VCAI::makeTurn, this);
+	makingTurn = std::make_unique<boost::thread>(&VCAI::makeTurn, this);
 }
 
 void VCAI::heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID)
@@ -756,7 +758,7 @@ void makePossibleUpgrades(const CArmedInstance * obj)
 		if(const CStackInstance * s = obj->getStackPtr(SlotID(i)))
 		{
 			UpgradeInfo ui;
-			cb->getUpgradeInfo(obj, SlotID(i), ui);
+			cb->fillUpgradeInfo(obj, SlotID(i), ui);
 			if(ui.oldID >= 0 && cb->getResourceAmount().canAfford(ui.cost[0] * s->count))
 			{
 				cb->upgradeCreature(obj, SlotID(i), ui.newID[0]);
@@ -1813,7 +1815,7 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 	{
 		//FIXME: this assertion fails also if AI moves onto defeated guarded object
 		assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object
-		cb->moveHero(*h, CGHeroInstance::convertPosition(dst, true));
+		cb->moveHero(*h, h->convertFromVisitablePos(dst));
 		afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly?
 		// If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared
 		teleportChannelProbingList.clear();
@@ -1867,14 +1869,14 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 
 		auto doMovement = [&](int3 dst, bool transit)
 		{
-			cb->moveHero(*h, CGHeroInstance::convertPosition(dst, true), transit);
+			cb->moveHero(*h, h->convertFromVisitablePos(dst), transit);
 		};
 
 		auto doTeleportMovement = [&](ObjectInstanceID exitId, int3 exitPos)
 		{
 			destinationTeleport = exitId;
 			if(exitPos.valid())
-				destinationTeleportPos = CGHeroInstance::convertPosition(exitPos, true);
+				destinationTeleportPos = h->convertFromVisitablePos(exitPos);
 			cb->moveHero(*h, h->pos);
 			destinationTeleport = ObjectInstanceID();
 			destinationTeleportPos = int3(-1);
@@ -1883,7 +1885,7 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 
 		auto doChannelProbing = [&]() -> void
 		{
-			auto currentPos = CGHeroInstance::convertPosition(h->pos, false);
+			auto currentPos = h->visitablePos();
 			auto currentExit = getObj(currentPos, true)->id;
 
 			status.setChannelProbing(true);
@@ -1900,7 +1902,7 @@ bool VCAI::moveHeroToTile(int3 dst, HeroPtr h)
 			int3 currentCoord = path.nodes[i].coord;
 			int3 nextCoord = path.nodes[i - 1].coord;
 
-			auto currentObject = getObj(currentCoord, currentCoord == CGHeroInstance::convertPosition(h->pos, false));
+			auto currentObject = getObj(currentCoord, currentCoord == h->visitablePos());
 			auto nextObjectTop = getObj(nextCoord, false);
 			auto nextObject = getObj(nextCoord, true);
 			auto destTeleportObj = getDestTeleportObj(currentObject, nextObjectTop, nextObject);

+ 1 - 1
AI/VCAI/VCAI.h

@@ -143,7 +143,7 @@ public:
 
 	std::string getBattleAIName() const override;
 
-	void init(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
+	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
 	void yourTurn() override;
 
 	void heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id

+ 1 - 6
CCallback.cpp

@@ -335,11 +335,6 @@ int3 CCallback::getGuardingCreaturePosition(int3 tile)
 	return gs->map->guardingCreaturePositions[tile.z][tile.x][tile.y];
 }
 
-void CCallback::calculatePaths( const CGHeroInstance *hero, CPathsInfo &out)
-{
-	gs->calculatePaths(hero, out);
-}
-
 void CCallback::dig( const CGObjectInstance *hero )
 {
 	DigWithHero dwh;
@@ -400,4 +395,4 @@ boost::optional<BattleAction> CBattleCallback::makeSurrenderRetreatDecision(
 	const BattleStateInfoForRetreat & battleState)
 {
 	return cl->playerint[getPlayerID().get()]->makeSurrenderRetreatDecision(battleState);
-}
+}

+ 0 - 2
CCallback.h

@@ -133,8 +133,6 @@ public:
 	virtual int3 getGuardingCreaturePosition(int3 tile);
 	virtual std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h);
 
-	virtual void calculatePaths(const CGHeroInstance *hero, CPathsInfo &out);
-
 	//Set of metrhods that allows adding more interfaces for this player that'll receive game event call-ins.
 	void registerBattleInterface(std::shared_ptr<IBattleEventsReceiver> battleEvents);
 	void unregisterBattleInterface(std::shared_ptr<IBattleEventsReceiver> battleEvents);

+ 1 - 1
CI/linux/before_install.sh

@@ -8,4 +8,4 @@ sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf
 sudo apt-get install qtbase5-dev
 sudo apt-get install ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev
 # Optional dependencies
-sudo apt-get install libminizip-dev libfuzzylite-dev
+sudo apt-get install libminizip-dev libfuzzylite-dev qttools5-dev

+ 9 - 0
CI/mxe/before_install.sh

@@ -1,5 +1,14 @@
 #!/bin/sh
 
+# steps to upgrade MXE dependencies:
+# 1) Use Debian/Ubuntu system or install one (virtual machines will work too)
+# 2) update following script to include any new dependencies 
+# You can also run it to upgrade existing ones, but don't expect much
+# MXE repository only provides ancient versions for the sake of "stability"
+# https://github.com/vcmi/vcmi-deps-mxe/blob/master/mirror-mxe.sh
+# 3) make release in vcmi-deps-mxe repository using resulting tar archive
+# 4) update paths to tar archive in this script
+
 # Install nsis for installer creation
 sudo add-apt-repository 'deb http://security.ubuntu.com/ubuntu bionic-security main'
 sudo apt-get install -qq nsis ninja-build libssl1.0.0

+ 92 - 35
CMakeLists.txt

@@ -33,10 +33,6 @@ if(APPLE)
 	endif()
 endif()
 
-if(APPLE_IOS)
-	set(BUILD_SINGLE_APP 1)
-endif()
-
 ############################################
 #        User-provided options             #
 ############################################
@@ -52,6 +48,9 @@ option(ENABLE_ERM "Enable compilation of ERM scripting module" OFF)
 option(ENABLE_LUA "Enable compilation of LUA scripting module" OFF)
 option(ENABLE_LAUNCHER "Enable compilation of launcher" ON)
 option(ENABLE_EDITOR "Enable compilation of map editor" ON)
+option(ENABLE_TRANSLATIONS "Enable generation of translations for launcher and editor" ON)
+option(ENABLE_NULLKILLER_AI "Enable compilation of Nullkiller AI library" ON)
+
 if(APPLE_IOS)
 	set(BUNDLE_IDENTIFIER_PREFIX "" CACHE STRING "Bundle identifier prefix")
 	set(APP_DISPLAY_NAME "VCMI" CACHE STRING "App name on the home screen")
@@ -63,7 +62,10 @@ if(NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0")
 endif(NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0")
 option(ENABLE_GITVERSION "Enable Version.cpp with Git commit hash" ON)
 option(ENABLE_DEBUG_CONSOLE "Enable debug console for Windows builds" ON)
+option(ENABLE_STRICT_COMPILATION "Treat all compiler warnings as errors" OFF)
 option(ENABLE_MULTI_PROCESS_BUILDS "Enable /MP flag for MSVS solution" ON)
+option(ENABLE_SINGLE_APP_BUILD "Builds client and server as single executable" OFF)
+option(COPY_CONFIG_ON_BUILD "Copies config folder into output directory at building phase" ON)
 
 # Used for Snap packages and also useful for debugging
 if(NOT APPLE_IOS)
@@ -74,11 +76,20 @@ endif()
 set(PACKAGE_NAME_SUFFIX "" CACHE STRING "Suffix for CPack package name")
 set(PACKAGE_FILE_NAME "" CACHE STRING "Override for CPack package filename")
 
+if(APPLE_IOS AND NOT ENABLE_SINGLE_APP_BUILD)
+	set(ENABLE_SINGLE_APP_BUILD ON)
+endif()
+
 # ERM depends on LUA implicitly
 if(ENABLE_ERM AND NOT ENABLE_LUA)
 	set(ENABLE_LUA ON)
 endif()
 
+# We don't want to deploy assets into build directory for iOS build
+if(APPLE_IOS AND COPY_CONFIG_ON_BUILD)
+	set(COPY_CONFIG_ON_BUILD OFF)
+endif()
+
 ############################################
 #        Miscellaneous options             #
 ############################################
@@ -118,14 +129,22 @@ else()
 endif(ENABLE_GITVERSION)
 
 # Precompiled header configuration
-if(ENABLE_PCH AND NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0")
+if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 6.0 )
+	set(ENABLE_PCH OFF) # broken
+endif()
+
+if( ${CMAKE_VERSION} VERSION_LESS "3.16.0")
+	set(ENABLE_PCH OFF) #not supported
+endif()
+
+if(ENABLE_PCH)
 	macro(enable_pch name)
 		target_precompile_headers(${name} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:<StdInc.h$<ANGLE-R>>)
 	endmacro(enable_pch)
-else(ENABLE_PCH AND NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0")
+else()
 	macro(enable_pch ignore)
 	endmacro(enable_pch)
-endif(ENABLE_PCH AND NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0")
+endif()
 
 ############################################
 #        Documentation section             #
@@ -162,8 +181,8 @@ set(CMAKE_XCODE_ATTRIBUTE_MARKETING_VERSION ${APP_SHORT_VERSION})
 set(CMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH NO)
 set(CMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH[variant=Debug] YES)
 
-if(BUILD_SINGLE_APP)
-	add_compile_definitions(SINGLE_PROCESS_APP=1)
+if(ENABLE_SINGLE_APP_BUILD)
+	add_definitions(-DSINGLE_PROCESS_APP=1)
 endif()
 
 if(APPLE_IOS)
@@ -203,10 +222,18 @@ if(MINGW OR MSVC)
 		# Suppress warnings
 		add_definitions(-D_CRT_SECURE_NO_WARNINGS)
 		add_definitions(-D_SCL_SECURE_NO_WARNINGS)
-		# 4250: 'class1' : inherits 'class2::member' via dominance
-		# 4251: class 'xxx' needs to have dll-interface to be used by clients of class 'yyy'
-		# 4275: non dll-interface class 'xxx' used as base for dll-interface class
-		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj /wd4250 /wd4251 /wd4275")
+
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj")
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4250") # 4250: 'class1' : inherits 'class2::member' via dominance
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4251") # 4251: class 'xxx' needs to have dll-interface to be used by clients of class 'yyy'
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4244") # 4244: conversion from 'xxx' to 'yyy', possible loss of data
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4267") # 4267: conversion from 'xxx' to 'yyy', possible loss of data
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4275") # 4275: non dll-interface class 'xxx' used as base for dll-interface class
+		#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4800") # 4800: implicit conversion from 'xxx' to bool. Possible information loss
+
+		if(ENABLE_STRICT_COMPILATION)
+			set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wx") # Treats all compiler warnings as errors
+		endif()
 
 		if(ENABLE_MULTI_PROCESS_BUILDS)
 			set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP")
@@ -241,15 +268,30 @@ if(MINGW OR MSVC)
 	endif(MINGW)
 endif(MINGW OR MSVC)
 
-if(CMAKE_COMPILER_IS_GNUCXX OR NOT WIN32) #so far all *nix compilers support such parameters
-	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpointer-arith -Wuninitialized")
-	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-strict-aliasing -Wno-switch -Wno-sign-compare -Wno-unused-local-typedefs")
-	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-parameter -Wno-overloaded-virtual -Wno-type-limits -Wno-unknown-pragmas")
-	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-reorder")
-	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-varargs") # fuzzylite - Operation.h
+if(CMAKE_COMPILER_IS_GNUCXX OR NOT WIN32)
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wpointer-arith")
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wuninitialized")
 
-	if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
-		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-mismatched-tags -Wno-unknown-warning-option -Wno-missing-braces")
+	if (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 10.0 OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang" )
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wmismatched-tags")
+	endif()
+
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-parameter")   # low chance of valid reports, a lot of emitted warnings
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-switch")             # large number of false-positives, disabled
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-reorder")            # large number of noise, low chance of any significant issues
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-sign-compare")       # low chance of any significant issues
+	set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-varargs")            # emitted in fuzzylite headers, disabled
+
+	if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 6.0)
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unknown-pragmas") # emitted only by ancient gcc 5.5 in MXE build, remove after upgrade
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-variable") # emitted only by ancient gcc 5.5 in MXE build, remove after upgrade
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-maybe-uninitialized") # emitted only by ancient gcc 5.5 in MXE build, remove after upgrade
+	endif()
+
+	if(ENABLE_STRICT_COMPILATION)
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror")
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error=array-bounds") # false positives in boost::multiarray during release build, keep as warning-only
 	endif()
 
 	if(UNIX)
@@ -270,7 +312,7 @@ if(NOT WIN32 AND NOT APPLE_IOS)
 endif()
 
 if(ENABLE_LUA)
-	add_compile_definitions(SCRIPTING_ENABLED=1)
+	add_definitions(-DSCRIPTING_ENABLED=1)
 endif()
 
 ############################################
@@ -312,12 +354,24 @@ find_package(SDL2_ttf REQUIRED)
 if(TARGET SDL2_ttf::SDL2_ttf)
 	add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf)
 endif()
-find_package(TBB REQUIRED)
 
 if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
 	# Widgets finds its own dependencies (QtGui and QtCore).
 	find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Network)
 	find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Network)
+
+	find_package(QT NAMES Qt6 Qt5 COMPONENTS LinguistTools)
+	find_package(Qt${QT_VERSION_MAJOR} COMPONENTS LinguistTools)
+	if(NOT Qt${QT_VERSION_MAJOR}LinguistTools_DIR)
+		set(ENABLE_TRANSLATIONS OFF)
+	endif()
+	if(ENABLE_TRANSLATIONS)
+		add_definitions(-DENABLE_QT_TRANSLATIONS)
+	endif()
+endif()
+
+if(ENABLE_NULLKILLER_AI)
+	find_package(TBB REQUIRED)
 endif()
 
 if(ENABLE_LUA)
@@ -385,6 +439,12 @@ else()
 		set(BIN_DIR "." CACHE STRING "Where to install binaries")
 		set(LIB_DIR "." CACHE STRING "Where to install main library")
 		set(DATA_DIR "." CACHE STRING "Where to install data files")
+
+		# following constants only used for platforms using XDG (Linux, BSD, etc)
+		add_definitions(-DM_DATA_DIR="${DATA_DIR}")
+		add_definitions(-DM_BIN_DIR="${BIN_DIR}")
+		add_definitions(-DM_LIB_DIR="${LIB_DIR}")
+
 		set(CMAKE_INSTALL_RPATH "$ORIGIN/")
 	else()
 		if(NOT BIN_DIR)
@@ -396,14 +456,14 @@ else()
 		if(NOT DATA_DIR)
 			set(DATA_DIR "${CMAKE_INSTALL_DATAROOTDIR}/vcmi" CACHE STRING "Where to install data files")
 		endif()
-		set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${LIB_DIR}")
-	endif()
 
+		# following constants only used for platforms using XDG (Linux, BSD, etc)
+		add_definitions(-DM_DATA_DIR="${CMAKE_INSTALL_PREFIX}/${DATA_DIR}")
+		add_definitions(-DM_BIN_DIR="${CMAKE_INSTALL_PREFIX}/${BIN_DIR}")
+		add_definitions(-DM_LIB_DIR="${CMAKE_INSTALL_PREFIX}/${LIB_DIR}")
 
-	# following constants only used for platforms using XDG (Linux, BSD, etc)
-	add_definitions(-DM_DATA_DIR="${CMAKE_INSTALL_PREFIX}/${DATA_DIR}")
-	add_definitions(-DM_BIN_DIR="${CMAKE_INSTALL_PREFIX}/${BIN_DIR}")
-	add_definitions(-DM_LIB_DIR="${CMAKE_INSTALL_PREFIX}/${LIB_DIR}")
+		set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${LIB_DIR}")
+	endif()
 endif()
 
 # iOS has flat libs directory structure
@@ -424,13 +484,10 @@ if(APPLE_IOS)
 endif()
 
 include(VCMI_lib)
-if(BUILD_SINGLE_APP)
-	add_subdirectory(lib_client)
+add_subdirectory(lib)
+set(VCMI_LIB_TARGET vcmi)
+if(ENABLE_SINGLE_APP_BUILD)
 	add_subdirectory(lib_server)
-	set(VCMI_LIB_TARGET vcmi_lib_client)
-else()
-	add_subdirectory(lib)
-	set(VCMI_LIB_TARGET vcmi)
 endif()
 
 if(ENABLE_ERM)

+ 1 - 0
CMakePresets.json

@@ -24,6 +24,7 @@
                 "PACKAGE_NAME_SUFFIX" : "$env{VCMI_PACKAGE_NAME_SUFFIX}",
                 "CMAKE_BUILD_TYPE": "RelWithDebInfo",
                 "ENABLE_TEST": "OFF",
+                "ENABLE_STRICT_COMPILATION": "ON",
                 "ENABLE_GITVERSION": "$env{VCMI_PACKAGE_GITVERSION}"
             }
         },

+ 29 - 65
Global.h

@@ -15,25 +15,6 @@
 // Fixed width bool data type is important for serialization
 static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 
-#ifdef __GNUC__
-#  define GCC_VERSION (__GNUC__ * 100 + __GNUC_MINOR__ * 10 + __GNUC_PATCHLEVEL__)
-#endif
-
-#if !defined(__clang__) && defined(__GNUC__) && (GCC_VERSION < 470)
-#  error VCMI requires at least gcc-4.7.2 for successful compilation or clang-3.1. Please update your compiler
-#endif
-
-#if defined(__GNUC__) && (GCC_VERSION == 470 || GCC_VERSION == 471)
-#  error This GCC version has buggy std::array::at version and should not be used. Please update to 4.7.2 or later
-#endif
-
-/* ---------------------------------------------------------------------------- */
-/* Suppress some compiler warnings */
-/* ---------------------------------------------------------------------------- */
-#ifdef _MSC_VER
-#  pragma warning (disable : 4800 ) /* disable conversion to bool warning -- I think it's intended in all places */
-#endif
-
 /* ---------------------------------------------------------------------------- */
 /* System detection. */
 /* ---------------------------------------------------------------------------- */
@@ -83,6 +64,7 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #endif
 
 // Each compiler uses own way to supress fall through warning. Try to find it.
+// TODO: replace with c++17 [[fallthrough]]
 #ifdef __has_cpp_attribute
 #  if __has_cpp_attribute(fallthrough)
 #    define FALLTHROUGH [[fallthrough]];
@@ -101,9 +83,15 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 /* Commonly used C++, Boost headers */
 /* ---------------------------------------------------------------------------- */
 #ifdef VCMI_WINDOWS
-#  define WIN32_LEAN_AND_MEAN		// Exclude rarely-used stuff from Windows headers - delete this line if something is missing.
-#  define NOMINMAX					// Exclude min/max macros from <Windows.h>. Use std::[min/max] from <algorithm> instead.
-#  define _NO_W32_PSEUDO_MODIFIERS  // Exclude more macros for compiling with MinGW on Linux.
+#  ifndef WIN32_LEAN_AND_MEAN
+#    define WIN32_LEAN_AND_MEAN		 // Exclude rarely-used stuff from Windows headers - delete this line if something is missing.
+#  endif
+#  ifndef NOMINMAX
+#    define NOMINMAX				 // Exclude min/max macros from <Windows.h>. Use std::[min/max] from <algorithm> instead.
+#  endif
+#  ifndef _NO_W32_PSEUDO_MODIFIERS
+#    define _NO_W32_PSEUDO_MODIFIERS // Exclude more macros for compiling with MinGW on Linux.
+#  endif
 #endif
 
 #ifdef VCMI_ANDROID
@@ -122,10 +110,6 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #  define STRONG_INLINE inline
 #endif
 
-#define TO_STRING_HELPER(x) #x
-#define TO_STRING(x) TO_STRING_HELPER(x)
-#define LINE_IN_FILE __FILE__ ":" TO_STRING(__LINE__)
-
 #define _USE_MATH_DEFINES
 
 #include <cstdio>
@@ -233,7 +217,10 @@ typedef boost::lock_guard<boost::recursive_mutex> TLockGuardRec;
 /* ---------------------------------------------------------------------------- */
 // Import + Export macro declarations
 #ifdef VCMI_WINDOWS
-#  ifdef __GNUC__
+#ifdef VCMI_DLL_STATIC
+#    define DLL_IMPORT
+#    define DLL_EXPORT
+#elif defined(__GNUC__)
 #    define DLL_IMPORT __attribute__((dllimport))
 #    define DLL_EXPORT __attribute__((dllexport))
 #  else
@@ -262,7 +249,8 @@ template<typename T, size_t N> char (&_ArrayCountObj(const T (&)[N]))[N];
 #define ARRAY_COUNT(arr)    (sizeof(_ArrayCountObj(arr)))
 
 // should be used for variables that becomes unused in release builds (e.g. only used for assert checks)
-#define UNUSED(VAR) ((void)VAR)
+// TODO: replace with c++17 [[maybe_unused]]
+#define MAYBE_UNUSED(VAR) ((void)VAR)
 
 // old iOS SDKs compatibility
 #ifdef VCMI_IOS
@@ -503,36 +491,6 @@ namespace vstd
 		ptr = nullptr;
 	}
 
-#if _MSC_VER >= 1800
-	using std::make_unique;
-#else
-	template<typename T>
-	std::unique_ptr<T> make_unique()
-	{
-		return std::unique_ptr<T>(new T());
-	}
-	template<typename T, typename Arg1>
-	std::unique_ptr<T> make_unique(Arg1 &&arg1)
-	{
-		return std::unique_ptr<T>(new T(std::forward<Arg1>(arg1)));
-	}
-	template<typename T, typename Arg1, typename Arg2>
-	std::unique_ptr<T> make_unique(Arg1 &&arg1, Arg2 &&arg2)
-	{
-		return std::unique_ptr<T>(new T(std::forward<Arg1>(arg1), std::forward<Arg2>(arg2)));
-	}
-	template<typename T, typename Arg1, typename Arg2, typename Arg3>
-	std::unique_ptr<T> make_unique(Arg1 &&arg1, Arg2 &&arg2, Arg3 &&arg3)
-	{
-		return std::unique_ptr<T>(new T(std::forward<Arg1>(arg1), std::forward<Arg2>(arg2), std::forward<Arg3>(arg3)));
-	}
-	template<typename T, typename Arg1, typename Arg2, typename Arg3, typename Arg4>
-	std::unique_ptr<T> make_unique(Arg1 &&arg1, Arg2 &&arg2, Arg3 &&arg3, Arg4 &&arg4)
-	{
-		return std::unique_ptr<T>(new T(std::forward<Arg1>(arg1), std::forward<Arg2>(arg2), std::forward<Arg3>(arg3), std::forward<Arg4>(arg4)));
-	}
-#endif
-
 	template <typename Container>
 	typename Container::const_reference circularAt(const Container &r, size_t index)
 	{
@@ -543,6 +501,12 @@ namespace vstd
 		return *itr;
 	}
 
+	template <typename Container, typename Item>
+	void erase(Container &c, const Item &item)
+	{
+		c.erase(boost::remove(c, item), c.end());
+	}
+
 	template<typename Range, typename Predicate>
 	void erase_if(Range &vec, Predicate pred)
 	{
@@ -704,12 +668,6 @@ namespace vstd
 		return false;
 	}
 
-	template <typename Container, typename Pred>
-	void erase(Container &c, Pred pred)
-	{
-		c.erase(boost::remove_if(c, pred), c.end());
-	}
-
 	template<typename T>
 	void removeDuplicates(std::vector<T> &vec)
 	{
@@ -760,10 +718,16 @@ namespace vstd
 		return v;
 	}
 
+	//c++20 feature
+	template<typename Arithmetic, typename Floating>
+	Arithmetic lerp(const Arithmetic & a, const Arithmetic & b, const Floating & f)
+	{
+		return a + (b - a) * f;
+	}
+
 	using boost::math::round;
 }
 using vstd::operator-=;
-using vstd::make_unique;
 
 #ifdef NO_STD_TOSTRING
 namespace std

+ 2 - 1
README.md

@@ -1,7 +1,8 @@
 [![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml)
 [![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/github/vcmi/vcmi?branch=develop&svg=true)](https://ci.appveyor.com/project/vcmi/vcmi)
 [![Coverity Scan Build Status](https://scan.coverity.com/projects/vcmi/badge.svg)](https://scan.coverity.com/projects/vcmi)
-[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.0.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.0.0)
+[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.1.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.1.0)
+[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
 # VCMI Project
 VCMI is work-in-progress attempt to recreate engine for Heroes III, giving it new and extended possibilities.
 

+ 29 - 15
client/CBitmapHandler.cpp

@@ -20,7 +20,7 @@ namespace BitmapHandler
 {
 	SDL_Surface * loadH3PCX(ui8 * data, size_t size);
 
-	SDL_Surface * loadBitmapFromDir(std::string path, std::string fname, bool setKey=true);
+	SDL_Surface * loadBitmapFromDir(std::string path, std::string fname);
 }
 
 bool isPCX(const ui8 *header)//check whether file can be PCX according to header
@@ -102,7 +102,7 @@ SDL_Surface * BitmapHandler::loadH3PCX(ui8 * pcx, size_t size)
 	return ret;
 }
 
-SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fname, bool setKey)
+SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fname)
 {
 	if(!fname.size())
 	{
@@ -121,14 +121,7 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fna
 	if (isPCX(readFile.first.get()))
 	{//H3-style PCX
 		ret = loadH3PCX(readFile.first.get(), readFile.second);
-		if (ret)
-		{
-			if(ret->format->BytesPerPixel == 1  &&  setKey)
-			{
-				CSDL_Ext::setColorKey(ret,ret->format->palette->colors[0]);
-			}
-		}
-		else
+		if (!ret)
 		{
 			logGlobal->error("Failed to open %s as H3 PCX!", fname);
 			return nullptr;
@@ -144,7 +137,8 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fna
 		{
 			if (ret->format->palette)
 			{
-				//set correct value for alpha\unused channel
+				// set correct value for alpha\unused channel
+				// NOTE: might be unnecessary with SDL2
 				for (int i=0; i < ret->format->palette->ncolors; i++)
 					ret->format->palette->colors[i].a = SDL_ALPHA_OPAQUE;
 			}
@@ -161,7 +155,26 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fna
 	// 1) Vampire mansion in Necropolis (not 1st color is transparent)
 	// 2) Battle background when fighting on grass/dirt, topmost sky part (NO transparent color)
 	// 3) New objects that may use 24-bit images for icons (e.g. witchking arts)
-	if (ret->format->palette)
+	// 4) special case - there are 2 .bmp images that have semi-transparency (CCELLGRD.BMP & CCELLSHD.BMP)
+	if (ret->format->palette &&
+		ret->format->palette->colors[0].r == 255 &&
+		ret->format->palette->colors[0].g ==   0 &&
+		ret->format->palette->colors[0].b == 255 )
+	{
+		static SDL_Color shadow[3] =
+		{
+			{   0,   0,   0,   0},// 100% - transparency
+			{   0,   0,   0,  32},//  75% - shadow border,
+			{   0,   0,   0, 128},//  50% - shadow body
+		};
+
+		CSDL_Ext::setColorKey(ret, ret->format->palette->colors[0]);
+
+		ret->format->palette->colors[0] = shadow[0];
+		ret->format->palette->colors[1] = shadow[1];
+		ret->format->palette->colors[4] = shadow[2];
+	}
+	else if (ret->format->palette)
 	{
 		CSDL_Ext::setDefaultColorKeyPresize(ret);
 	}
@@ -173,15 +186,16 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fna
 	{
 		CSDL_Ext::setDefaultColorKey(ret);
 	}
+
 	return ret;
 }
 
-SDL_Surface * BitmapHandler::loadBitmap(std::string fname, bool setKey)
+SDL_Surface * BitmapHandler::loadBitmap(std::string fname)
 {
 	SDL_Surface * bitmap = nullptr;
 
-	if (!(bitmap = loadBitmapFromDir("DATA/", fname, setKey)) &&
-		!(bitmap = loadBitmapFromDir("SPRITES/", fname, setKey)))
+	if (!(bitmap = loadBitmapFromDir("DATA/", fname)) &&
+		!(bitmap = loadBitmapFromDir("SPRITES/", fname)))
 	{
 		logGlobal->error("Error: Failed to find file %s", fname);
 	}

+ 1 - 1
client/CBitmapHandler.h

@@ -14,5 +14,5 @@ struct SDL_Surface;
 namespace BitmapHandler
 {
 	//Load file from /DATA or /SPRITES
-	SDL_Surface * loadBitmap(std::string fname, bool setKey=true);
+	SDL_Surface * loadBitmap(std::string fname);
 }

+ 66 - 87
client/CMT.cpp

@@ -468,10 +468,9 @@ int main(int argc, char * argv[])
 	if(!settings["session"]["headless"].Bool())
 	{
 		pomtime.getDiff();
-		CCS->curh = new CCursorHandler();
-		graphics = new Graphics(); // should be before curh->init()
+		graphics = new Graphics(); // should be before curh
 
-		CCS->curh->initCursor();
+		CCS->curh = new CCursorHandler();
 		logGlobal->info("Screen handler: %d ms", pomtime.getDiff());
 		pomtime.getDiff();
 
@@ -585,6 +584,7 @@ void removeGUI()
 	GH.curInt = nullptr;
 	if(GH.topInt())
 		GH.topInt()->deactivate();
+	adventureInt = nullptr;
 	GH.listInt.clear();
 	GH.objsToBlit.clear();
 	GH.statusbar = nullptr;
@@ -664,36 +664,7 @@ void processCommand(const std::string &message)
 //	}
 	else if(message=="convert txt")
 	{
-		std::cout << "Command accepted.\t";
-
-		const bfs::path outPath =
-			VCMIDirs::get().userExtractedPath();
-
-		bfs::create_directories(outPath);
-
-		auto extractVector = [=](const std::vector<std::string> & source, const std::string & name)
-		{
-			JsonNode data(JsonNode::JsonType::DATA_VECTOR);
-			size_t index = 0;
-			for(auto & line : source)
-			{
-				JsonNode lineNode(JsonNode::JsonType::DATA_STRUCT);
-				lineNode["text"].String() = line;
-				lineNode["index"].Integer() = index++;
-				data.Vector().push_back(lineNode);
-			}
-
-			const bfs::path filePath = outPath / (name + ".json");
-			bfs::ofstream file(filePath);
-			file << data.toJson();
-		};
-
-		extractVector(VLC->generaltexth->allTexts, "generalTexts");
-		extractVector(VLC->generaltexth->jktexts, "jkTexts");
-		extractVector(VLC->generaltexth->arraytxt, "arrayTexts");
-
-		std::cout << "\rExtracting done :)\n";
-		std::cout << " Extracted files can be found in " << outPath << " directory\n";
+		VLC->generaltexth->dumpAllTexts();
 	}
 	else if(message=="get config")
 	{
@@ -878,7 +849,7 @@ void processCommand(const std::string &message)
 	{
 		std::string URI;
 		readed >> URI;
-		std::unique_ptr<CAnimation> anim = make_unique<CAnimation>(URI);
+		std::unique_ptr<CAnimation> anim = std::make_unique<CAnimation>(URI);
 		anim->preload();
 		anim->exportBitmaps(VCMIDirs::get().userExtractedPath());
 	}
@@ -1086,7 +1057,6 @@ static bool recreateWindow(int w, int h, int bpp, bool fullscreen, int displayIn
 	if(!checkVideoMode(displayIndex, w, h))
 	{
 		logGlobal->error("Error: SDL says that %dx%d resolution is not available!", w, h);
-		return false;
 	}
 #endif
 
@@ -1508,80 +1478,89 @@ static void mainLoop()
 	}
 }
 
-void handleQuit(bool ask)
+static void quitApplication()
 {
-	auto quitApplication = []()
+	if(!settings["session"]["headless"].Bool())
 	{
-		if(!settings["session"]["headless"].Bool())
-		{
-			if(CSH->client)
-				CSH->endGameplay();
-		}
+		if(CSH->client)
+			CSH->endGameplay();
+	}
 
-		GH.listInt.clear();
-		GH.objsToBlit.clear();
+	GH.listInt.clear();
+	GH.objsToBlit.clear();
 
-		CMM.reset();
+	CMM.reset();
 
-		if(!settings["session"]["headless"].Bool())
+	if(!settings["session"]["headless"].Bool())
+	{
+		// cleanup, mostly to remove false leaks from analyzer
+		if(CCS)
 		{
-			// cleanup, mostly to remove false leaks from analyzer
-			if(CCS)
-			{
-				CCS->musich->release();
-				CCS->soundh->release();
-
-				vstd::clear_pointer(CCS);
-			}
-			CMessage::dispose();
+			CCS->musich->release();
+			CCS->soundh->release();
 
-			vstd::clear_pointer(graphics);
+			vstd::clear_pointer(CCS);
 		}
+		CMessage::dispose();
 
-		vstd::clear_pointer(VLC);
+		vstd::clear_pointer(graphics);
+	}
 
-		vstd::clear_pointer(console);// should be removed after everything else since used by logging
+	vstd::clear_pointer(VLC);
 
-		boost::this_thread::sleep(boost::posix_time::milliseconds(750));//???
-		if(!settings["session"]["headless"].Bool())
-		{
-			if(settings["general"]["notifications"].Bool())
-			{
-				NotificationHandler::destroy();
-			}
+	vstd::clear_pointer(console);// should be removed after everything else since used by logging
 
-			cleanupRenderer();
-
-			if(nullptr != mainRenderer)
-			{
-				SDL_DestroyRenderer(mainRenderer);
-				mainRenderer = nullptr;
-			}
+	boost::this_thread::sleep(boost::posix_time::milliseconds(750));//???
+	if(!settings["session"]["headless"].Bool())
+	{
+		if(settings["general"]["notifications"].Bool())
+		{
+			NotificationHandler::destroy();
+		}
 
-			if(nullptr != mainWindow)
-			{
-				SDL_DestroyWindow(mainWindow);
-				mainWindow = nullptr;
-			}
+		cleanupRenderer();
 
-			SDL_Quit();
+		if(nullptr != mainRenderer)
+		{
+			SDL_DestroyRenderer(mainRenderer);
+			mainRenderer = nullptr;
 		}
 
-		if(logConfig != nullptr)
+		if(nullptr != mainWindow)
 		{
-			logConfig->deconfigure();
-			delete logConfig;
-			logConfig = nullptr;
+			SDL_DestroyWindow(mainWindow);
+			mainWindow = nullptr;
 		}
 
-		std::cout << "Ending...\n";
-		exit(0);
-	};
+		SDL_Quit();
+	}
+
+	if(logConfig != nullptr)
+	{
+		logConfig->deconfigure();
+		delete logConfig;
+		logConfig = nullptr;
+	}
+
+	std::cout << "Ending...\n";
+	exit(0);
+}
+
+void handleQuit(bool ask)
+{
 
 	if(CSH->client && LOCPLINT && ask)
 	{
-		CCS->curh->changeGraphic(ECursor::ADVENTURE, 0);
-		LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr);
+		CCS->curh->set(Cursor::Map::POINTER);
+		LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], [](){
+			// Workaround for assertion failure on exit:
+			// handleQuit() is alway called during SDL event processing
+			// during which, eventsM is kept locked
+			// this leads to assertion failure if boost::mutex is in locked state
+			eventsM.unlock();
+
+			quitApplication();
+		}, nullptr);
 	}
 	else
 	{

+ 38 - 10
client/CMakeLists.txt

@@ -2,19 +2,31 @@ set(client_SRCS
 		StdInc.cpp
 		../CCallback.cpp
 
-		battle/CBattleAnimations.cpp
-		battle/CBattleInterfaceClasses.cpp
-		battle/CBattleInterface.cpp
-		battle/CCreatureAnimation.cpp
+		battle/BattleActionsController.cpp
+		battle/BattleAnimationClasses.cpp
+		battle/BattleEffectsController.cpp
+		battle/BattleFieldController.cpp
+		battle/BattleInterfaceClasses.cpp
+		battle/BattleInterface.cpp
+		battle/BattleObstacleController.cpp
+		battle/BattleProjectileController.cpp
+		battle/BattleRenderer.cpp
+		battle/BattleSiegeController.cpp
+		battle/BattleStacksController.cpp
+		battle/BattleWindow.cpp
+		battle/CreatureAnimation.cpp
 
 		gui/CAnimation.cpp
+		gui/Canvas.cpp
 		gui/CCursorHandler.cpp
 		gui/CGuiHandler.cpp
 		gui/CIntObject.cpp
+		gui/ColorFilter.cpp
 		gui/Fonts.cpp
 		gui/Geometries.cpp
 		gui/SDL_Extensions.cpp
 		gui/NotificationHandler.cpp
+		gui/InterfaceObjectConfigurable.cpp
 
 		widgets/AdventureMapClasses.cpp
 		widgets/Buttons.cpp
@@ -75,14 +87,26 @@ set(client_SRCS
 set(client_HEADERS
 		StdInc.h
 
-		battle/CBattleAnimations.h
-		battle/CBattleInterfaceClasses.h
-		battle/CBattleInterface.h
-		battle/CCreatureAnimation.h
+		battle/BattleActionsController.h
+		battle/BattleAnimationClasses.h
+		battle/BattleEffectsController.h
+		battle/BattleFieldController.h
+		battle/BattleInterfaceClasses.h
+		battle/BattleInterface.h
+		battle/BattleObstacleController.h
+		battle/BattleProjectileController.h
+		battle/BattleRenderer.h
+		battle/BattleSiegeController.h
+		battle/BattleStacksController.h
+		battle/BattleWindow.h
+		battle/CreatureAnimation.h
+		battle/BattleConstants.h
 
 		gui/CAnimation.h
+		gui/Canvas.h
 		gui/CCursorHandler.h
 		gui/CGuiHandler.h
+		gui/ColorFilter.h
 		gui/CIntObject.h
 		gui/Fonts.h
 		gui/Geometries.h
@@ -90,6 +114,7 @@ set(client_HEADERS
 		gui/SDL_Extensions.h
 		gui/SDL_Pixels.h
 		gui/NotificationHandler.h
+		gui/InterfaceObjectConfigurable.h
 
 		widgets/AdventureMapClasses.h
 		widgets/Buttons.h
@@ -177,7 +202,10 @@ else()
 	add_executable(vcmiclient WIN32 ${client_SRCS} ${client_HEADERS} ${client_ICON})
 endif(ENABLE_DEBUG_CONSOLE)
 
-add_dependencies(vcmiclient vcmiserver BattleAI StupidAI VCAI Nullkiller)
+add_dependencies(vcmiclient vcmiserver BattleAI StupidAI VCAI)
+if(ENABLE_NULLKILLER_AI)
+	add_dependencies(vcmiclient Nullkiller)
+endif()
 if(APPLE_IOS)
 	if(ENABLE_ERM)
 		add_dependencies(vcmiclient vcmiERM)
@@ -243,7 +271,7 @@ elseif(APPLE_IOS)
 	set(CMAKE_EXE_LINKER_FLAGS "-Wl,-e,_client_main")
 endif()
 
-if(BUILD_SINGLE_APP)
+if(ENABLE_SINGLE_APP_BUILD)
 	target_link_libraries(vcmiclient PRIVATE vcmiserver)
 	if(ENABLE_LAUNCHER)
 		target_link_libraries(vcmiclient PRIVATE vcmilauncher)

+ 1 - 1
client/CMessage.cpp

@@ -71,7 +71,7 @@ void CMessage::init()
 {
 	for(int i=0; i<PlayerColor::PLAYER_LIMIT_I; i++)
 	{
-		dialogBorders[i] = make_unique<CAnimation>("DIALGBOX");
+		dialogBorders[i] = std::make_unique<CAnimation>("DIALGBOX");
 		dialogBorders[i]->preload();
 
 		for(int j=0; j < dialogBorders[i]->size(0); j++)

+ 1 - 1
client/CMusicHandler.cpp

@@ -458,7 +458,7 @@ void CMusicHandler::queueNext(CMusicHandler *owner, const std::string & setName,
 {
 	try
 	{
-		queueNext(make_unique<MusicEntry>(owner, setName, musicURI, looped, fromStart));
+		queueNext(std::make_unique<MusicEntry>(owner, setName, musicURI, looped, fromStart));
 	}
 	catch(std::exception &e)
 	{

+ 87 - 164
client/CPlayerInterface.cpp

@@ -12,8 +12,11 @@
 #include <vcmi/Artifact.h>
 
 #include "windows/CAdvmapInterface.h"
-#include "battle/CBattleInterface.h"
-#include "battle/CBattleInterfaceClasses.h"
+#include "battle/BattleInterface.h"
+#include "battle/BattleEffectsController.h"
+#include "battle/BattleFieldController.h"
+#include "battle/BattleInterfaceClasses.h"
+#include "battle/BattleWindow.h"
 #include "../CCallback.h"
 #include "windows/CCastleInterface.h"
 #include "gui/CCursorHandler.h"
@@ -29,7 +32,6 @@
 #include "windows/CTradeWindow.h"
 #include "windows/CSpellWindow.h"
 #include "../lib/CConfigHandler.h"
-#include "battle/CCreatureAnimation.h"
 #include "Graphics.h"
 #include "windows/GUIClasses.h"
 #include "../lib/CArtHandler.h"
@@ -55,6 +57,7 @@
 #include "../lib/CPlayerState.h"
 #include "../lib/GameConstants.h"
 #include "gui/CGuiHandler.h"
+#include "gui/CAnimation.h"
 #include "windows/InfoWindows.h"
 #include "../lib/UnlockGuard.h"
 #include "../lib/CPathfinder.h"
@@ -90,7 +93,7 @@ boost::recursive_mutex * CPlayerInterface::pim = new boost::recursive_mutex;
 
 CPlayerInterface * LOCPLINT;
 
-CBattleInterface * CPlayerInterface::battleInt;
+std::shared_ptr<BattleInterface> CPlayerInterface::battleInt;
 
 enum  EMoveState {STOP_MOVE, WAITING_MOVE, CONTINUE_MOVE, DURING_MOVE};
 CondSh<EMoveState> stillMoveHero(STOP_MOVE); //used during hero movement
@@ -139,14 +142,16 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player)
 
 CPlayerInterface::~CPlayerInterface()
 {
-	if(CCS->soundh) CCS->soundh->ambientStopAllChannels();
+	if(CCS && CCS->soundh)
+		CCS->soundh->ambientStopAllChannels();
+
 	logGlobal->trace("\tHuman player interface for player %s being destructed", playerID.getStr());
 	delete showingDialog;
 	delete cingconsole;
 	if (LOCPLINT == this)
 		LOCPLINT = nullptr;
 }
-void CPlayerInterface::init(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
+void CPlayerInterface::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
 {
 	cb = CB;
 	env = ENV;
@@ -264,8 +269,8 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose)
 				assert(adventureInt->terrain.currentPath->nodes.size() >= 2);
 				std::vector<CGPathNode>::const_iterator nodesIt = adventureInt->terrain.currentPath->nodes.end() - 1;
 
-				if((nodesIt)->coord == CGHeroInstance::convertPosition(details.start, false)
-					&& (nodesIt - 1)->coord == CGHeroInstance::convertPosition(details.end, false))
+				if((nodesIt)->coord == hero->convertToVisitablePos(details.start)
+					&& (nodesIt - 1)->coord == hero->convertToVisitablePos(details.end))
 				{
 					//path was between entrance and exit of teleport -> OK, erase node as usual
 					removeLastNodeFromPath(hero);
@@ -689,7 +694,7 @@ void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet
 	if (settings["adventure"]["quickCombat"].Bool())
 	{
 		autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
-		autofightingAI->init(env, cb);
+		autofightingAI->initBattleInterface(env, cb);
 		autofightingAI->battleStart(army1, army2, int3(0,0,0), hero1, hero2, side);
 		isAutoFightOn = true;
 		cb->registerBattleInterface(autofightingAI);
@@ -704,7 +709,7 @@ void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet
 	BATTLE_EVENT_POSSIBLE_RETURN;
 }
 
-void CPlayerInterface::battleUnitsChanged(const std::vector<UnitChanges> & units, const std::vector<CustomEffectInfo> & customEffects)
+void CPlayerInterface::battleUnitsChanged(const std::vector<UnitChanges> & units)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
@@ -715,34 +720,14 @@ void CPlayerInterface::battleUnitsChanged(const std::vector<UnitChanges> & units
 		{
 		case UnitChanges::EOperation::RESET_STATE:
 			{
-				const battle::Unit * unit = cb->battleGetUnitByID(info.id);
+				const CStack * stack = cb->battleGetStackByID(info.id );
 
-				if(!unit)
+				if(!stack)
 				{
 					logGlobal->error("Invalid unit ID %d", info.id);
 					continue;
 				}
-
-				auto iter = battleInt->creAnims.find(info.id);
-
-				if(iter == battleInt->creAnims.end())
-				{
-					logGlobal->error("Unit %d have no animation", info.id);
-					continue;
-				}
-
-				auto animation = iter->second;
-
-				if(unit->alive() && animation->isDead())
-					animation->setType(CCreatureAnim::HOLDING);
-
-				if (unit->isClone())
-				{
-					std::unique_ptr<ColorShifterDeepBlue> shifter(new ColorShifterDeepBlue());
-					animation->shiftColor(shifter.get());
-				}
-
-				//TODO: handle more cases
+				battleInt->stackReset(stack);
 			}
 			break;
 		case UnitChanges::EOperation::REMOVE:
@@ -756,7 +741,7 @@ void CPlayerInterface::battleUnitsChanged(const std::vector<UnitChanges> & units
 					logGlobal->error("Invalid unit ID %d", info.id);
 					continue;
 				}
-				battleInt->unitAdded(unit);
+				battleInt->stackAdded(unit);
 			}
 			break;
 		default:
@@ -764,8 +749,6 @@ void CPlayerInterface::battleUnitsChanged(const std::vector<UnitChanges> & units
 			break;
 		}
 	}
-
-	battleInt->displayCustomEffects(customEffects);
 }
 
 void CPlayerInterface::battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles)
@@ -773,7 +756,7 @@ void CPlayerInterface::battleObstaclesChanged(const std::vector<ObstacleChanges>
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
 
-	bool needUpdate = false;
+	std::vector<std::shared_ptr<const CObstacleInstance>> newObstacles;
 
 	for(auto & change : obstacles)
 	{
@@ -781,19 +764,16 @@ void CPlayerInterface::battleObstaclesChanged(const std::vector<ObstacleChanges>
 		{
 			auto instance = cb->battleGetObstacleByID(change.id);
 			if(instance)
-				battleInt->obstaclePlaced(*instance);
+				newObstacles.push_back(instance);
 			else
 				logNetwork->error("Invalid obstacle instance %d", change.id);
 		}
-		else
-		{
-			needUpdate = true;
-		}
 	}
 
-	if(needUpdate)
-		//update accessible hexes
-		battleInt->redrawBackgroundWithHexes(battleInt->activeStack);
+	if (!newObstacles.empty())
+		battleInt->obstaclePlaced(newObstacles);
+
+	battleInt->fieldController->redrawBackgroundWithHexes();
 }
 
 void CPlayerInterface::battleCatapultAttacked(const CatapultAttack & ca)
@@ -858,36 +838,36 @@ BattleAction CPlayerInterface::activeStack(const CStack * stack) //called when i
 		autofightingAI.reset();
 	}
 
-	CBattleInterface *b = battleInt;
+	assert(battleInt);
 
-	if(!b)
+	if(!battleInt)
 	{
 		return BattleAction::makeDefend(stack); // probably battle is finished already
 	}
 
-	if(CBattleInterface::givenCommand.get())
+	if(BattleInterface::givenCommand.get())
 	{
 		logGlobal->error("Command buffer must be clean! (we don't want to use old command)");
-		vstd::clear_pointer(CBattleInterface::givenCommand.data);
+		vstd::clear_pointer(BattleInterface::givenCommand.data);
 	}
 
 	{
 		boost::unique_lock<boost::recursive_mutex> un(*pim);
-		b->stackActivated(stack);
+		battleInt->stackActivated(stack);
 		//Regeneration & mana drain go there
 	}
 	//wait till BattleInterface sets its command
-	boost::unique_lock<boost::mutex> lock(CBattleInterface::givenCommand.mx);
-	while(!CBattleInterface::givenCommand.data)
+	boost::unique_lock<boost::mutex> lock(BattleInterface::givenCommand.mx);
+	while(!BattleInterface::givenCommand.data)
 	{
-		CBattleInterface::givenCommand.cond.wait(lock);
+		BattleInterface::givenCommand.cond.wait(lock);
 		if (!battleInt) //battle ended while we were waiting for movement (eg. because of spell)
 			throw boost::thread_interrupted(); //will shut the thread peacefully
 	}
 
 	//tidy up
-	BattleAction ret = *(CBattleInterface::givenCommand.data);
-	vstd::clear_pointer(CBattleInterface::givenCommand.data);
+	BattleAction ret = *(BattleInterface::givenCommand.data);
+	vstd::clear_pointer(BattleInterface::givenCommand.data);
 
 	if(ret.actionType == EActionType::CANCEL)
 	{
@@ -912,7 +892,7 @@ void CPlayerInterface::battleEnd(const BattleResult *br)
 
 		if(!battleInt)
 		{
-			GH.pushIntT<CBattleResultWindow>(*br, *this);
+			GH.pushIntT<BattleResultWindow>(*br, *this);
 			// #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it.
 			// Otherwise NewTurn causes freeze.
 			waitWhileDialog();
@@ -935,12 +915,12 @@ void CPlayerInterface::battleLogMessage(const std::vector<MetaString> & lines)
 	battleInt->displayBattleLog(lines);
 }
 
-void CPlayerInterface::battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance)
+void CPlayerInterface::battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
 
-	battleInt->stackMoved(stack, dest, distance);
+	battleInt->stackMoved(stack, dest, distance, teleport);
 }
 void CPlayerInterface::battleSpellCast( const BattleSpellCast *sc )
 {
@@ -962,9 +942,9 @@ void CPlayerInterface::battleTriggerEffect (const BattleTriggerEffect & bte)
 	//TODO why is this different (no return on LOPLINT != this) ?
 
 	RETURN_IF_QUICK_COMBAT;
-	battleInt->battleTriggerEffect(bte);
+	battleInt->effectsController->battleTriggerEffect(bte);
 }
-void CPlayerInterface::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa)
+void CPlayerInterface::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
@@ -974,24 +954,25 @@ void CPlayerInterface::battleStacksAttacked(const std::vector<BattleStackAttacke
 	{
 		const CStack * defender = cb->battleGetStackByID(elem.stackAttacked, false);
 		const CStack * attacker = cb->battleGetStackByID(elem.attackerID, false);
-		if(elem.isEffect())
-		{
-			if(defender && !elem.isSecondary())
-				battleInt->displayEffect(elem.effect, defender->getPosition());
-		}
-		if(elem.isSpell())
-		{
-			if(defender)
-				battleInt->displaySpellEffect(elem.spellID, defender->getPosition());
-		}
-		//FIXME: why action is deleted during enchanter cast?
-		bool remoteAttack = false;
 
-		if(LOCPLINT->curAction)
-			remoteAttack |= LOCPLINT->curAction->actionType != EActionType::WALK_AND_ATTACK;
+		assert(defender);
+
+		StackAttackedInfo     info;
+		info.defender       = defender;
+		info.attacker       = attacker;
+		info.damageDealt    = elem.damageAmount;
+		info.amountKilled   = elem.killedAmount;
+		info.spellEffect    = SpellID::NONE;
+		info.indirectAttack = ranged;
+		info.killed         = elem.killed();
+		info.rebirth        = elem.willRebirth();
+		info.cloneKilled    = elem.cloneKilled();
+		info.fireShield     = elem.fireShield();
 
-		StackAttackedInfo to_put = {defender, elem.damageAmount, elem.killedAmount, attacker, remoteAttack, elem.killed(), elem.willRebirth(), elem.cloneKilled()};
-		arg.push_back(to_put);
+		if (elem.isSpell())
+			info.spellEffect = elem.spellID;
+
+		arg.push_back(info);
 	}
 	battleInt->stacksAreAttacked(arg);
 }
@@ -1002,96 +983,36 @@ void CPlayerInterface::battleAttack(const BattleAttack * ba)
 
 	assert(curAction);
 
-	const CStack * attacker = cb->battleGetStackByID(ba->stackAttacking);
-
-	if(!attacker)
-	{
-		logGlobal->error("Attacking stack not found");
-		return;
-	}
-
-	if(ba->lucky()) //lucky hit
-	{
-		battleInt->console->addText(attacker->formatGeneralMessage(-45));
-		battleInt->displayEffect(18, attacker->getPosition());
-		CCS->soundh->playSound(soundBase::GOODLUCK);
-	}
-	if(ba->unlucky()) //unlucky hit
-	{
-		battleInt->console->addText(attacker->formatGeneralMessage(-44));
-		battleInt->displayEffect(48, attacker->getPosition());
-		CCS->soundh->playSound(soundBase::BADLUCK);
-	}
-	if(ba->deathBlow())
-	{
-		battleInt->console->addText(attacker->formatGeneralMessage(365));
-		for(auto & elem : ba->bsa)
-		{
-			const CStack * attacked = cb->battleGetStackByID(elem.stackAttacked);
-			battleInt->displayEffect(73, attacked->getPosition());
-		}
-		CCS->soundh->playSound(soundBase::deathBlow);
-	}
-
-	battleInt->displayCustomEffects(ba->customEffects);
+	StackAttackInfo info;
+	info.attacker = cb->battleGetStackByID(ba->stackAttacking);
+	info.defender = nullptr;
+	info.indirectAttack = ba->shot();
+	info.lucky = ba->lucky();
+	info.unlucky = ba->unlucky();
+	info.deathBlow = ba->deathBlow();
+	info.lifeDrain = ba->lifeDrain();
+	info.tile = ba->tile;
+	info.spellEffect = SpellID::NONE;
 
-	battleInt->waitForAnims();
+	if (ba->spellLike())
+		info.spellEffect = ba->spellID;
 
-	auto actionTarget = curAction->getTarget(cb.get());
-
-	if(actionTarget.empty() || (actionTarget.size() < 2 && !ba->shot()))
-	{
-		logNetwork->error("Invalid current action: no destination.");
-		return;
-	}
-
-	if(ba->shot())
+	for(auto & elem : ba->bsa)
 	{
-		for(auto & elem : ba->bsa)
+		if(!elem.isSecondary())
 		{
-			if(!elem.isSecondary()) //display projectile only for primary target
-			{
-				const CStack * attacked = cb->battleGetStackByID(elem.stackAttacked);
-				battleInt->stackAttacking(attacker, attacked->getPosition(), attacked, true);
-			}
+			assert(info.defender == nullptr);
+			info.defender = cb->battleGetStackByID(elem.stackAttacked);
 		}
-	}
-	else
-	{
-		auto attackTarget = actionTarget.at(1).hexValue;
-
-		//TODO: use information from BattleAttack but not curAction
-
-		int shift = 0;
-		if(ba->counter() && BattleHex::mutualPosition(attackTarget, attacker->getPosition()) < 0)
-		{
-			int distp = BattleHex::getDistance(attackTarget + 1, attacker->getPosition());
-			int distm = BattleHex::getDistance(attackTarget - 1, attacker->getPosition());
-
-			if(distp < distm)
-				shift = 1;
-			else
-				shift = -1;
-		}
-
-		if(!ba->bsa.empty())
+		else
 		{
-			const CStack * attacked = cb->battleGetStackByID(ba->bsa.begin()->stackAttacked);
-			battleInt->stackAttacking(attacker, ba->counter() ? BattleHex(attackTarget + shift) : attackTarget, attacked, false);
+			info.secondaryDefender.push_back(cb->battleGetStackByID(elem.stackAttacked));
 		}
 	}
+	assert(info.defender != nullptr);
+	assert(info.attacker != nullptr);
 
-	//battleInt->waitForAnims(); //FIXME: freeze
-
-	if(ba->spellLike())
-	{
-		//TODO: use information from BattleAttack but not curAction
-
-		auto destination = actionTarget.at(0).hexValue;
-		//display hit animation
-		SpellID spellID = ba->spellID;
-		battleInt->displaySpellHit(spellID, destination);
-	}
+	battleInt->stackAttacking(info);
 }
 
 void CPlayerInterface::battleGateStateChanged(const EGateState state)
@@ -1573,7 +1494,7 @@ void CPlayerInterface::newObject( const CGObjectInstance * obj )
 	//we might have built a boat in shipyard in opened town screen
 	if (obj->ID == Obj::BOAT
 		&& LOCPLINT->castleInt
-		&&  obj->pos-obj->getVisitableOffset() == LOCPLINT->castleInt->town->bestLocation())
+		&&  obj->visitablePos() == LOCPLINT->castleInt->town->bestLocation())
 	{
 		CCS->soundh->playSound(soundBase::newBuilding);
 		LOCPLINT->castleInt->addBuilding(BuildingID::SHIP);
@@ -1625,7 +1546,7 @@ void CPlayerInterface::playerBlocked(int reason, bool start)
 			GH.curInt = this;
 			adventureInt->selection = nullptr;
 			adventureInt->setPlayer(playerID);
-			std::string msg = CGI->generaltexth->localizedTexts["adventureMap"]["playerAttacked"].String();
+			std::string msg = CGI->generaltexth->translate("vcmi.adventureMap.playerAttacked");
 			boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name);
 			std::vector<std::shared_ptr<CComponent>> cmp;
 			cmp.push_back(std::make_shared<CComponent>(CComponent::flag, playerID.getNum(), 0));
@@ -2008,7 +1929,7 @@ CGPath * CPlayerInterface::getAndVerifyPath(const CGHeroInstance * h)
 		}
 		else
 		{
-			assert(h->getPosition(false) == path.startPos());
+			assert(h->visitablePos() == path.startPos());
 			//update the hero path in case of something has changed on map
 			if (LOCPLINT->cb->getPathsInfo(h)->getPath(path, path.endPos()))
 				return &path;
@@ -2112,7 +2033,7 @@ void CPlayerInterface::acceptTurn()
 void CPlayerInterface::tryDiggging(const CGHeroInstance * h)
 {
 	int msgToShow = -1;
-	const bool isBlocked = CGI->mh->hasObjectHole(h->getPosition(false)); // Don't dig in the pit.
+	const bool isBlocked = CGI->mh->hasObjectHole(h->visitablePos()); // Don't dig in the pit.
 
 	const auto diggingStatus = isBlocked
 		? EDiggingStatus::TILE_OCCUPIED
@@ -2409,7 +2330,7 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
 	int i = 1;
 	auto getObj = [&](int3 coord, bool ignoreHero)
 	{
-		return cb->getTile(CGHeroInstance::convertPosition(coord,false))->topVisitableObj(ignoreHero);
+		return cb->getTile(h->convertToVisitablePos(coord))->topVisitableObj(ignoreHero);
 	};
 
 	auto isTeleportAction = [&](CGPathNode::ENodeAction action) -> bool
@@ -2448,7 +2369,9 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
 	};
 
 	{
-		path.convert(0);
+		for (auto & elem : path.nodes)
+			elem.coord = h->convertFromVisitablePos(elem.coord);
+
 		TerrainId currentTerrain = Terrain::BORDER; // not init yet
 		TerrainId newTerrain;
 		int sh = -1;
@@ -2505,7 +2428,7 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
 				sh = CCS->soundh->playSound(soundBase::horseFlying, -1);
 #endif
 			{
-				newTerrain = cb->getTile(CGHeroInstance::convertPosition(currentCoord, false))->terType->id;
+				newTerrain = cb->getTile(h->convertToVisitablePos(currentCoord))->terType->id;
 				if(newTerrain != currentTerrain)
 				{
 					CCS->soundh->stopSound(sh);

+ 6 - 7
client/CPlayerInterface.h

@@ -40,9 +40,8 @@ class CButton;
 class CToggleGroup;
 class CAdvMapInt;
 class CCastleInterface;
-class CBattleInterface;
+class BattleInterface;
 class CComponent;
-class CCreatureAnimation;
 class CSelectableComponent;
 class CSlider;
 class CInGameConsole;
@@ -85,7 +84,7 @@ public:
 	static const int SAVES_COUNT = 5;
 
 	CCastleInterface * castleInt; //nullptr if castle window isn't opened
-	static CBattleInterface * battleInt; //nullptr if no battle
+	static std::shared_ptr<BattleInterface> battleInt; //nullptr if no battle
 	CInGameConsole * cingconsole;
 
 	std::shared_ptr<CCallback> cb; //to communicate with engine
@@ -194,14 +193,14 @@ public:
 	void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; used for HP regen handling
 	void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
 	void battleLogMessage(const std::vector<MetaString> & lines) override;
-	void battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance) override;
+	void battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override;
 	void battleSpellCast(const BattleSpellCast *sc) override;
 	void battleStacksEffectsSet(const SetStackEffect & sse) override; //called when a specific effect is set to stacks
 	void battleTriggerEffect(const BattleTriggerEffect & bte) override; //various one-shot effect
-	void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa) override;
+	void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override;
 	void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right
 	void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right
-	void battleUnitsChanged(const std::vector<UnitChanges> & units, const std::vector<CustomEffectInfo> & customEffects) override;
+	void battleUnitsChanged(const std::vector<UnitChanges> & units) override;
 	void battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles) override;
 	void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack
 	void battleGateStateChanged(const EGateState state) override;
@@ -221,7 +220,7 @@ public:
 	void openTownWindow(const CGTownInstance * town); //shows townscreen
 	void openHeroWindow(const CGHeroInstance * hero); //shows hero window with given hero
 	void updateInfo(const CGObjectInstance * specific);
-	void init(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
+	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
 	int3 repairScreenPos(int3 pos); //returns position closest to pos we can center screen on
 	void activateForSpectator(); // TODO: spectator probably need own player interface class
 

+ 9 - 6
client/CServerHandler.cpp

@@ -25,12 +25,15 @@
 #include "../lib/CAndroidVMHelper.h"
 #elif defined(VCMI_IOS)
 #include "ios/utils.h"
-#include "../server/CVCMIServer.h"
-
 #include <dispatch/dispatch.h>
 #else
 #include "../lib/Interprocess.h"
 #endif
+
+#ifdef SINGLE_PROCESS_APP
+#include "../server/CVCMIServer.h"
+#endif
+
 #include "../lib/CConfigHandler.h"
 #include "../lib/CGeneralTextHandler.h"
 #include "../lib/CThreadHelper.h"
@@ -129,7 +132,7 @@ void CServerHandler::resetStateForLobby(const StartInfo::EMode mode, const std::
 {
 	hostClientId = -1;
 	state = EClientState::NONE;
-	th = make_unique<CStopWatch>();
+	th = std::make_unique<CStopWatch>();
 	packsForLobbyScreen.clear();
 	c.reset();
 	si = std::make_shared<StartInfo>();
@@ -142,7 +145,7 @@ void CServerHandler::resetStateForLobby(const StartInfo::EMode mode, const std::
 	else
 		myNames.push_back(settings["general"]["playerName"].String());
 
-#if !defined(VCMI_ANDROID) && !defined(VCMI_IOS)
+#if !defined(VCMI_ANDROID) && !defined(SINGLE_PROCESS_APP)
 	shm.reset();
 
 	if(!settings["session"]["disable-shm"].Bool())
@@ -173,7 +176,7 @@ void CServerHandler::startLocalServerAndConnect()
 
 	th->update();
 	
-	auto errorMsg = CGI->generaltexth->localizedTexts["server"]["errors"]["existingProcess"].String();
+	auto errorMsg = CGI->generaltexth->translate("vcmi.server.errors.existingProcess");
 	try
 	{
 		CConnection testConnection(localhostAddress, getDefaultPort(), NAME, uuid);
@@ -715,7 +718,7 @@ void CServerHandler::restoreLastSession()
 		saveSession->Bool() = false;
 	};
 	
-	CInfoWindow::showYesNoDialog(VLC->generaltexth->localizedTexts["server"]["confirmReconnect"].String(), {}, loadSession, cleanUpSession);
+	CInfoWindow::showYesNoDialog(VLC->generaltexth->translate("vcmi.server.confirmReconnect"), {}, loadSession, cleanUpSession);
 }
 
 void CServerHandler::debugStartTest(std::string filename, bool save)

+ 0 - 2
client/CServerHandler.h

@@ -30,8 +30,6 @@ template<typename T> class CApplier;
 
 VCMI_LIB_NAMESPACE_END
 
-struct SharedMemory;
-
 class CClient;
 
 class CBaseForLobbyApply;

+ 7 - 16
client/Client.cpp

@@ -42,7 +42,7 @@
 #include "mainmenu/CMainMenu.h"
 #include "mainmenu/CCampaignScreen.h"
 #include "lobby/CBonusSelection.h"
-#include "battle/CBattleInterface.h"
+#include "battle/BattleInterface.h"
 #include "../lib/CThreadHelper.h"
 #include "../lib/registerTypes/RegisterTypes.h"
 #include "gui/CGuiHandler.h"
@@ -380,20 +380,12 @@ void CClient::endGame()
 	{
 		boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
 		logNetwork->info("Ending current game!");
-		if(GH.topInt())
-		{
-			GH.topInt()->deactivate();
-		}
-		GH.listInt.clear();
-		GH.objsToBlit.clear();
-		GH.statusbar = nullptr;
-		logNetwork->info("Removed GUI.");
+		removeGUI();
 
 		vstd::clear_pointer(const_cast<CGameInfo *>(CGI)->mh);
 		vstd::clear_pointer(gs);
 
 		logNetwork->info("Deleted mapHandler and gameState.");
-		LOCPLINT = nullptr;
 	}
 
 	playerint.clear();
@@ -509,7 +501,7 @@ void CClient::installNewPlayerInterface(std::shared_ptr<CGameInterface> gameInte
 	logGlobal->trace("\tInitializing the interface for player %s", color.getStr());
 	auto cb = std::make_shared<CCallback>(gs, color, this);
 	battleCallbacks[color] = cb;
-	gameInterface->init(playerEnvironments.at(color), cb);
+	gameInterface->initGameInterface(playerEnvironments.at(color), cb);
 
 	installNewBattleInterface(gameInterface, color, battlecb);
 }
@@ -525,7 +517,7 @@ void CClient::installNewBattleInterface(std::shared_ptr<CBattleGameInterface> ba
 		logGlobal->trace("\tInitializing the battle interface for player %s", color.getStr());
 		auto cbc = std::make_shared<CBattleCallback>(color, this);
 		battleCallbacks[color] = cbc;
-		battleInterface->init(playerEnvironments.at(color), cbc);
+		battleInterface->initBattleInterface(playerEnvironments.at(color), cbc);
 	}
 }
 
@@ -594,11 +586,10 @@ void CClient::battleStarted(const BattleInfo * info)
 
 	if(!settings["session"]["headless"].Bool())
 	{
-		Rect battleIntRect((screen->w - 800)/2, (screen->h - 600)/2, 800, 600);
 		if(!!att || !!def)
 		{
 			boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
-			GH.pushIntT<CBattleInterface>(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, battleIntRect, att, def);
+			CPlayerInterface::battleInt = std::make_shared<BattleInterface>(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def);
 		}
 		else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
 		{
@@ -606,7 +597,7 @@ void CClient::battleStarted(const BattleInfo * info)
 			auto spectratorInt = std::dynamic_pointer_cast<CPlayerInterface>(playerint[PlayerColor::SPECTATOR]);
 			spectratorInt->cb->setBattle(info);
 			boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
-			GH.pushIntT<CBattleInterface>(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, battleIntRect, att, def, spectratorInt);
+			CPlayerInterface::battleInt = std::make_shared<BattleInterface>(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt);
 		}
 	}
 
@@ -764,7 +755,7 @@ scripting::Pool * CClient::getContextPool() const
 
 void CClient::reinitScripting()
 {
-	clientEventBus = make_unique<events::EventBus>();
+	clientEventBus = std::make_unique<events::EventBus>();
 #if SCRIPTING_ENABLED
 	clientScripts.reset(new scripting::PoolImpl(this));
 #endif

+ 3 - 3
client/CreatureCostBox.cpp

@@ -18,9 +18,9 @@ CreatureCostBox::CreatureCostBox(Rect position, std::string titleText)
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 
 	type |= REDRAW_PARENT;
-	pos = position + pos;
+	pos = position + pos.topLeft();
 
-	title = std::make_shared<CLabel>(pos.w/2, 10, FONT_SMALL, CENTER, Colors::WHITE, titleText);
+	title = std::make_shared<CLabel>(pos.w/2, 10, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, titleText);
 }
 
 void CreatureCostBox::set(TResources res)
@@ -39,7 +39,7 @@ void CreatureCostBox::createItems(TResources res)
 	while(iter.valid())
 	{
 		ImagePtr image = std::make_shared<CAnimImage>("RESOURCE", iter->resType);
-		LabelPtr text = std::make_shared<CLabel>(15, 43, FONT_SMALL, CENTER, Colors::WHITE, "0");
+		LabelPtr text = std::make_shared<CLabel>(15, 43, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, "0");
 
 		resources.insert(std::make_pair(iter->resType, std::make_pair(text, image)));
 		iter++;

+ 0 - 1
client/Graphics.cpp

@@ -29,7 +29,6 @@
 #include "../lib/VCMI_Lib.h"
 #include "../CCallback.h"
 #include "../lib/CGeneralTextHandler.h"
-#include "CBitmapHandler.h"
 #include "../lib/CGameState.h"
 #include "../lib/JsonNode.h"
 #include "../lib/vcmi_endian.h"

+ 1 - 1
client/Graphics.h

@@ -30,7 +30,7 @@ struct SDL_Surface;
 struct SDL_Color;
 class CAnimation;
 
-enum EFonts
+enum EFonts : int
 {
 	FONT_BIG, FONT_CALLI, FONT_CREDITS, FONT_HIGH_SCORE, FONT_MEDIUM, FONT_SMALL, FONT_TIMES, FONT_TINY, FONT_VERD
 };

+ 15 - 12
client/NetPacksClient.cpp

@@ -30,7 +30,7 @@
 #include "windows/GUIClasses.h"
 #include "../lib/CConfigHandler.h"
 #include "gui/SDL_Extensions.h"
-#include "battle/CBattleInterface.h"
+#include "battle/BattleInterface.h"
 #include "../lib/mapping/CCampaignHandler.h"
 #include "../lib/CGameState.h"
 #include "../lib/CStack.h"
@@ -272,15 +272,18 @@ void EraseArtifact::applyCl(CClient *cl)
 	callInterfaceIfPresent(cl, al.owningPlayer(), &IGameEventsReceiver::artifactRemoved, al);
 }
 
-void MoveArtifact::applyCl(CClient *cl)
+void MoveArtifact::applyCl(CClient * cl)
 {
-	callInterfaceIfPresent(cl, src.owningPlayer(), &IGameEventsReceiver::artifactMoved, src, dst);
-	callInterfaceIfPresent(cl, src.owningPlayer(), &IGameEventsReceiver::artifactPossibleAssembling, dst);
-	if(src.owningPlayer() != dst.owningPlayer())
+	auto moveArtifact = [this, cl](PlayerColor player) -> void
 	{
-		callInterfaceIfPresent(cl, dst.owningPlayer(), &IGameEventsReceiver::artifactMoved, src, dst);
-		callInterfaceIfPresent(cl, dst.owningPlayer(), &IGameEventsReceiver::artifactPossibleAssembling, dst);
-	}
+		callInterfaceIfPresent(cl, player, &IGameEventsReceiver::artifactMoved, src, dst);
+		if(askAssemble)
+			callInterfaceIfPresent(cl, player, &IGameEventsReceiver::artifactPossibleAssembling, dst);
+	};
+
+	moveArtifact(src.owningPlayer());
+	if(src.owningPlayer() != dst.owningPlayer())
+		moveArtifact(dst.owningPlayer());
 }
 
 void BulkMoveArtifacts::applyCl(CClient * cl)
@@ -738,7 +741,7 @@ void BattleResult::applyFirstCl(CClient *cl)
 void BattleStackMoved::applyFirstCl(CClient *cl)
 {
 	const CStack * movedStack = GS(cl)->curB->battleGetStackByID(stack);
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStackMoved, movedStack, tilesToMove, distance);
+	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStackMoved, movedStack, tilesToMove, distance, teleporting);
 }
 
 void BattleAttack::applyFirstCl(CClient *cl)
@@ -748,7 +751,7 @@ void BattleAttack::applyFirstCl(CClient *cl)
 
 void BattleAttack::applyCl(CClient *cl)
 {
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, bsa);
+	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, bsa, shot());
 }
 
 void StartAction::applyFirstCl(CClient *cl)
@@ -770,7 +773,7 @@ void SetStackEffect::applyCl(CClient *cl)
 
 void StacksInjured::applyCl(CClient *cl)
 {
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, stacks);
+	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, stacks, false);
 }
 
 void BattleResultsApplied::applyCl(CClient *cl)
@@ -782,7 +785,7 @@ void BattleResultsApplied::applyCl(CClient *cl)
 
 void BattleUnitsChanged::applyCl(CClient * cl)
 {
-	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleUnitsChanged, changedStacks, customEffects);
+	callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleUnitsChanged, changedStacks);
 }
 
 void BattleObstaclesChanged::applyCl(CClient *cl)

+ 791 - 0
client/battle/BattleActionsController.cpp

@@ -0,0 +1,791 @@
+/*
+ * BattleActionsController.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 "BattleActionsController.h"
+
+#include "BattleWindow.h"
+#include "BattleStacksController.h"
+#include "BattleInterface.h"
+#include "BattleFieldController.h"
+#include "BattleSiegeController.h"
+#include "BattleInterfaceClasses.h"
+
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+#include "../gui/CCursorHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/CIntObject.h"
+#include "../windows/CCreatureWindow.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();
+}
+
+
+BattleActionsController::BattleActionsController(BattleInterface & owner):
+	owner(owner),
+	creatureCasting(false),
+	spellDestSelectMode(false),
+	spellToCast(nullptr),
+	currentSpell(nullptr)
+{
+	currentAction = PossiblePlayerBattleAction::INVALID;
+	selectedAction = PossiblePlayerBattleAction::INVALID;
+}
+
+void BattleActionsController::endCastingSpell()
+{
+	if(spellDestSelectMode)
+	{
+		spellToCast.reset();
+
+		currentSpell = nullptr;
+		spellDestSelectMode = false;
+		CCS->curh->set(Cursor::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 BattleActionsController::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->set(Cursor::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> BattleActionsController::getPossibleActionsForStack(const CStack *stack) const
+{
+	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 BattleActionsController::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::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 BattleActionsController::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
+	currentSpell = spellID.toSpell();
+	PossiblePlayerBattleAction spellSelMode = owner.curInt->cb->getCasterAction(currentSpell, 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 BattleActionsController::handleHex(BattleHex myNumber, int eventType)
+{
+	if (!owner.myTurn) //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.
+
+	std::string newConsoleMsg;
+	//used when hovering -> tooltip message and cursor to be set
+	bool setCursor = true; //if we want to suppress setting cursor
+	bool spellcastingCursor = false;
+	auto cursorFrame = Cursor::Combat::POINTER;
+
+	//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;
+
+	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?
+					{
+						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->isAttackableByCatapult(myNumber))
+					legalAction = true;
+				break;
+			case PossiblePlayerBattleAction::HEAL:
+				if (shere && ourStack && shere->canBeHealed())
+					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:
+				newConsoleMsg = (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 = Cursor::Combat::FLY;
+					newConsoleMsg = (boost::format(CGI->generaltexth->allTexts[295]) % owner.stacksController->getActiveStack()->getName()).str(); //Fly %s here
+				}
+				else
+				{
+					cursorFrame = Cursor::Combat::MOVE;
+					newConsoleMsg = (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
+					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
+					newConsoleMsg = (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 = Cursor::Combat::SHOOT_PENALTY;
+				else
+					cursorFrame = Cursor::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)
+				newConsoleMsg = (boost::format(CGI->generaltexth->allTexts[296]) % shere->getName() % owner.stacksController->getActiveStack()->shots.available() % estDmgText).str();
+			}
+				break;
+			case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+				currentSpell = CGI->spellh->objects[creatureCasting ? owner.stacksController->activeStackSpellToCast() : spellToCast->actionSubtype]; //necessary if creature has random Genie spell at same time
+				newConsoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[27]) % currentSpell->name % shere->getName()); //Cast %s on %s
+				switch (currentSpell->id)
+				{
+					case SpellID::SACRIFICE:
+					case SpellID::TELEPORT:
+						owner.stacksController->setSelectedStack(shere); //remember first target
+						secondaryTarget = true;
+						break;
+				}
+				isCastingPossible = true;
+				break;
+			case PossiblePlayerBattleAction::ANY_LOCATION:
+				currentSpell = CGI->spellh->objects[creatureCasting ? owner.stacksController->activeStackSpellToCast() : spellToCast->actionSubtype]; //necessary if creature has random Genie spell at same time
+				newConsoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % currentSpell->name); //Cast %s
+				isCastingPossible = true;
+				break;
+			case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell
+				currentSpell = nullptr;
+				newConsoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[301]) % shere->getName()); //Cast a spell on %
+				creatureCasting = true;
+				isCastingPossible = true;
+				break;
+			case PossiblePlayerBattleAction::TELEPORT:
+				newConsoleMsg = CGI->generaltexth->allTexts[25]; //Teleport Here
+				cursorFrame = Cursor::Combat::TELEPORT;
+				isCastingPossible = true;
+				break;
+			case PossiblePlayerBattleAction::OBSTACLE:
+				newConsoleMsg = CGI->generaltexth->allTexts[550];
+				//TODO: remove obstacle cursor
+				isCastingPossible = true;
+				break;
+			case PossiblePlayerBattleAction::SACRIFICE:
+				newConsoleMsg = (boost::format(CGI->generaltexth->allTexts[549]) % shere->getName()).str(); //sacrifice the %s
+				cursorFrame = Cursor::Combat::SACRIFICE;
+				isCastingPossible = true;
+				break;
+			case PossiblePlayerBattleAction::FREE_LOCATION:
+				newConsoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % currentSpell->name); //Cast %s
+				isCastingPossible = true;
+				break;
+			case PossiblePlayerBattleAction::HEAL:
+				cursorFrame = Cursor::Combat::HEAL;
+				newConsoleMsg = (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::CATAPULT:
+				cursorFrame = Cursor::Combat::SHOOT_CATAPULT;
+				realizeAction = [=](){ owner.giveCommand(EActionType::CATAPULT, myNumber); };
+				break;
+			case PossiblePlayerBattleAction::CREATURE_INFO:
+			{
+				cursorFrame = Cursor::Combat::QUERY;
+				newConsoleMsg = (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 = Cursor::Combat::BLOCKED;
+				newConsoleMsg = CGI->generaltexth->allTexts[23];
+				break;
+			case PossiblePlayerBattleAction::TELEPORT:
+				cursorFrame = Cursor::Combat::BLOCKED;
+				newConsoleMsg = CGI->generaltexth->allTexts[24]; //Invalid Teleport Destination
+				break;
+			case PossiblePlayerBattleAction::SACRIFICE:
+				newConsoleMsg = CGI->generaltexth->allTexts[543]; //choose army to sacrifice
+				break;
+			case PossiblePlayerBattleAction::FREE_LOCATION:
+				cursorFrame = Cursor::Combat::BLOCKED;
+				newConsoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[181]) % currentSpell->name); //No room to place %s here
+				break;
+			default:
+				if (myNumber == -1)
+					CCS->curh->set(Cursor::Combat::POINTER);
+				else
+					cursorFrame = Cursor::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:
+				spellcastingCursor = true;
+				if (newConsoleMsg.empty() && currentSpell)
+					newConsoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % currentSpell->name); //Cast %s
+				break;
+		}
+
+		realizeAction = [=]()
+		{
+			if(secondaryTarget) //select that target now
+			{
+
+				possibleActions.clear();
+				switch (currentSpell->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 (currentSpell)
+					{
+						owner.giveCommand(EActionType::MONSTER_SPELL, myNumber, owner.stacksController->activeStackSpellToCast());
+					}
+					else //unknown random spell
+					{
+						owner.giveCommand(EActionType::MONSTER_SPELL, myNumber);
+					}
+				}
+				else
+				{
+					assert(currentSpell);
+					switch (currentSpell->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)
+			{
+				if (spellcastingCursor)
+					CCS->curh->set(Cursor::Spellcast::SPELL);
+				else
+					CCS->curh->set(cursorFrame);
+			}
+
+			if (!currentConsoleMsg.empty())
+				GH.statusbar->clearIfMatching(currentConsoleMsg);
+			if (!newConsoleMsg.empty())
+				GH.statusbar->write(newConsoleMsg);
+
+			currentConsoleMsg = newConsoleMsg;
+		}
+		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->set(Cursor::Combat::POINTER);
+			GH.statusbar->clear();
+		}
+	}
+}
+
+
+bool BattleActionsController::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;
+	}
+
+
+	currentSpell = nullptr;
+	if (spellID >= 0)
+		currentSpell = CGI->spellh->objects[spellID];
+
+	if (currentSpell)
+	{
+		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, currentSpell);
+
+			auto m = currentSpell->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 BattleActionsController::canStackMoveHere(const CStack * stackToMove, BattleHex myNumber) const
+{
+	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 BattleActionsController::activateStack()
+{
+	const CStack * s = owner.stacksController->getActiveStack();
+	if(s)
+	{
+		possibleActions = getPossibleActionsForStack(s);
+		std::list<PossiblePlayerBattleAction> actionsToSelect;
+		if(!possibleActions.empty())
+		{
+			switch(possibleActions.front())
+			{
+				case PossiblePlayerBattleAction::SHOOT:
+					actionsToSelect.push_back(possibleActions.front());
+					actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK);
+					break;
+					
+				case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
+					actionsToSelect.push_back(possibleActions.front());
+					actionsToSelect.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK);
+					break;
+					
+				case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+					actionsToSelect.push_back(possibleActions.front());
+					break;
+			}
+		}
+		owner.windowObject->setAlternativeActions(actionsToSelect);
+	}
+}
+
+bool BattleActionsController::spellcastingModeActive() const
+{
+	return spellDestSelectMode;
+}
+
+SpellID BattleActionsController::selectedSpell() const
+{
+	if (!spellToCast)
+		return SpellID::NONE;
+	return SpellID(spellToCast->actionSubtype);
+}
+
+const std::vector<PossiblePlayerBattleAction> & BattleActionsController::getPossibleActions() const
+{
+	return possibleActions;
+}
+
+void BattleActionsController::removePossibleAction(PossiblePlayerBattleAction action)
+{
+	vstd::erase(possibleActions, action);
+}
+
+void BattleActionsController::pushFrontPossibleAction(PossiblePlayerBattleAction action)
+{
+	possibleActions.insert(possibleActions.begin(), action);
+}

+ 103 - 0
client/battle/BattleActionsController.h

@@ -0,0 +1,103 @@
+/*
+ * BattleActionsController.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"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class BattleAction;
+
+VCMI_LIB_NAMESPACE_END
+
+class BattleInterface;
+
+enum class MouseHoveredHexContext
+{
+	UNOCCUPIED_HEX,
+	OCCUPIED_HEX
+};
+
+/// Class that controls actions that can be performed by player, e.g. moving stacks, attacking, etc
+/// As well as all relevant feedback for these actions in user interface
+class BattleActionsController
+{
+	BattleInterface & owner;
+
+	/// all actions possible to call at the moment by player
+	std::vector<PossiblePlayerBattleAction> possibleActions;
+
+	/// actions possible to take on hovered hex
+	std::vector<PossiblePlayerBattleAction> localActions;
+
+	/// these actions display message in case of illegal target
+	std::vector<PossiblePlayerBattleAction> illegalActions;
+
+	/// action that will be performed on l-click
+	PossiblePlayerBattleAction currentAction;
+
+	/// last action chosen (and saved) by player
+	PossiblePlayerBattleAction selectedAction;
+
+	/// if there are not possible actions to choose from, this action should be show as "illegal" in UI
+	PossiblePlayerBattleAction illegalAction;
+
+	/// if true, stack currently aims to cats a spell
+	bool creatureCasting;
+
+	/// if true, player is choosing destination for his spell - only for GUI / console
+	bool spellDestSelectMode;
+
+	/// spell for which player is choosing destination
+	std::shared_ptr<BattleAction> spellToCast;
+
+	/// spell for which player is choosing destination, pointer for convenience
+	const CSpell *currentSpell;
+
+	/// cached message that was set by this class in status bar
+	std::string currentConsoleMsg;
+
+	bool isCastingPossibleHere (const CStack *sactive, const CStack *shere, BattleHex myNumber);
+	bool canStackMoveHere (const CStack *sactive, BattleHex MyNumber) const; //TODO: move to BattleState / callback
+	std::vector<PossiblePlayerBattleAction> getPossibleActionsForStack (const CStack *stack) const; //called when stack gets its turn
+	void reorderPossibleActionsPriority(const CStack * stack, MouseHoveredHexContext context);
+
+public:
+	BattleActionsController(BattleInterface & owner);
+
+	/// initialize list of potential actions for new active stack
+	void activateStack();
+
+	/// initialize potential actions for spells that can be cast by active stack
+	void enterCreatureCastingMode();
+
+	/// initialize potential actions for selected spell
+	void castThisSpell(SpellID spellID);
+
+	/// ends casting spell (eg. when spell has been cast or canceled)
+	void endCastingSpell();
+
+	/// update UI (e.g. status bar/cursor) according to new active hex
+	void handleHex(BattleHex myNumber, int eventType);
+
+	/// returns currently selected spell or SpellID::NONE on error
+	SpellID selectedSpell() const;
+
+	/// returns true if UI is currently in target selection mode
+	bool spellcastingModeActive() const;
+	
+	/// methods to work with array of possible actions, needed to control special creatures abilities
+	const std::vector<PossiblePlayerBattleAction> & getPossibleActions() const;
+	void removePossibleAction(PossiblePlayerBattleAction);
+	
+	/// inserts possible action in the beggining in order to prioritize it
+	void pushFrontPossibleAction(PossiblePlayerBattleAction);
+
+};

+ 1121 - 0
client/battle/BattleAnimationClasses.cpp

@@ -0,0 +1,1121 @@
+/*
+ * BattleAnimationClasses.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 "BattleAnimationClasses.h"
+
+#include "BattleInterface.h"
+#include "BattleInterfaceClasses.h"
+#include "BattleProjectileController.h"
+#include "BattleSiegeController.h"
+#include "BattleFieldController.h"
+#include "BattleEffectsController.h"
+#include "BattleStacksController.h"
+#include "CreatureAnimation.h"
+
+#include "../CGameInfo.h"
+#include "../CMusicHandler.h"
+#include "../CPlayerInterface.h"
+#include "../gui/CCursorHandler.h"
+#include "../gui/CGuiHandler.h"
+
+#include "../../CCallback.h"
+#include "../../lib/CStack.h"
+
+BattleAnimation::BattleAnimation(BattleInterface & owner)
+	: owner(owner),
+	  ID(owner.stacksController->animIDhelper++),
+	  initialized(false)
+{
+	logAnim->trace("Animation #%d created", ID);
+}
+
+bool BattleAnimation::tryInitialize()
+{
+	assert(!initialized);
+
+	if ( init() )
+	{
+		initialized = true;
+		return true;
+	}
+	return false;
+}
+
+bool BattleAnimation::isInitialized()
+{
+	return initialized;
+}
+
+BattleAnimation::~BattleAnimation()
+{
+	logAnim->trace("Animation #%d ended, type is %s", ID, typeid(this).name());
+	for(auto & elem : pendingAnimations())
+	{
+		if(elem == this)
+			elem = nullptr;
+	}
+	logAnim->trace("Animation #%d deleted", ID);
+}
+
+std::vector<BattleAnimation *> & BattleAnimation::pendingAnimations()
+{
+	return owner.stacksController->currentAnimations;
+}
+
+std::shared_ptr<CreatureAnimation> BattleAnimation::stackAnimation(const CStack * stack) const
+{
+	return owner.stacksController->stackAnimation[stack->ID];
+}
+
+bool BattleAnimation::stackFacingRight(const CStack * stack)
+{
+	return owner.stacksController->stackFacingRight[stack->ID];
+}
+
+void BattleAnimation::setStackFacingRight(const CStack * stack, bool facingRight)
+{
+	owner.stacksController->stackFacingRight[stack->ID] = facingRight;
+}
+
+BattleStackAnimation::BattleStackAnimation(BattleInterface & owner, const CStack * stack)
+	: BattleAnimation(owner),
+	  myAnim(stackAnimation(stack)),
+	  stack(stack)
+{
+	assert(myAnim);
+}
+
+StackActionAnimation::StackActionAnimation(BattleInterface & owner, const CStack * stack)
+	: BattleStackAnimation(owner, stack)
+	, nextGroup(ECreatureAnimType::HOLDING)
+	, currGroup(ECreatureAnimType::HOLDING)
+{
+}
+
+ECreatureAnimType StackActionAnimation::getGroup() const
+{
+	return currGroup;
+}
+
+void StackActionAnimation::setNextGroup( ECreatureAnimType group )
+{
+	nextGroup = group;
+}
+
+void StackActionAnimation::setGroup( ECreatureAnimType group )
+{
+	currGroup = group;
+}
+
+void StackActionAnimation::setSound( std::string sound )
+{
+	this->sound = sound;
+}
+
+bool StackActionAnimation::init()
+{
+	if (!sound.empty())
+		CCS->soundh->playSound(sound);
+
+	if (myAnim->framesInGroup(currGroup) > 0)
+	{
+		myAnim->playOnce(currGroup);
+		myAnim->onAnimationReset += [&](){ delete this; };
+	}
+	else
+		delete this;
+
+	return true;
+}
+
+StackActionAnimation::~StackActionAnimation()
+{
+	if (stack->isFrozen())
+		myAnim->setType(ECreatureAnimType::HOLDING);
+	else
+		myAnim->setType(nextGroup);
+
+}
+
+ECreatureAnimType AttackAnimation::findValidGroup( const std::vector<ECreatureAnimType> candidates ) const
+{
+	for ( auto group : candidates)
+	{
+		if(myAnim->framesInGroup(group) > 0)
+			return group;
+	}
+
+	assert(0);
+	return ECreatureAnimType::HOLDING;
+}
+
+const CCreature * AttackAnimation::getCreature() const
+{
+	if (attackingStack->getCreature()->idNumber == CreatureID::ARROW_TOWERS)
+		return owner.siegeController->getTurretCreature();
+	else
+		return attackingStack->getCreature();
+}
+
+
+AttackAnimation::AttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender)
+	: StackActionAnimation(owner, attacker),
+	  dest(_dest),
+	  defendingStack(defender),
+	  attackingStack(attacker)
+{
+	assert(attackingStack && "attackingStack is nullptr in CBattleAttack::CBattleAttack !\n");
+	attackingStackPosBeforeReturn = attackingStack->getPosition();
+}
+
+HittedAnimation::HittedAnimation(BattleInterface & owner, const CStack * stack)
+	: StackActionAnimation(owner, stack)
+{
+	setGroup(ECreatureAnimType::HITTED);
+	setSound(battle_sound(stack->getCreature(), wince));
+	logAnim->debug("Created HittedAnimation for %s", stack->getName());
+}
+
+DefenceAnimation::DefenceAnimation(BattleInterface & owner, const CStack * stack)
+	: StackActionAnimation(owner, stack)
+{
+	setGroup(ECreatureAnimType::DEFENCE);
+	setSound(battle_sound(stack->getCreature(), defend));
+	logAnim->debug("Created DefenceAnimation for %s", stack->getName());
+}
+
+DeathAnimation::DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged):
+	StackActionAnimation(owner, stack)
+{
+	setSound(battle_sound(stack->getCreature(), killed));
+
+	if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEATH_RANGED) > 0)
+		setGroup(ECreatureAnimType::DEATH_RANGED);
+	else
+		setGroup(ECreatureAnimType::DEATH);
+
+	if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEAD_RANGED) > 0)
+		setNextGroup(ECreatureAnimType::DEAD_RANGED);
+	else
+		setNextGroup(ECreatureAnimType::DEAD);
+
+	logAnim->debug("Created DeathAnimation for %s", stack->getName());
+}
+
+DummyAnimation::DummyAnimation(BattleInterface & owner, int howManyFrames)
+	: BattleAnimation(owner),
+	  counter(0),
+	  howMany(howManyFrames)
+{
+	logAnim->debug("Created dummy animation for %d frames", howManyFrames);
+}
+
+bool DummyAnimation::init()
+{
+	return true;
+}
+
+void DummyAnimation::nextFrame()
+{
+	counter++;
+	if(counter > howMany)
+		delete this;
+}
+
+ECreatureAnimType MeleeAttackAnimation::getUpwardsGroup(bool multiAttack) const
+{
+	if (!multiAttack)
+		return ECreatureAnimType::ATTACK_UP;
+
+	return findValidGroup({
+		ECreatureAnimType::GROUP_ATTACK_UP,
+		ECreatureAnimType::SPECIAL_UP,
+		ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3
+		ECreatureAnimType::ATTACK_UP
+	});
+}
+
+ECreatureAnimType MeleeAttackAnimation::getForwardGroup(bool multiAttack) const
+{
+	if (!multiAttack)
+		return ECreatureAnimType::ATTACK_FRONT;
+
+	return findValidGroup({
+		ECreatureAnimType::GROUP_ATTACK_FRONT,
+		ECreatureAnimType::SPECIAL_FRONT,
+		ECreatureAnimType::ATTACK_FRONT
+	});
+}
+
+ECreatureAnimType MeleeAttackAnimation::getDownwardsGroup(bool multiAttack) const
+{
+	if (!multiAttack)
+		return ECreatureAnimType::ATTACK_DOWN;
+
+	return findValidGroup({
+		ECreatureAnimType::GROUP_ATTACK_DOWN,
+		ECreatureAnimType::SPECIAL_DOWN,
+		ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3
+		ECreatureAnimType::ATTACK_DOWN
+	});
+}
+
+ECreatureAnimType MeleeAttackAnimation::selectGroup(bool multiAttack)
+{
+	const ECreatureAnimType mutPosToGroup[] =
+	{
+		getUpwardsGroup  (multiAttack),
+		getUpwardsGroup  (multiAttack),
+		getForwardGroup  (multiAttack),
+		getDownwardsGroup(multiAttack),
+		getDownwardsGroup(multiAttack),
+		getForwardGroup  (multiAttack)
+	};
+
+	int revShiftattacker = (attackingStack->side == BattleSide::ATTACKER ? -1 : 1);
+
+	int mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, dest);
+	if(mutPos == -1 && attackingStack->doubleWide())
+	{
+		mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, defendingStack->getPosition());
+	}
+	if (mutPos == -1 && defendingStack->doubleWide())
+	{
+		mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, defendingStack->occupiedHex());
+	}
+	if (mutPos == -1 && defendingStack->doubleWide() && attackingStack->doubleWide())
+	{
+		mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, defendingStack->occupiedHex());
+	}
+
+	assert(mutPos >= 0 && mutPos <=5);
+
+	return mutPosToGroup[mutPos];
+}
+
+void MeleeAttackAnimation::nextFrame()
+{
+	size_t currentFrame = stackAnimation(attackingStack)->getCurrentFrame();
+	size_t totalFrames = stackAnimation(attackingStack)->framesInGroup(getGroup());
+
+	if ( currentFrame * 2 >= totalFrames )
+	{
+		if(owner.getAnimationCondition(EAnimationEvents::HIT) == false)
+			owner.setAnimationCondition(EAnimationEvents::HIT, true);
+	}
+	AttackAnimation::nextFrame();
+}
+
+MeleeAttackAnimation::MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack)
+	: AttackAnimation(owner, attacker, _dest, _attacked)
+{
+	logAnim->debug("Created MeleeAttackAnimation for %s", attacker->getName());
+	setSound(battle_sound(getCreature(), attack));
+	setGroup(selectGroup(multiAttack));
+}
+
+StackMoveAnimation::StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex):
+	BattleStackAnimation(owner, _stack),
+	prevHex(prevHex),
+	nextHex(nextHex)
+{
+}
+
+bool MovementAnimation::init()
+{
+	assert(stack);
+	assert(!myAnim->isDeadOrDying());
+	assert(stackAnimation(stack)->framesInGroup(ECreatureAnimType::MOVING) > 0);
+
+	if(stackAnimation(stack)->framesInGroup(ECreatureAnimType::MOVING) == 0)
+	{
+		//no movement, end immediately
+		delete this;
+		return false;
+	}
+
+	logAnim->debug("CMovementAnimation::init: stack %s moves %d -> %d", stack->getName(), prevHex, nextHex);
+
+	//reverse unit if necessary
+	if(owner.stacksController->shouldRotate(stack, prevHex, nextHex))
+	{
+		// it seems that H3 does NOT plays full rotation animation during movement
+		// Logical since it takes quite a lot of time
+		rotateStack(prevHex);
+	}
+
+	if(myAnim->getType() != ECreatureAnimType::MOVING)
+	{
+		myAnim->setType(ECreatureAnimType::MOVING);
+	}
+
+	if (owner.moveSoundHander == -1)
+	{
+		owner.moveSoundHander = CCS->soundh->playSound(battle_sound(stack->getCreature(), move), -1);
+	}
+
+	Point begPosition = owner.stacksController->getStackPositionAtHex(prevHex, stack);
+	Point endPosition = owner.stacksController->getStackPositionAtHex(nextHex, stack);
+
+	timeToMove = AnimationControls::getMovementDuration(stack->getCreature());
+
+	begX = begPosition.x;
+	begY = begPosition.y;
+	//progress = 0;
+	distanceX = endPosition.x - begPosition.x;
+	distanceY = endPosition.y - begPosition.y;
+
+	if (stack->hasBonus(Selector::type()(Bonus::FLYING)))
+	{
+		float distance = static_cast<float>(sqrt(distanceX * distanceX + distanceY * distanceY));
+
+		timeToMove *= AnimationControls::getFlightDistance(stack->getCreature()) / distance;
+	}
+
+	return true;
+}
+
+void MovementAnimation::nextFrame()
+{
+	progress += float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000 * timeToMove;
+
+	//moving instructions
+	myAnim->pos.x = static_cast<Sint16>(begX + distanceX * progress );
+	myAnim->pos.y = static_cast<Sint16>(begY + distanceY * progress );
+
+	BattleAnimation::nextFrame();
+
+	if(progress >= 1.0)
+	{
+		progress -= 1.0;
+		// Sets the position of the creature animation sprites
+		Point coords = owner.stacksController->getStackPositionAtHex(nextHex, stack);
+		myAnim->pos.moveTo(coords);
+
+		// true if creature haven't reached the final destination hex
+		if ((curentMoveIndex + 1) < destTiles.size())
+		{
+			// update the next hex field which has to be reached by the stack
+			curentMoveIndex++;
+			prevHex = nextHex;
+			nextHex = destTiles[curentMoveIndex];
+
+			// request re-initialization
+			initialized = false;
+		}
+		else
+			delete this;
+	}
+}
+
+MovementAnimation::~MovementAnimation()
+{
+	assert(stack);
+
+	if(owner.moveSoundHander != -1)
+	{
+		CCS->soundh->stopSound(owner.moveSoundHander);
+		owner.moveSoundHander = -1;
+	}
+}
+
+MovementAnimation::MovementAnimation(BattleInterface & owner, const CStack *stack, std::vector<BattleHex> _destTiles, int _distance)
+	: StackMoveAnimation(owner, stack, stack->getPosition(), _destTiles.front()),
+	  destTiles(_destTiles),
+	  curentMoveIndex(0),
+	  begX(0), begY(0),
+	  distanceX(0), distanceY(0),
+	  timeToMove(0.0),
+	  progress(0.0)
+{
+	logAnim->debug("Created MovementAnimation for %s", stack->getName());
+}
+
+MovementEndAnimation::MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile)
+: StackMoveAnimation(owner, _stack, destTile, destTile)
+{
+	logAnim->debug("Created MovementEndAnimation for %s", stack->getName());
+}
+
+bool MovementEndAnimation::init()
+{
+	assert(stack);
+	assert(!myAnim->isDeadOrDying());
+
+	if(!stack || myAnim->isDeadOrDying())
+	{
+		delete this;
+		return false;
+	}
+
+	logAnim->debug("CMovementEndAnimation::init: stack %s", stack->getName());
+	myAnim->pos.moveTo(owner.stacksController->getStackPositionAtHex(nextHex, stack));
+
+	CCS->soundh->playSound(battle_sound(stack->getCreature(), endMoving));
+
+	if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_END))
+	{
+		delete this;
+		return false;
+	}
+
+
+	myAnim->setType(ECreatureAnimType::MOVE_END);
+	myAnim->onAnimationReset += [&](){ delete this; };
+
+	return true;
+}
+
+MovementEndAnimation::~MovementEndAnimation()
+{
+	if(myAnim->getType() != ECreatureAnimType::DEAD)
+		myAnim->setType(ECreatureAnimType::HOLDING); //resetting to default
+
+	CCS->curh->show();
+}
+
+MovementStartAnimation::MovementStartAnimation(BattleInterface & owner, const CStack * _stack)
+	: StackMoveAnimation(owner, _stack, _stack->getPosition(), _stack->getPosition())
+{
+	logAnim->debug("Created MovementStartAnimation for %s", stack->getName());
+}
+
+bool MovementStartAnimation::init()
+{
+	assert(stack);
+	assert(!myAnim->isDeadOrDying());
+
+	if(!stack || myAnim->isDeadOrDying())
+	{
+		delete this;
+		return false;
+	}
+
+	logAnim->debug("CMovementStartAnimation::init: stack %s", stack->getName());
+	CCS->soundh->playSound(battle_sound(stack->getCreature(), startMoving));
+
+	if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_START))
+	{
+		delete this;
+		return false;
+	}
+
+	myAnim->setType(ECreatureAnimType::MOVE_START);
+	myAnim->onAnimationReset += [&](){ delete this; };
+	return true;
+}
+
+ReverseAnimation::ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest)
+	: StackMoveAnimation(owner, stack, dest, dest)
+{
+	logAnim->debug("Created ReverseAnimation for %s", stack->getName());
+}
+
+bool ReverseAnimation::init()
+{
+	assert(myAnim);
+	assert(!myAnim->isDeadOrDying());
+
+	if(myAnim == nullptr || myAnim->isDeadOrDying())
+	{
+		delete this;
+		return false; //there is no such creature
+	}
+
+	logAnim->debug("CReverseAnimation::init: stack %s", stack->getName());
+	if(myAnim->framesInGroup(ECreatureAnimType::TURN_L))
+	{
+		myAnim->playOnce(ECreatureAnimType::TURN_L);
+		myAnim->onAnimationReset += std::bind(&ReverseAnimation::setupSecondPart, this);
+	}
+	else
+	{
+		setupSecondPart();
+	}
+	return true;
+}
+
+void BattleStackAnimation::rotateStack(BattleHex hex)
+{
+	setStackFacingRight(stack, !stackFacingRight(stack));
+
+	stackAnimation(stack)->pos.moveTo(owner.stacksController->getStackPositionAtHex(hex, stack));
+}
+
+void ReverseAnimation::setupSecondPart()
+{
+	assert(stack);
+
+	if(!stack)
+	{
+		delete this;
+		return;
+	}
+
+	rotateStack(nextHex);
+
+	if(myAnim->framesInGroup(ECreatureAnimType::TURN_R))
+	{
+		myAnim->playOnce(ECreatureAnimType::TURN_R);
+		myAnim->onAnimationReset += [&](){ delete this; };
+	}
+	else
+		delete this;
+}
+
+ResurrectionAnimation::ResurrectionAnimation(BattleInterface & owner, const CStack * _stack):
+	StackActionAnimation(owner, _stack)
+{
+	setGroup(ECreatureAnimType::RESURRECTION);
+	logAnim->debug("Created ResurrectionAnimation for %s", stack->getName());
+}
+
+bool ColorTransformAnimation::init()
+{
+	return true;
+}
+
+void ColorTransformAnimation::nextFrame()
+{
+	float elapsed  = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+	float fullTime = AnimationControls::getFadeInDuration();
+	float delta    = elapsed / fullTime;
+	totalProgress += delta;
+
+	size_t index = 0;
+
+	while (index < timePoints.size() && timePoints[index] < totalProgress )
+		++index;
+
+	if (index == timePoints.size())
+	{
+		//end of animation. Apply ColorShifter using final values and die
+		const auto & shifter = steps[index - 1];
+		owner.stacksController->setStackColorFilter(shifter, stack, spell, false);
+		delete this;
+		return;
+	}
+
+	assert(index != 0);
+
+	const auto & prevShifter = steps[index - 1];
+	const auto & nextShifter = steps[index];
+
+	float prevPoint = timePoints[index-1];
+	float nextPoint = timePoints[index];
+	float localProgress = totalProgress - prevPoint;
+	float stepDuration = (nextPoint - prevPoint);
+	float factor = localProgress / stepDuration;
+
+	auto shifter = ColorFilter::genInterpolated(prevShifter, nextShifter, factor);
+
+	owner.stacksController->setStackColorFilter(shifter, stack, spell, true);
+}
+
+ColorTransformAnimation::ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell):
+	BattleStackAnimation(owner, _stack),
+	spell(spell),
+	totalProgress(0.f)
+{
+	auto effect = owner.effectsController->getMuxerEffect(colorFilterName);
+	steps = effect.filters;
+	timePoints = effect.timePoints;
+
+	assert(!steps.empty() && steps.size() == timePoints.size());
+
+	logAnim->debug("Created ColorTransformAnimation for %s", stack->getName());
+}
+
+RangedAttackAnimation::RangedAttackAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest_, const CStack * defender)
+	: AttackAnimation(owner_, attacker, dest_, defender),
+	  projectileEmitted(false)
+{
+	setSound(battle_sound(getCreature(), shoot));
+}
+
+bool RangedAttackAnimation::init()
+{
+	setAnimationGroup();
+	initializeProjectile();
+
+	return AttackAnimation::init();
+}
+
+void RangedAttackAnimation::setAnimationGroup()
+{
+	Point shooterPos = stackAnimation(attackingStack)->pos.topLeft();
+	Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack);
+
+	//maximal angle in radians between straight horizontal line and shooting line for which shot is considered to be straight (absoulte value)
+	static const double straightAngle = 0.2;
+
+	double projectileAngle = -atan2(shotTarget.y - shooterPos.y, std::abs(shotTarget.x - shooterPos.x));
+
+	// Calculate projectile start position. Offsets are read out of the CRANIM.TXT.
+	if (projectileAngle > straightAngle)
+		setGroup(getUpwardsGroup());
+	else if (projectileAngle < -straightAngle)
+		setGroup(getDownwardsGroup());
+	else
+		setGroup(getForwardGroup());
+}
+
+void RangedAttackAnimation::initializeProjectile()
+{
+	const CCreature *shooterInfo = getCreature();
+	Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225);
+	Point shotOrigin = stackAnimation(attackingStack)->pos.topLeft() + Point(222, 265);
+	int multiplier = stackFacingRight(attackingStack) ? 1 : -1;
+
+	if (getGroup() == getUpwardsGroup())
+	{
+		shotOrigin.x += ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier;
+		shotOrigin.y += shooterInfo->animation.upperRightMissleOffsetY;
+	}
+	else if (getGroup() == getDownwardsGroup())
+	{
+		shotOrigin.x += ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier;
+		shotOrigin.y += shooterInfo->animation.lowerRightMissleOffsetY;
+	}
+	else if (getGroup() == getForwardGroup())
+	{
+		shotOrigin.x += ( -25 + shooterInfo->animation.rightMissleOffsetX ) * multiplier;
+		shotOrigin.y += shooterInfo->animation.rightMissleOffsetY;
+	}
+	else
+	{
+		assert(0);
+	}
+
+	createProjectile(shotOrigin, shotTarget);
+}
+
+void RangedAttackAnimation::emitProjectile()
+{
+	logAnim->debug("Ranged attack projectile emitted");
+	owner.projectilesController->emitStackProjectile(attackingStack);
+	projectileEmitted = true;
+}
+
+void RangedAttackAnimation::nextFrame()
+{
+	// animation should be paused if there is an active projectile
+	if (projectileEmitted)
+	{
+		if (!owner.projectilesController->hasActiveProjectile(attackingStack, false))
+		{
+			if(owner.getAnimationCondition(EAnimationEvents::HIT) == false)
+				owner.setAnimationCondition(EAnimationEvents::HIT, true);
+		}
+	}
+
+	AttackAnimation::nextFrame();
+
+	if (!projectileEmitted)
+	{
+		// emit projectile once animation playback reached "climax" frame
+		if ( stackAnimation(attackingStack)->getCurrentFrame() >= getAttackClimaxFrame() )
+		{
+			emitProjectile();
+			return;
+		}
+	}
+}
+
+RangedAttackAnimation::~RangedAttackAnimation()
+{
+	assert(!owner.projectilesController->hasActiveProjectile(attackingStack, false));
+	assert(projectileEmitted);
+
+	// FIXME: is this possible? Animation is over but we're yet to fire projectile?
+	if (!projectileEmitted)
+	{
+		logAnim->warn("Shooting animation has finished but projectile was not emitted! Emitting it now...");
+		emitProjectile();
+	}
+}
+
+ShootingAnimation::ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked)
+	: RangedAttackAnimation(owner, attacker, _dest, _attacked)
+{
+	logAnim->debug("Created ShootingAnimation for %s", stack->getName());
+}
+
+void ShootingAnimation::createProjectile(const Point & from, const Point & dest) const
+{
+	owner.projectilesController->createProjectile(attackingStack, from, dest);
+}
+
+uint32_t ShootingAnimation::getAttackClimaxFrame() const
+{
+	const CCreature *shooterInfo = getCreature();
+	return shooterInfo->animation.attackClimaxFrame;
+}
+
+ECreatureAnimType ShootingAnimation::getUpwardsGroup() const
+{
+	return ECreatureAnimType::SHOOT_UP;
+}
+
+ECreatureAnimType ShootingAnimation::getForwardGroup() const
+{
+	return ECreatureAnimType::SHOOT_FRONT;
+}
+
+ECreatureAnimType ShootingAnimation::getDownwardsGroup() const
+{
+	return ECreatureAnimType::SHOOT_DOWN;
+}
+
+CatapultAnimation::CatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, int _catapultDmg)
+	: ShootingAnimation(owner, attacker, _dest, _attacked),
+	catapultDamage(_catapultDmg),
+	explosionEmitted(false)
+{
+	logAnim->debug("Created shooting anim for %s", stack->getName());
+}
+
+void CatapultAnimation::nextFrame()
+{
+	ShootingAnimation::nextFrame();
+
+	if ( explosionEmitted)
+		return;
+
+	if ( !projectileEmitted)
+		return;
+
+	if (owner.projectilesController->hasActiveProjectile(attackingStack, false))
+		return;
+
+	explosionEmitted = true;
+	Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225) - Point(126, 105);
+
+	std::string soundFilename  = (catapultDamage > 0) ? "WALLHIT" : "WALLMISS";
+	std::string effectFilename = (catapultDamage > 0) ? "SGEXPL" : "CSGRCK";
+
+	CCS->soundh->playSound( soundFilename );
+	owner.stacksController->addNewAnim( new EffectAnimation(owner, effectFilename, shotTarget));
+}
+
+void CatapultAnimation::createProjectile(const Point & from, const Point & dest) const
+{
+	owner.projectilesController->createCatapultProjectile(attackingStack, from, dest);
+}
+
+CastAnimation::CastAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest, const CStack * defender, const CSpell * spell)
+	: RangedAttackAnimation(owner_, attacker, dest, defender),
+	  spell(spell)
+{
+	if(!dest.isValid())
+	{
+		assert(spell->animationInfo.projectile.empty());
+
+		if (defender)
+			dest = defender->getPosition();
+		else
+			dest = attacker->getPosition();
+	}
+}
+
+ECreatureAnimType CastAnimation::getUpwardsGroup() const
+{
+	return findValidGroup({
+		ECreatureAnimType::CAST_UP,
+		ECreatureAnimType::SPECIAL_UP,
+		ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3
+		ECreatureAnimType::SHOOT_UP,
+		ECreatureAnimType::ATTACK_UP
+	});
+}
+
+ECreatureAnimType CastAnimation::getForwardGroup() const
+{
+	return findValidGroup({
+		ECreatureAnimType::CAST_FRONT,
+		ECreatureAnimType::SPECIAL_FRONT,
+		ECreatureAnimType::SHOOT_FRONT,
+		ECreatureAnimType::ATTACK_FRONT
+	});
+}
+
+ECreatureAnimType CastAnimation::getDownwardsGroup() const
+{
+	return findValidGroup({
+		ECreatureAnimType::CAST_DOWN,
+		ECreatureAnimType::SPECIAL_DOWN,
+		ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3
+		ECreatureAnimType::SHOOT_DOWN,
+		ECreatureAnimType::ATTACK_DOWN
+	});
+}
+
+void CastAnimation::createProjectile(const Point & from, const Point & dest) const
+{
+	if (!spell->animationInfo.projectile.empty())
+		owner.projectilesController->createSpellProjectile(attackingStack, from, dest, spell);
+}
+
+uint32_t CastAnimation::getAttackClimaxFrame() const
+{
+	//TODO: allow defining this parameter in config file, separately from attackClimaxFrame of missile attacks
+	uint32_t maxFrames = stackAnimation(attackingStack)->framesInGroup(getGroup());
+
+	return maxFrames / 2;
+}
+
+EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, int effects):
+	BattleAnimation(owner),
+	animation(std::make_shared<CAnimation>(animationName)),
+	effectFlags(effects),
+	effectFinished(false)
+{
+	logAnim->debug("CPointEffectAnimation::init: effect %s", animationName);
+}
+
+EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, std::vector<BattleHex> hex, int effects):
+	EffectAnimation(owner, animationName, effects)
+{
+	battlehexes = hex;
+}
+
+EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, BattleHex hex, int effects):
+	EffectAnimation(owner, animationName, effects)
+{
+	assert(hex.isValid());
+	battlehexes.push_back(hex);
+}
+
+EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, std::vector<Point> pos, int effects):
+	EffectAnimation(owner, animationName, effects)
+{
+	positions = pos;
+}
+
+EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, Point pos, int effects):
+	EffectAnimation(owner, animationName, effects)
+{
+	positions.push_back(pos);
+}
+
+EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, Point pos, BattleHex hex,   int effects):
+	EffectAnimation(owner, animationName, effects)
+{
+	assert(hex.isValid());
+	battlehexes.push_back(hex);
+	positions.push_back(pos);
+}
+
+bool EffectAnimation::init()
+{
+	animation->preload();
+
+	auto first = animation->getImage(0, 0, true);
+	if(!first)
+	{
+		delete this;
+		return false;
+	}
+
+	if (screenFill())
+	{
+		for(int i=0; i * first->width() < owner.fieldController->pos.w ; ++i)
+			for(int j=0; j * first->height() < owner.fieldController->pos.h ; ++j)
+				positions.push_back(Point( i * first->width(), j * first->height()));
+	}
+
+	BattleEffect be;
+	be.effectID = ID;
+	be.animation = animation;
+	be.currentFrame = 0;
+
+	for (size_t i = 0; i < std::max(battlehexes.size(), positions.size()); ++i)
+	{
+		bool hasTile = i < battlehexes.size();
+		bool hasPosition = i < positions.size();
+
+		if (hasTile && !forceOnTop())
+			be.tile = battlehexes[i];
+		else
+			be.tile = BattleHex::INVALID;
+
+		if (hasPosition)
+		{
+			be.pos.x = positions[i].x;
+			be.pos.y = positions[i].y;
+		}
+		else
+		{
+			const CStack * destStack = owner.getCurrentPlayerInterface()->cb->battleGetStackByPos(battlehexes[i], false);
+			Rect tilePos = owner.fieldController->hexPositionLocal(battlehexes[i]);
+
+			be.pos.x = tilePos.x + tilePos.w/2 - first->width()/2;
+
+			if(destStack && destStack->doubleWide()) // Correction for 2-hex creatures.
+				be.pos.x += (destStack->side == BattleSide::ATTACKER ? -1 : 1)*tilePos.w/2;
+
+			if (alignToBottom())
+				be.pos.y = tilePos.y + tilePos.h - first->height();
+			else
+				be.pos.y = tilePos.y - first->height()/2;
+		}
+		owner.effectsController->battleEffects.push_back(be);
+	}
+	return true;
+}
+
+void EffectAnimation::nextFrame()
+{
+	playEffect();
+
+	if (effectFinished)
+	{
+		//remove visual effect itself only if sound has finished as well - necessary for obstacles like force field
+		clearEffect();
+		delete this;
+	}
+}
+
+bool EffectAnimation::alignToBottom() const
+{
+	return effectFlags & ALIGN_TO_BOTTOM;
+}
+
+bool EffectAnimation::forceOnTop() const
+{
+	return effectFlags & FORCE_ON_TOP;
+}
+
+bool EffectAnimation::screenFill() const
+{
+	return effectFlags & SCREEN_FILL;
+}
+
+void EffectAnimation::onEffectFinished()
+{
+	effectFinished = true;
+}
+
+void EffectAnimation::playEffect()
+{
+	if ( effectFinished )
+		return;
+
+	for(auto & elem : owner.effectsController->battleEffects)
+	{
+		if(elem.effectID == ID)
+		{
+			elem.currentFrame += AnimationControls::getSpellEffectSpeed() * GH.mainFPSmng->getElapsedMilliseconds() / 1000;
+
+			if(elem.currentFrame >= elem.animation->size())
+			{
+				elem.currentFrame = elem.animation->size() - 1;
+				onEffectFinished();
+				break;
+			}
+		}
+	}
+}
+
+void EffectAnimation::clearEffect()
+{
+	auto & effects = owner.effectsController->battleEffects;
+
+	vstd::erase_if(effects, [&](const BattleEffect & effect){
+		return effect.effectID == ID;
+	});
+}
+
+EffectAnimation::~EffectAnimation()
+{
+	assert(effectFinished);
+}
+
+HeroCastAnimation::HeroCastAnimation(BattleInterface & owner, std::shared_ptr<BattleHero> hero, BattleHex dest, const CStack * defender, const CSpell * spell):
+	BattleAnimation(owner),
+	projectileEmitted(false),
+	hero(hero),
+	target(defender),
+	tile(dest),
+	spell(spell)
+{
+}
+
+bool HeroCastAnimation::init()
+{
+	hero->setPhase(EHeroAnimType::CAST_SPELL);
+
+	hero->onPhaseFinished([&](){
+		assert(owner.getAnimationCondition(EAnimationEvents::HIT) == true);
+		delete this;
+	});
+
+	initializeProjectile();
+
+	return true;
+}
+
+void HeroCastAnimation::initializeProjectile()
+{
+	// spell has no projectile to play, ignore this step
+	if (spell->animationInfo.projectile.empty())
+		return;
+
+	// targeted spells should have well, target
+	assert(tile.isValid());
+
+	Point srccoord = hero->pos.center() - hero->parent->pos.topLeft();
+	Point destcoord = owner.stacksController->getStackPositionAtHex(tile, target); //position attacked by projectile
+
+	destcoord += Point(222, 265); // FIXME: what are these constants?
+	owner.projectilesController->createSpellProjectile( nullptr, srccoord, destcoord, spell);
+}
+
+void HeroCastAnimation::emitProjectile()
+{
+	if (projectileEmitted)
+		return;
+
+	//spell has no projectile to play, skip this step and immediately play hit animations
+	if (spell->animationInfo.projectile.empty())
+		emitAnimationEvent();
+	else
+		owner.projectilesController->emitStackProjectile( nullptr );
+
+	projectileEmitted = true;
+}
+
+void HeroCastAnimation::emitAnimationEvent()
+{
+	if(owner.getAnimationCondition(EAnimationEvents::HIT) == false)
+		owner.setAnimationCondition(EAnimationEvents::HIT, true);
+}
+
+void HeroCastAnimation::nextFrame()
+{
+	float frame = hero->getFrame();
+
+	if (frame < 4.0f) // middle point of animation //TODO: un-hardcode
+		return;
+
+	if (!projectileEmitted)
+	{
+		emitProjectile();
+		hero->pause();
+		return;
+	}
+
+	if (!owner.projectilesController->hasActiveProjectile(nullptr, false))
+	{
+		emitAnimationEvent();
+		//TODO: check H3 - it is possible that hero animation should be paused until hit effect is over, not just projectile
+		hero->play();
+	}
+}

+ 367 - 0
client/battle/BattleAnimationClasses.h

@@ -0,0 +1,367 @@
+/*
+ * BattleAnimations.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/BattleHex.h"
+#include "../gui/Geometries.h"
+#include "BattleConstants.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CStack;
+class CCreature;
+class CSpell;
+
+VCMI_LIB_NAMESPACE_END
+
+struct SDL_Color;
+class ColorFilter;
+class BattleHero;
+class CAnimation;
+class BattleInterface;
+class CreatureAnimation;
+struct StackAttackedInfo;
+struct Point;
+
+/// Base class of battle animations
+class BattleAnimation
+{
+protected:
+	BattleInterface & owner;
+	bool initialized;
+
+	std::vector<BattleAnimation *> & pendingAnimations();
+	std::shared_ptr<CreatureAnimation> stackAnimation(const CStack * stack) const;
+	bool stackFacingRight(const CStack * stack);
+	void setStackFacingRight(const CStack * stack, bool facingRight);
+
+	virtual bool init() = 0; //to be called - if returned false, call again until returns true
+
+public:
+	ui32 ID; //unique identifier
+
+	bool isInitialized();
+	bool tryInitialize();
+	virtual void nextFrame() {} //call every new frame
+	virtual ~BattleAnimation();
+
+	BattleAnimation(BattleInterface & owner);
+};
+
+/// Sub-class which is responsible for managing the battle stack animation.
+class BattleStackAnimation : public BattleAnimation
+{
+public:
+	std::shared_ptr<CreatureAnimation> myAnim; //animation for our stack, managed by BattleInterface
+	const CStack * stack; //id of stack whose animation it is
+
+	BattleStackAnimation(BattleInterface & owner, const CStack * _stack);
+	void rotateStack(BattleHex hex);
+};
+
+class StackActionAnimation : public BattleStackAnimation
+{
+	ECreatureAnimType nextGroup;
+	ECreatureAnimType currGroup;
+	std::string sound;
+public:
+	void setNextGroup( ECreatureAnimType group );
+	void setGroup( ECreatureAnimType group );
+	void setSound( std::string sound );
+
+	ECreatureAnimType getGroup() const;
+
+	StackActionAnimation(BattleInterface & owner, const CStack * _stack);
+	~StackActionAnimation();
+
+	bool init() override;
+};
+
+/// Animation of a defending unit
+class DefenceAnimation : public StackActionAnimation
+{
+public:
+	DefenceAnimation(BattleInterface & owner, const CStack * stack);
+};
+
+/// Animation of a hit unit
+class HittedAnimation : public StackActionAnimation
+{
+public:
+	HittedAnimation(BattleInterface & owner, const CStack * stack);
+};
+
+/// Animation of a dying unit
+class DeathAnimation : public StackActionAnimation
+{
+public:
+	DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged);
+};
+
+/// Resurrects stack from dead state
+class ResurrectionAnimation : public StackActionAnimation
+{
+public:
+	ResurrectionAnimation(BattleInterface & owner, const CStack * _stack);
+};
+
+class ColorTransformAnimation : public BattleStackAnimation
+{
+	std::vector<ColorFilter> steps;
+	std::vector<float> timePoints;
+	const CSpell * spell;
+
+	float totalProgress;
+
+	bool init() override;
+	void nextFrame() override;
+
+public:
+	ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell);
+};
+
+/// Base class for all animations that play during stack movement
+class StackMoveAnimation : public BattleStackAnimation
+{
+public:
+	BattleHex nextHex;
+	BattleHex prevHex;
+
+protected:
+	StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex);
+};
+
+/// Move animation of a creature
+class MovementAnimation : public StackMoveAnimation
+{
+private:
+	std::vector<BattleHex> destTiles; //full path, includes already passed hexes
+	ui32 curentMoveIndex; // index of nextHex in destTiles
+
+	double begX, begY; // starting position
+	double distanceX, distanceY; // full movement distance, may be negative if creture moves topleft
+
+	double timeToMove; // full length of movement animation
+	double progress; // range 0 -> 1, indicates move progrees. 0 = movement starts, 1 = move ends
+
+public:
+	bool init() override;
+	void nextFrame() override;
+
+	MovementAnimation(BattleInterface & owner, const CStack *_stack, std::vector<BattleHex> _destTiles, int _distance);
+	~MovementAnimation();
+};
+
+/// Move end animation of a creature
+class MovementEndAnimation : public StackMoveAnimation
+{
+public:
+	bool init() override;
+
+	MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile);
+	~MovementEndAnimation();
+};
+
+/// Move start animation of a creature
+class MovementStartAnimation : public StackMoveAnimation
+{
+public:
+	bool init() override;
+
+	MovementStartAnimation(BattleInterface & owner, const CStack * _stack);
+};
+
+/// Class responsible for animation of stack chaning direction (left <-> right)
+class ReverseAnimation : public StackMoveAnimation
+{
+	void setupSecondPart();
+public:
+	bool init() override;
+
+	ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest);
+};
+
+/// This class is responsible for managing the battle attack animation
+class AttackAnimation : public StackActionAnimation
+{
+protected:
+	BattleHex dest; //attacked hex
+	const CStack *defendingStack;
+	const CStack *attackingStack;
+	int attackingStackPosBeforeReturn; //for stacks with return_after_strike feature
+
+	const CCreature * getCreature() const;
+	ECreatureAnimType findValidGroup( const std::vector<ECreatureAnimType> candidates ) const;
+
+public:
+	AttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender);
+};
+
+/// Hand-to-hand attack
+class MeleeAttackAnimation : public AttackAnimation
+{
+	ECreatureAnimType getUpwardsGroup(bool multiAttack) const;
+	ECreatureAnimType getForwardGroup(bool multiAttack) const;
+	ECreatureAnimType getDownwardsGroup(bool multiAttack) const;
+
+	ECreatureAnimType selectGroup(bool multiAttack);
+
+public:
+	MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack);
+
+	void nextFrame() override;
+};
+
+
+class RangedAttackAnimation : public AttackAnimation
+{
+	void setAnimationGroup();
+	void initializeProjectile();
+	void emitProjectile();
+	void emitExplosion();
+
+protected:
+	bool projectileEmitted;
+
+	virtual ECreatureAnimType getUpwardsGroup() const = 0;
+	virtual ECreatureAnimType getForwardGroup() const = 0;
+	virtual ECreatureAnimType getDownwardsGroup() const = 0;
+
+	virtual void createProjectile(const Point & from, const Point & dest) const = 0;
+	virtual uint32_t getAttackClimaxFrame() const = 0;
+
+public:
+	RangedAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender);
+	~RangedAttackAnimation();
+
+	bool init() override;
+	void nextFrame() override;
+};
+
+/// Shooting attack
+class ShootingAnimation : public RangedAttackAnimation
+{
+	ECreatureAnimType getUpwardsGroup() const override;
+	ECreatureAnimType getForwardGroup() const override;
+	ECreatureAnimType getDownwardsGroup() const override;
+
+	void createProjectile(const Point & from, const Point & dest) const override;
+	uint32_t getAttackClimaxFrame() const override;
+
+public:
+	ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender);
+
+};
+
+/// Catapult attack
+class CatapultAnimation : public ShootingAnimation
+{
+private:
+	bool explosionEmitted;
+	int catapultDamage;
+
+public:
+	CatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender, int _catapultDmg = 0);
+
+	void createProjectile(const Point & from, const Point & dest) const override;
+	void nextFrame() override;
+};
+
+class CastAnimation : public RangedAttackAnimation
+{
+	const CSpell * spell;
+
+	ECreatureAnimType getUpwardsGroup() const override;
+	ECreatureAnimType getForwardGroup() const override;
+	ECreatureAnimType getDownwardsGroup() const override;
+
+	void createProjectile(const Point & from, const Point & dest) const override;
+	uint32_t getAttackClimaxFrame() const override;
+
+public:
+	CastAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest_, const CStack * defender, const CSpell * spell);
+};
+
+class DummyAnimation : public BattleAnimation
+{
+private:
+	int counter;
+	int howMany;
+public:
+	bool init() override;
+	void nextFrame() override;
+
+	DummyAnimation(BattleInterface & owner, int howManyFrames);
+};
+
+/// Class that plays effect at one or more positions along with (single) sound effect
+class EffectAnimation : public BattleAnimation
+{
+	std::string soundName;
+	bool effectFinished;
+	int effectFlags;
+
+	std::shared_ptr<CAnimation>	animation;
+	std::vector<Point> positions;
+	std::vector<BattleHex> battlehexes;
+
+	bool alignToBottom() const;
+	bool waitForSound() const;
+	bool forceOnTop() const;
+	bool screenFill() const;
+
+	void onEffectFinished();
+	void clearEffect();
+	void playEffect();
+
+public:
+	enum EEffectFlags
+	{
+		ALIGN_TO_BOTTOM = 1,
+		FORCE_ON_TOP    = 2,
+		SCREEN_FILL     = 4,
+	};
+
+	/// Create animation with screen-wide effect
+	EffectAnimation(BattleInterface & owner, std::string animationName, int effects = 0);
+
+	/// Create animation positioned at point(s). Note that positions must be are absolute, including battleint position offset
+	EffectAnimation(BattleInterface & owner, std::string animationName, Point pos                 , int effects = 0);
+	EffectAnimation(BattleInterface & owner, std::string animationName, std::vector<Point> pos    , int effects = 0);
+
+	/// Create animation positioned at certain hex(es)
+	EffectAnimation(BattleInterface & owner, std::string animationName, BattleHex hex             , int effects = 0);
+	EffectAnimation(BattleInterface & owner, std::string animationName, std::vector<BattleHex> hex, int effects = 0);
+
+	EffectAnimation(BattleInterface & owner, std::string animationName, Point pos, BattleHex hex,   int effects = 0);
+	 ~EffectAnimation();
+
+	bool init() override;
+	void nextFrame() override;
+};
+
+class HeroCastAnimation : public BattleAnimation
+{
+	std::shared_ptr<BattleHero> hero;
+	const CStack * target;
+	const CSpell * spell;
+	BattleHex tile;
+	bool projectileEmitted;
+
+	void initializeProjectile();
+	void emitProjectile();
+	void emitAnimationEvent();
+
+public:
+	HeroCastAnimation(BattleInterface & owner, std::shared_ptr<BattleHero> hero, BattleHex dest, const CStack * defender, const CSpell * spell);
+
+	void nextFrame() override;
+	bool init() override;
+};

+ 92 - 0
client/battle/BattleConstants.h

@@ -0,0 +1,92 @@
+/*
+ * BattleConstants.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
+
+enum class EBattleEffect
+{
+	// list of battle effects that have hardcoded triggers
+	MAGIC_MIRROR = 3,
+	FIRE_SHIELD  = 11,
+	FEAR         = 15,
+	GOOD_LUCK    = 18,
+	GOOD_MORALE  = 20,
+	BAD_MORALE   = 30,
+	BAD_LUCK     = 48,
+	RESURRECT    = 50,
+	DRAIN_LIFE   = 52,
+	POISON       = 67,
+	DEATH_BLOW   = 73,
+	REGENERATION = 74,
+	MANA_DRAIN   = 77,
+	RESISTANCE   = 78,
+
+	INVALID      = -1,
+};
+
+enum class EAnimationEvents {
+	OPENING     = 0, // battle opening sound is playing
+	ACTION      = 1, // there are any ongoing animations
+	MOVEMENT    = 2, // stacks are moving or turning around
+	BEFORE_HIT  = 3, // effects played before all attack/defence/hit animations
+	ATTACK      = 4, // attack and defence animations are playing
+	HIT         = 5, // hit & death animations are playing
+	AFTER_HIT   = 6, // after all hit & death animations are over
+	COUNT
+};
+
+enum class EHeroAnimType
+{
+	HOLDING    = 0,
+	IDLE       = 1, // idling movement that happens from time to time
+	DEFEAT     = 2, // played when army loses stack or on friendly fire
+	VICTORY    = 3, // when enemy stack killed or huge damage is dealt
+	CAST_SPELL = 4  // spellcasting
+};
+
+enum class ECreatureAnimType
+{
+	INVALID         = -1,
+
+	MOVING          = 0,
+	MOUSEON         = 1,
+	HOLDING         = 2,  // base idling animation
+	HITTED          = 3,  // base animation for when stack is taking damage
+	DEFENCE         = 4,  // alternative animation for defending in melee if stack spent its action on defending
+	DEATH           = 5,
+	DEATH_RANGED    = 6,  // Optional, alternative animation for when stack is killed by ranged attack
+	TURN_L          = 7,
+	TURN_R          = 8,
+	//TURN_L2       = 9,  //unused - identical to TURN_L
+	//TURN_R2       = 10, //unused - identical to TURN_R
+	ATTACK_UP       = 11,
+	ATTACK_FRONT    = 12,
+	ATTACK_DOWN     = 13,
+	SHOOT_UP        = 14, // Shooters only
+	SHOOT_FRONT     = 15, // Shooters only
+	SHOOT_DOWN      = 16, // Shooters only
+	SPECIAL_UP      = 17, // If empty, fallback to SPECIAL_FRONT
+	SPECIAL_FRONT   = 18, // Used for any special moves - dragon breath, spellcasting, Pit Lord/Ogre Mage ability
+	SPECIAL_DOWN    = 19, // If empty, fallback to SPECIAL_FRONT
+	MOVE_START      = 20, // small animation to be played before MOVING
+	MOVE_END        = 21, // small animation to be played after MOVING
+
+	DEAD            = 22, // new group, used to show dead stacks. If empty - last frame from "DEATH" will be copied here
+	DEAD_RANGED     = 23, // new group, used to show dead stacks (if DEATH_RANGED was used). If empty - last frame from "DEATH_RANGED" will be copied here
+	RESURRECTION    = 24, // new group, used for animating resurrection, if empty - reversed "DEATH" animation will be copiend here
+	FROZEN          = 25, // new group, used when stack animation is paused (e.g. petrified). If empty - consist of first frame from HOLDING animation
+
+	CAST_UP            = 30,
+	CAST_FRONT         = 31,
+	CAST_DOWN          = 32,
+
+	GROUP_ATTACK_UP    = 40,
+	GROUP_ATTACK_FRONT = 41,
+	GROUP_ATTACK_DOWN  = 42
+};

+ 168 - 0
client/battle/BattleEffectsController.cpp

@@ -0,0 +1,168 @@
+/*
+ * BattleEffectsController.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 "BattleEffectsController.h"
+
+#include "BattleAnimationClasses.h"
+#include "BattleWindow.h"
+#include "BattleInterface.h"
+#include "BattleInterfaceClasses.h"
+#include "BattleFieldController.h"
+#include "BattleStacksController.h"
+#include "BattleRenderer.h"
+
+#include "../CMusicHandler.h"
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+#include "../gui/CAnimation.h"
+#include "../gui/Canvas.h"
+
+#include "../../CCallback.h"
+#include "../../lib/battle/BattleAction.h"
+#include "../../lib/filesystem/ResourceID.h"
+#include "../../lib/NetPacks.h"
+#include "../../lib/CStack.h"
+#include "../../lib/IGameEventsReceiver.h"
+#include "../../lib/CGeneralTextHandler.h"
+
+BattleEffectsController::BattleEffectsController(BattleInterface & owner):
+	owner(owner)
+{
+	loadColorMuxers();
+}
+
+void BattleEffectsController::displayEffect(EBattleEffect effect, const BattleHex & destTile)
+{
+	displayEffect(effect, "", destTile);
+}
+
+void BattleEffectsController::displayEffect(EBattleEffect effect, std::string soundFile, const BattleHex & destTile)
+{
+	size_t effectID = static_cast<size_t>(effect);
+
+	std::string customAnim = graphics->battleACToDef[effectID][0];
+
+	CCS->soundh->playSound( soundFile );
+
+	owner.stacksController->addNewAnim(new EffectAnimation(owner, customAnim, destTile));
+}
+
+void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bte)
+{
+	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+
+	const CStack * stack = owner.curInt->cb->battleGetStackByID(bte.stackID);
+	if(!stack)
+	{
+		logGlobal->error("Invalid stack ID %d", bte.stackID);
+		return;
+	}
+	//don't show animation when no HP is regenerated
+	switch(bte.effect)
+	{
+		case Bonus::HP_REGENERATION:
+			displayEffect(EBattleEffect::REGENERATION, "REGENER", stack->getPosition());
+			break;
+		case Bonus::MANA_DRAIN:
+			displayEffect(EBattleEffect::MANA_DRAIN, "MANADRAI", stack->getPosition());
+			break;
+		case Bonus::POISON:
+			displayEffect(EBattleEffect::POISON, "POISON", stack->getPosition());
+			break;
+		case Bonus::FEAR:
+			displayEffect(EBattleEffect::FEAR, "FEAR", stack->getPosition());
+			break;
+		case Bonus::MORALE:
+		{
+			std::string hlp = CGI->generaltexth->allTexts[33];
+			boost::algorithm::replace_first(hlp,"%s",(stack->getName()));
+			displayEffect(EBattleEffect::GOOD_MORALE, "GOODMRLE", stack->getPosition());
+			owner.appendBattleLog(hlp);
+			break;
+		}
+		default:
+			return;
+	}
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+}
+
+void BattleEffectsController::startAction(const BattleAction* action)
+{
+	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+
+	const CStack *stack = owner.curInt->cb->battleGetStackByID(action->stackNumber);
+
+	switch(action->actionType)
+	{
+	case EActionType::WAIT:
+		owner.appendBattleLog(stack->formatGeneralMessage(136));
+		break;
+	case EActionType::BAD_MORALE:
+		owner.appendBattleLog(stack->formatGeneralMessage(-34));
+		displayEffect(EBattleEffect::BAD_MORALE, "BADMRLE", stack->getPosition());
+		break;
+	}
+
+	//displaying special abilities
+	auto actionTarget = action->getTarget(owner.curInt->cb.get());
+	switch(action->actionType)
+	{
+		case EActionType::STACK_HEAL:
+			displayEffect(EBattleEffect::REGENERATION, "REGENER", actionTarget.at(0).hexValue);
+			break;
+	}
+
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+}
+
+void BattleEffectsController::collectRenderableObjects(BattleRenderer & renderer)
+{
+	for (auto & elem : battleEffects)
+	{
+		renderer.insert( EBattleFieldLayer::EFFECTS, elem.tile, [&elem](BattleRenderer::RendererRef canvas)
+		{
+			int currentFrame = static_cast<int>(floor(elem.currentFrame));
+			currentFrame %= elem.animation->size();
+
+			auto img = elem.animation->getImage(currentFrame);
+
+			canvas.draw(img, elem.pos);
+		});
+	}
+}
+
+void BattleEffectsController::loadColorMuxers()
+{
+	const JsonNode config(ResourceID("config/battleEffects.json"));
+
+	for(auto & muxer : config["colorMuxers"].Struct())
+	{
+		ColorMuxerEffect effect;
+		std::string identifier = muxer.first;
+
+		for (const JsonNode & entry : muxer.second.Vector() )
+		{
+			effect.timePoints.push_back(entry["time"].Float());
+			effect.filters.push_back(ColorFilter::genFromJson(entry));
+		}
+		colorMuxerEffects[identifier] = effect;
+	}
+}
+
+const ColorMuxerEffect & BattleEffectsController::getMuxerEffect(const std::string & name)
+{
+	static const ColorMuxerEffect emptyEffect;
+
+	if (colorMuxerEffects.count(name))
+		return colorMuxerEffects[name];
+
+	logAnim->error("Failed to find color muxer effect named '%s'!", name);
+	return emptyEffect;
+}

+ 67 - 0
client/battle/BattleEffectsController.h

@@ -0,0 +1,67 @@
+/*
+ * BattleEffectsController.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/BattleHex.h"
+#include "../gui/Geometries.h"
+#include "BattleConstants.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class BattleAction;
+struct BattleTriggerEffect;
+
+VCMI_LIB_NAMESPACE_END
+
+struct ColorMuxerEffect;
+class CAnimation;
+class Canvas;
+class BattleInterface;
+class BattleRenderer;
+class EffectAnimation;
+
+/// Struct for battle effect animation e.g. morale, prayer, armageddon, bless,...
+struct BattleEffect
+{
+	Point pos; //position on the screen
+	float currentFrame;
+	std::shared_ptr<CAnimation> animation;
+	int effectID; //uniqueID equal ot ID of appropriate CSpellEffectAnim
+	BattleHex tile; //Indicates if effect which hex the effect is drawn on
+};
+
+/// Controls rendering of effects in battle, e.g. from spells, abilities and various other actions like morale
+class BattleEffectsController
+{
+	BattleInterface & owner;
+
+	/// list of current effects that are being displayed on screen (spells & creature abilities)
+	std::vector<BattleEffect> battleEffects;
+
+	std::map<std::string, ColorMuxerEffect> colorMuxerEffects;
+
+	void loadColorMuxers();
+public:
+	const ColorMuxerEffect &getMuxerEffect(const std::string & name);
+
+	BattleEffectsController(BattleInterface & owner);
+
+	void startAction(const BattleAction* action);
+
+	//displays custom effect on the battlefield
+	void displayEffect(EBattleEffect effect, const BattleHex & destTile);
+	void displayEffect(EBattleEffect effect, std::string soundFile, const BattleHex & destTile);
+
+	void battleTriggerEffect(const BattleTriggerEffect & bte);
+
+	void collectRenderableObjects(BattleRenderer & renderer);
+
+	friend class EffectAnimation;
+};

+ 556 - 0
client/battle/BattleFieldController.cpp

@@ -0,0 +1,556 @@
+/*
+ * BattleFieldController.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 "BattleFieldController.h"
+
+#include "BattleInterface.h"
+#include "BattleActionsController.h"
+#include "BattleInterfaceClasses.h"
+#include "BattleEffectsController.h"
+#include "BattleSiegeController.h"
+#include "BattleStacksController.h"
+#include "BattleObstacleController.h"
+#include "BattleProjectileController.h"
+#include "BattleRenderer.h"
+
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+#include "../widgets/AdventureMapClasses.h"
+#include "../gui/CAnimation.h"
+#include "../gui/Canvas.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/CCursorHandler.h"
+
+#include "../../CCallback.h"
+#include "../../lib/BattleFieldHandler.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CStack.h"
+#include "../../lib/spells/ISpellMechanics.h"
+
+BattleFieldController::BattleFieldController(BattleInterface & owner):
+	owner(owner)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	strongInterest = true;
+
+	//preparing cells and hexes
+	cellBorder = IImage::createFromFile("CCELLGRD.BMP");
+	cellShade = IImage::createFromFile("CCELLSHD.BMP");
+
+	if(!owner.siegeController)
+	{
+		auto bfieldType = owner.curInt->cb->battleGetBattlefieldType();
+
+		if(bfieldType == BattleField::NONE)
+			logGlobal->error("Invalid battlefield returned for current battle");
+		else
+			background = IImage::createFromFile(bfieldType.getInfo()->graphics);
+	}
+	else
+	{
+		std::string backgroundName = owner.siegeController->getBattleBackgroundName();
+		background = IImage::createFromFile(backgroundName);
+	}
+	pos.w = background->width();
+	pos.h = background->height();
+
+	//preparing graphic with cell borders
+	cellBorders = std::make_unique<Canvas>(Point(background->width(), background->height()));
+
+	for (int i=0; i<GameConstants::BFIELD_SIZE; ++i)
+	{
+		if ( i % GameConstants::BFIELD_WIDTH == 0)
+			continue;
+		if ( i % GameConstants::BFIELD_WIDTH == GameConstants::BFIELD_WIDTH - 1)
+			continue;
+
+		cellBorders->draw(cellBorder, hexPositionLocal(i).topLeft());
+	}
+
+	backgroundWithHexes = std::make_unique<Canvas>(Point(background->width(), background->height()));
+
+	for (int h = 0; h < GameConstants::BFIELD_SIZE; ++h)
+	{
+		auto hex = std::make_shared<ClickableHex>();
+		hex->myNumber = h;
+		hex->pos = hexPositionAbsolute(h);
+		hex->myInterface = &owner;
+		bfield.push_back(hex);
+	}
+
+	auto accessibility = owner.curInt->cb->getAccesibility();
+	for(int i = 0; i < accessibility.size(); i++)
+		stackCountOutsideHexes[i] = (accessibility[i] == EAccessibility::ACCESSIBLE);
+
+	addUsedEvents(MOVE);
+	LOCPLINT->cingconsole->pos = this->pos;
+}
+
+void BattleFieldController::createHeroes()
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	// create heroes as part of our constructor for correct positioning inside battlefield
+	if(owner.attackingHeroInstance)
+		owner.attackingHero = std::make_shared<BattleHero>(owner, owner.attackingHeroInstance, false);
+
+	if(owner.defendingHeroInstance)
+		owner.defendingHero = std::make_shared<BattleHero>(owner, owner.defendingHeroInstance, true);
+}
+
+void BattleFieldController::mouseMoved(const SDL_MouseMotionEvent &event)
+{
+	BattleHex selectedHex = getHoveredHex();
+
+	owner.actionsController->handleHex(selectedHex, MOVE);
+}
+
+
+void BattleFieldController::renderBattlefield(Canvas & canvas)
+{
+	Canvas clippedCanvas(canvas, pos);
+
+	showBackground(clippedCanvas);
+
+	BattleRenderer renderer(owner);
+
+	renderer.execute(clippedCanvas);
+
+	owner.projectilesController->showProjectiles(clippedCanvas);
+}
+
+void BattleFieldController::showBackground(Canvas & canvas)
+{
+	if (owner.stacksController->getActiveStack() != nullptr ) //&& creAnims[stacksController->getActiveStack()->ID]->isIdle() //show everything with range
+		showBackgroundImageWithHexes(canvas);
+	else
+		showBackgroundImage(canvas);
+
+	showHighlightedHexes(canvas);
+
+}
+
+void BattleFieldController::showBackgroundImage(Canvas & canvas)
+{
+	canvas.draw(background, Point(0, 0));
+
+	owner.obstacleController->showAbsoluteObstacles(canvas);
+	if ( owner.siegeController )
+		owner.siegeController->showAbsoluteObstacles(canvas);
+
+	if (settings["battle"]["cellBorders"].Bool())
+		canvas.draw(*cellBorders, Point(0, 0));
+}
+
+void BattleFieldController::showBackgroundImageWithHexes(Canvas & canvas)
+{
+	canvas.draw(*backgroundWithHexes.get(), Point(0, 0));
+}
+
+void BattleFieldController::redrawBackgroundWithHexes()
+{
+	const CStack *activeStack = owner.stacksController->getActiveStack();
+	std::vector<BattleHex> attackableHexes;
+	if (activeStack)
+		occupyableHexes = owner.curInt->cb->battleGetAvailableHexes(activeStack, true, &attackableHexes);
+
+	auto accessibility = owner.curInt->cb->getAccesibility();
+
+	for(int i = 0; i < accessibility.size(); i++)
+		stackCountOutsideHexes[i] = (accessibility[i] == EAccessibility::ACCESSIBLE);
+
+	//prepare background graphic with hexes and shaded hexes
+	backgroundWithHexes->draw(background, Point(0,0));
+	owner.obstacleController->showAbsoluteObstacles(*backgroundWithHexes);
+	if ( owner.siegeController )
+		owner.siegeController->showAbsoluteObstacles(*backgroundWithHexes);
+
+	if (settings["battle"]["stackRange"].Bool())
+	{
+		std::vector<BattleHex> hexesToShade = occupyableHexes;
+		hexesToShade.insert(hexesToShade.end(), attackableHexes.begin(), attackableHexes.end());
+		for (BattleHex hex : hexesToShade)
+		{
+			backgroundWithHexes->draw(cellShade, hexPositionLocal(hex).topLeft());
+		}
+	}
+
+	if(settings["battle"]["cellBorders"].Bool())
+		backgroundWithHexes->draw(*cellBorders, Point(0, 0));
+}
+
+void BattleFieldController::showHighlightedHex(Canvas & canvas, BattleHex hex, bool darkBorder)
+{
+	Point hexPos = hexPositionLocal(hex).topLeft();
+
+	canvas.draw(cellShade, hexPos);
+	if(!darkBorder && settings["battle"]["cellBorders"].Bool())
+		canvas.draw(cellBorder, hexPos);
+}
+
+std::set<BattleHex> BattleFieldController::getHighlightedHexesStackRange()
+{
+	std::set<BattleHex> result;
+
+	if ( !owner.stacksController->getActiveStack())
+		return result;
+
+	if ( !settings["battle"]["stackRange"].Bool())
+		return result;
+
+	auto hoveredHex = getHoveredHex();
+
+	std::set<BattleHex> set = owner.curInt->cb->battleGetAttackedHexes(owner.stacksController->getActiveStack(), hoveredHex, attackingHex);
+	for(BattleHex hex : set)
+		result.insert(hex);
+
+	// display the movement shadow of the stack at b (i.e. stack under mouse)
+	const CStack * const shere = owner.curInt->cb->battleGetStackByPos(hoveredHex, false);
+	if(shere && shere != owner.stacksController->getActiveStack() && shere->alive())
+	{
+		std::vector<BattleHex> v = owner.curInt->cb->battleGetAvailableHexes(shere, true, nullptr);
+		for(BattleHex hex : v)
+			result.insert(hex);
+	}
+	return result;
+}
+
+std::set<BattleHex> BattleFieldController::getHighlightedHexesSpellRange()
+{
+	std::set<BattleHex> result;
+	auto hoveredHex = getHoveredHex();
+
+	if(!settings["battle"]["mouseShadow"].Bool())
+		return result;
+
+	const spells::Caster *caster = nullptr;
+	const CSpell *spell = nullptr;
+
+	spells::Mode mode = spells::Mode::HERO;
+
+	if(owner.actionsController->spellcastingModeActive())//hero casts spell
+	{
+		spell = owner.actionsController->selectedSpell().toSpell();
+		caster = owner.getActiveHero();
+	}
+	else if(owner.stacksController->activeStackSpellToCast() != SpellID::NONE)//stack casts spell
+	{
+		spell = SpellID(owner.stacksController->activeStackSpellToCast()).toSpell();
+		caster = owner.stacksController->getActiveStack();
+		mode = spells::Mode::CREATURE_ACTIVE;
+	}
+
+	if(caster && spell) //when casting spell
+	{
+		// printing shaded hex(es)
+		spells::BattleCast event(owner.curInt->cb.get(), caster, mode, spell);
+		auto shaded = spell->battleMechanics(&event)->rangeInHexes(hoveredHex);
+
+		for(BattleHex shadedHex : shaded)
+		{
+			if((shadedHex.getX() != 0) && (shadedHex.getX() != GameConstants::BFIELD_WIDTH - 1))
+				result.insert(shadedHex);
+		}
+	}
+	return result;
+}
+
+std::set<BattleHex> BattleFieldController::getHighlightedHexesMovementTarget()
+{
+	const CStack * stack = owner.stacksController->getActiveStack();
+	auto hoveredHex = getHoveredHex();
+
+	if (stack)
+	{
+		std::vector<BattleHex> v = owner.curInt->cb->battleGetAvailableHexes(stack, false, nullptr);
+
+		auto hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true);
+		if(owner.curInt->cb->battleCanAttack(stack, hoveredStack, hoveredHex))
+		{
+			if (isTileAttackable(hoveredHex))
+			{
+				BattleHex attackFromHex = fromWhichHexAttack(hoveredHex);
+
+				if (stack->doubleWide())
+					return {attackFromHex, stack->occupiedHex(attackFromHex)};
+				else
+					return {attackFromHex};
+			}
+		}
+
+		if (vstd::contains(v,hoveredHex))
+		{
+			if (stack->doubleWide())
+				return {hoveredHex, stack->occupiedHex(hoveredHex)};
+			else
+				return {hoveredHex};
+		}
+		if (stack->doubleWide())
+		{
+			for (auto const & hex : v)
+			{
+				if (stack->occupiedHex(hex) == hoveredHex)
+					return { hoveredHex, hex };
+			}
+		}
+	}
+	return {};
+}
+
+void BattleFieldController::showHighlightedHexes(Canvas & canvas)
+{
+	std::set<BattleHex> hoveredStack = getHighlightedHexesStackRange();
+	std::set<BattleHex> hoveredSpell = getHighlightedHexesSpellRange();
+	std::set<BattleHex> hoveredMove  = getHighlightedHexesMovementTarget();
+
+	auto const & hoveredMouse = owner.actionsController->spellcastingModeActive() ? hoveredSpell : hoveredMove;
+
+	for(int b=0; b<GameConstants::BFIELD_SIZE; ++b)
+	{
+		bool stack = hoveredStack.count(b);
+		bool mouse = hoveredMouse.count(b);
+
+		if ( stack && mouse )
+		{
+			// area where enemy stack can move AND affected by mouse cursor - create darker highlight by blitting twice
+			showHighlightedHex(canvas, b, true);
+			showHighlightedHex(canvas, b, true);
+		}
+		if ( !stack && mouse )
+		{
+			showHighlightedHex(canvas, b, true);
+		}
+		if ( stack && !mouse )
+		{
+			showHighlightedHex(canvas, b, false);
+		}
+	}
+}
+
+Rect BattleFieldController::hexPositionLocal(BattleHex hex) const
+{
+	int x = 14 + ((hex.getY())%2==0 ? 22 : 0) + 44*hex.getX();
+	int y = 86 + 42 *hex.getY();
+	int w = cellShade->width();
+	int h = cellShade->height();
+	return Rect(x, y, w, h);
+}
+
+Rect BattleFieldController::hexPositionAbsolute(BattleHex hex) const
+{
+	return hexPositionLocal(hex) + pos.topLeft();
+}
+
+bool BattleFieldController::isPixelInHex(Point const & position)
+{
+	return !cellShade->isTransparent(position);
+}
+
+BattleHex BattleFieldController::getHoveredHex()
+{
+	for ( auto const & hex : bfield)
+		if (hex->hovered && hex->strictHovered)
+			return hex->myNumber;
+
+	return BattleHex::INVALID;
+}
+
+void BattleFieldController::setBattleCursor(BattleHex myNumber)
+{
+	Point cursorPos = CCS->curh->position();
+
+	std::vector<Cursor::Combat> sectorCursor = {
+		Cursor::Combat::HIT_SOUTHEAST,
+		Cursor::Combat::HIT_SOUTHWEST,
+		Cursor::Combat::HIT_WEST,
+		Cursor::Combat::HIT_NORTHWEST,
+		Cursor::Combat::HIT_NORTHEAST,
+		Cursor::Combat::HIT_EAST,
+		Cursor::Combat::HIT_SOUTH,
+		Cursor::Combat::HIT_NORTH,
+	};
+
+	auto direction = static_cast<size_t>(selectAttackDirection(myNumber, cursorPos));
+
+	assert(direction != -1);
+	if (direction != -1)
+		CCS->curh->set(sectorCursor[direction]);
+}
+
+BattleHex::EDir BattleFieldController::selectAttackDirection(BattleHex myNumber, const Point & cursorPos)
+{
+	const bool doubleWide = owner.stacksController->getActiveStack()->doubleWide();
+	auto neighbours = myNumber.allNeighbouringTiles();
+	//   0 1
+	//  5 x 2
+	//   4 3
+
+	// if true - our current stack can move into this hex (and attack)
+	std::array<bool, 8> attackAvailability;
+
+	if (doubleWide)
+	{
+		// For double-hexes we need to ensure that both hexes needed for this direction are occupyable:
+		// |    -0-   |   -1-    |    -2-   |   -3-    |    -4-   |   -5-    |    -6-   |   -7-
+		// |  o o -   |   - o o  |    - -   |   - -    |    - -   |   - -    |    o o   |   - -
+		// |   - x -  |  - x -   |   - x o o|  - x -   |   - x -  |o o x -   |   - x -  |  - x -
+		// |    - -   |   - -    |    - -   |   - o o  |  o o -   |   - -    |    - -   |   o o
+
+		for (size_t i : { 1, 2, 3})
+			attackAvailability[i] = vstd::contains(occupyableHexes, neighbours[i]) && vstd::contains(occupyableHexes, neighbours[i].cloneInDirection(BattleHex::RIGHT, false));
+
+		for (size_t i : { 4, 5, 0})
+			attackAvailability[i] = vstd::contains(occupyableHexes, neighbours[i]) && vstd::contains(occupyableHexes, neighbours[i].cloneInDirection(BattleHex::LEFT, false));
+
+		attackAvailability[6] = vstd::contains(occupyableHexes, neighbours[0]) && vstd::contains(occupyableHexes, neighbours[1]);
+		attackAvailability[7] = vstd::contains(occupyableHexes, neighbours[3]) && vstd::contains(occupyableHexes, neighbours[4]);
+	}
+	else
+	{
+		for (size_t i = 0; i < 6; ++i)
+			attackAvailability[i] = vstd::contains(occupyableHexes, neighbours[i]);
+
+		attackAvailability[6] = false;
+		attackAvailability[7] = false;
+	}
+
+	// Zero available tiles to attack from
+	if ( vstd::find(attackAvailability, true) == attackAvailability.end())
+	{
+		logGlobal->error("Error: cannot find a hex to attack hex %d from!", myNumber);
+		return BattleHex::NONE;
+	}
+
+	// For each valid direction, select position to test against
+	std::array<Point, 8> testPoint;
+
+	for (size_t i = 0; i < 6; ++i)
+		if (attackAvailability[i])
+			testPoint[i] = hexPositionAbsolute(neighbours[i]).center();
+
+	// For bottom/top directions select central point, but move it a bit away from true center to reduce zones allocated to them
+	if (attackAvailability[6])
+		testPoint[6] = (hexPositionAbsolute(neighbours[0]).center() + hexPositionAbsolute(neighbours[1]).center()) / 2 + Point(0, -5);
+
+	if (attackAvailability[7])
+		testPoint[7] = (hexPositionAbsolute(neighbours[3]).center() + hexPositionAbsolute(neighbours[4]).center()) / 2 + Point(0,  5);
+
+	// Compute distance between tested position & cursor position and pick nearest
+	std::array<int, 8> distance2;
+
+	for (size_t i = 0; i < 8; ++i)
+		if (attackAvailability[i])
+			distance2[i] = (testPoint[i].y - cursorPos.y)*(testPoint[i].y - cursorPos.y) + (testPoint[i].x - cursorPos.x)*(testPoint[i].x - cursorPos.x);
+
+	size_t nearest = -1;
+	for (size_t i = 0; i < 8; ++i)
+		if (attackAvailability[i] && (nearest == -1 || distance2[i] < distance2[nearest]) )
+			nearest = i;
+
+	assert(nearest != -1);
+	return BattleHex::EDir(nearest);
+}
+
+BattleHex BattleFieldController::fromWhichHexAttack(BattleHex attackTarget)
+{
+	BattleHex::EDir direction = selectAttackDirection(attackTarget, CCS->curh->position());
+
+	const CStack * attacker = owner.stacksController->getActiveStack();
+
+	assert(direction != BattleHex::NONE);
+	assert(attacker);
+
+	if (!attacker->doubleWide())
+	{
+		assert(direction != BattleHex::BOTTOM);
+		assert(direction != BattleHex::TOP);
+		return attackTarget.cloneInDirection(direction);
+	}
+	else
+	{
+		// We need to find position of right hex of double-hex creature (or left for defending side)
+		// | TOP_LEFT |TOP_RIGHT |   RIGHT  |BOTTOM_RIGHT|BOTTOM_LEFT|  LEFT    |    TOP   |BOTTOM
+		// |  o o -   |   - o o  |    - -   |   - -      |    - -    |   - -    |    o o   |   - -
+		// |   - x -  |  - x -   |   - x o o|  - x -     |   - x -   |o o x -   |   - x -  |  - x -
+		// |    - -   |   - -    |    - -   |   - o o    |  o o -    |   - -    |    - -   |   o o
+
+		switch (direction)
+		{
+		case BattleHex::TOP_LEFT:
+		case BattleHex::LEFT:
+		case BattleHex::BOTTOM_LEFT:
+		{
+			if ( attacker->side == BattleSide::ATTACKER )
+				return attackTarget.cloneInDirection(direction);
+			else
+				return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::LEFT);
+		}
+
+		case BattleHex::TOP_RIGHT:
+		case BattleHex::RIGHT:
+		case BattleHex::BOTTOM_RIGHT:
+		{
+			if ( attacker->side == BattleSide::ATTACKER )
+				return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::RIGHT);
+			else
+				return attackTarget.cloneInDirection(direction);
+		}
+
+		case BattleHex::TOP:
+		{
+			if ( attacker->side == BattleSide::ATTACKER )
+				return attackTarget.cloneInDirection(BattleHex::TOP_RIGHT);
+			else
+				return attackTarget.cloneInDirection(BattleHex::TOP_LEFT);
+		}
+
+		case BattleHex::BOTTOM:
+		{
+			if ( attacker->side == BattleSide::ATTACKER )
+				return attackTarget.cloneInDirection(BattleHex::BOTTOM_RIGHT);
+			else
+				return attackTarget.cloneInDirection(BattleHex::BOTTOM_LEFT);
+		}
+		default:
+			assert(0);
+			return attackTarget.cloneInDirection(BattleHex::LEFT);
+		}
+	}
+}
+
+bool BattleFieldController::isTileAttackable(const BattleHex & number) const
+{
+	for (auto & elem : occupyableHexes)
+	{
+		if (BattleHex::mutualPosition(elem, number) != -1 || elem == number)
+			return true;
+	}
+	return false;
+}
+
+bool BattleFieldController::stackCountOutsideHex(const BattleHex & number) const
+{
+	return stackCountOutsideHexes[number];
+}
+
+void BattleFieldController::showAll(SDL_Surface * to)
+{
+	show(to);
+}
+
+void BattleFieldController::show(SDL_Surface * to)
+{
+	owner.stacksController->update();
+	owner.obstacleController->update();
+
+	Canvas canvas(to);
+
+	renderBattlefield(canvas);
+}

+ 101 - 0
client/battle/BattleFieldController.h

@@ -0,0 +1,101 @@
+/*
+ * BattleFieldController.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/BattleHex.h"
+#include "../gui/CIntObject.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CStack;
+
+VCMI_LIB_NAMESPACE_END
+
+struct Rect;
+struct Point;
+
+class ClickableHex;
+class BattleHero;
+class Canvas;
+class IImage;
+class BattleInterface;
+
+/// Handles battlefield grid as well as rendering of background layer of battle interface
+class BattleFieldController : public CIntObject
+{
+	BattleInterface & owner;
+
+	std::shared_ptr<IImage> background;
+	std::shared_ptr<IImage> cellBorder;
+	std::shared_ptr<IImage> cellShade;
+
+	/// Canvas that contains background, hex grid (if enabled), absolute obstacles and movement range of active stack
+	std::unique_ptr<Canvas> backgroundWithHexes;
+
+	/// Canvas that contains cell borders of all tiles in the battlefield
+	std::unique_ptr<Canvas> cellBorders;
+
+	/// hex from which the stack would perform attack with current cursor
+	BattleHex attackingHex;
+
+	/// hexes to which currently active stack can move
+	std::vector<BattleHex> occupyableHexes;
+
+	/// hexes that when in front of a unit cause it's amount box to move back
+	std::array<bool, GameConstants::BFIELD_SIZE> stackCountOutsideHexes;
+
+	std::vector<std::shared_ptr<ClickableHex>> bfield;
+
+	void showHighlightedHex(Canvas & to, BattleHex hex, bool darkBorder);
+
+	std::set<BattleHex> getHighlightedHexesStackRange();
+	std::set<BattleHex> getHighlightedHexesSpellRange();
+	std::set<BattleHex> getHighlightedHexesMovementTarget();
+
+	void showBackground(Canvas & canvas);
+	void showBackgroundImage(Canvas & canvas);
+	void showBackgroundImageWithHexes(Canvas & canvas);
+	void showHighlightedHexes(Canvas & canvas);
+
+	BattleHex::EDir selectAttackDirection(BattleHex myNumber, const Point & point);
+
+	void mouseMoved(const SDL_MouseMotionEvent &event) override;
+	void showAll(SDL_Surface * to) override;
+	void show(SDL_Surface * to) override;
+
+public:
+	BattleFieldController(BattleInterface & owner);
+
+	void createHeroes();
+
+	void redrawBackgroundWithHexes();
+	void renderBattlefield(Canvas & canvas);
+
+	/// Returns position of hex relative to owner (BattleInterface)
+	Rect hexPositionLocal(BattleHex hex) const;
+
+	/// Returns position of hex relative to game window
+	Rect hexPositionAbsolute(BattleHex hex) const;
+
+	/// Checks whether selected pixel is transparent, uses local coordinates of a hex
+	bool isPixelInHex(Point const & position);
+
+	/// Returns ID of currently hovered hex or BattleHex::INVALID if none
+	BattleHex getHoveredHex();
+
+	/// returns true if selected tile can be attacked in melee by current stack
+	bool isTileAttackable(const BattleHex & number) const;
+
+	/// returns true if stack should render its stack count image in default position - outside own hex
+	bool stackCountOutsideHex(const BattleHex & number) const;
+
+	void setBattleCursor(BattleHex myNumber);
+	BattleHex fromWhichHexAttack(BattleHex myNumber);
+};

+ 768 - 0
client/battle/BattleInterface.cpp

@@ -0,0 +1,768 @@
+/*
+ * BattleInterface.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 "BattleInterface.h"
+
+#include "BattleAnimationClasses.h"
+#include "BattleActionsController.h"
+#include "BattleInterfaceClasses.h"
+#include "CreatureAnimation.h"
+#include "BattleProjectileController.h"
+#include "BattleEffectsController.h"
+#include "BattleObstacleController.h"
+#include "BattleSiegeController.h"
+#include "BattleFieldController.h"
+#include "BattleWindow.h"
+#include "BattleStacksController.h"
+#include "BattleRenderer.h"
+
+#include "../CGameInfo.h"
+#include "../CMessage.h"
+#include "../CMusicHandler.h"
+#include "../CPlayerInterface.h"
+#include "../gui/Canvas.h"
+#include "../gui/CCursorHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../windows/CAdvmapInterface.h"
+
+#include "../../CCallback.h"
+#include "../../lib/CStack.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/CHeroHandler.h"
+#include "../../lib/CondSh.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/NetPacks.h"
+#include "../../lib/UnlockGuard.h"
+
+CondSh<BattleAction *> BattleInterface::givenCommand(nullptr);
+
+BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *army2,
+		const CGHeroInstance *hero1, const CGHeroInstance *hero2,
+		std::shared_ptr<CPlayerInterface> att,
+		std::shared_ptr<CPlayerInterface> defen,
+		std::shared_ptr<CPlayerInterface> spectatorInt)
+	: attackingHeroInstance(hero1)
+	, defendingHeroInstance(hero2)
+	, attackerInt(att)
+	, defenderInt(defen)
+	, curInt(att)
+	, myTurn(false)
+	, moveSoundHander(-1)
+{
+	for ( auto & event : animationEvents)
+		event.setn(false);
+
+	if(spectatorInt)
+	{
+		curInt = spectatorInt;
+	}
+	else if(!curInt)
+	{
+		//May happen when we are defending during network MP game -> attacker interface is just not present
+		curInt = defenderInt;
+	}
+
+	givenCommand.setn(nullptr);
+
+	//hot-seat -> check tactics for both players (defender may be local human)
+	if(attackerInt && attackerInt->cb->battleGetTacticDist())
+		tacticianInterface = attackerInt;
+	else if(defenderInt && defenderInt->cb->battleGetTacticDist())
+		tacticianInterface = defenderInt;
+
+	//if we found interface of player with tactics, then enter tactics mode
+	tacticsMode = static_cast<bool>(tacticianInterface);
+
+	//initializing armies
+	this->army1 = army1;
+	this->army2 = army2;
+
+	const CGTownInstance *town = curInt->cb->battleGetDefendedTown();
+	if(town && town->hasFort())
+		siegeController.reset(new BattleSiegeController(*this, town));
+
+	windowObject = std::make_shared<BattleWindow>(*this);
+	projectilesController.reset(new BattleProjectileController(*this));
+	stacksController.reset( new BattleStacksController(*this));
+	actionsController.reset( new BattleActionsController(*this));
+	effectsController.reset(new BattleEffectsController(*this));
+	obstacleController.reset(new BattleObstacleController(*this));
+
+	CCS->musich->stopMusic();
+	setAnimationCondition(EAnimationEvents::OPENING, true);
+	battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds);
+	auto onIntroPlayed = [this]()
+	{
+		if(LOCPLINT->battleInt)
+		{
+			boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
+			onIntroSoundPlayed();
+		}
+	};
+
+	GH.pushInt(windowObject);
+	windowObject->blockUI(true);
+	windowObject->updateQueue();
+
+	if (battleIntroSoundChannel != -1)
+		CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed);
+	else
+		onIntroSoundPlayed();
+}
+
+void BattleInterface::onIntroSoundPlayed()
+{
+	setAnimationCondition(EAnimationEvents::OPENING, false);
+	CCS->musich->playMusicFromSet("battle", true, true);
+	if(tacticsMode)
+		tacticNextStack(nullptr);
+	activateStack();
+	battleIntroSoundChannel = -1;
+}
+
+BattleInterface::~BattleInterface()
+{
+	CPlayerInterface::battleInt = nullptr;
+	givenCommand.cond.notify_all(); //that two lines should make any stacksController->getActiveStack() waiting thread to finish
+
+	if (adventureInt && adventureInt->selection)
+	{
+		//FIXME: this should be moved to adventureInt which should restore correct track based on selection/active player
+		const auto & terrain = *(LOCPLINT->cb->getTile(adventureInt->selection->visitablePos())->terType);
+		CCS->musich->playMusicFromSet("terrain", terrain.name, true, false);
+	}
+
+	// may happen if user decided to close game while in battle
+	if (getAnimationCondition(EAnimationEvents::ACTION) == true)
+		logGlobal->error("Shutting down BattleInterface during animation playback!");
+	setAnimationCondition(EAnimationEvents::ACTION, false);
+}
+
+void BattleInterface::setPrintCellBorders(bool set)
+{
+	Settings cellBorders = settings.write["battle"]["cellBorders"];
+	cellBorders->Bool() = set;
+
+	fieldController->redrawBackgroundWithHexes();
+	GH.totalRedraw();
+}
+
+void BattleInterface::setPrintStackRange(bool set)
+{
+	Settings stackRange = settings.write["battle"]["stackRange"];
+	stackRange->Bool() = set;
+
+	fieldController->redrawBackgroundWithHexes();
+	GH.totalRedraw();
+}
+
+void BattleInterface::setPrintMouseShadow(bool set)
+{
+	Settings shadow = settings.write["battle"]["mouseShadow"];
+	shadow->Bool() = set;
+}
+
+void BattleInterface::stackReset(const CStack * stack)
+{
+	stacksController->stackReset(stack);
+}
+
+void BattleInterface::stackAdded(const CStack * stack)
+{
+	stacksController->stackAdded(stack, false);
+}
+
+void BattleInterface::stackRemoved(uint32_t stackID)
+{
+	stacksController->stackRemoved(stackID);
+	fieldController->redrawBackgroundWithHexes();
+	windowObject->updateQueue();
+}
+
+void BattleInterface::stackActivated(const CStack *stack)
+{
+	stacksController->stackActivated(stack);
+}
+
+void BattleInterface::stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance, bool teleport)
+{
+	if (teleport)
+		stacksController->stackTeleported(stack, destHex, distance);
+	else
+		stacksController->stackMoved(stack, destHex, distance);
+}
+
+void BattleInterface::stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos)
+{
+	stacksController->stacksAreAttacked(attackedInfos);
+
+	std::array<int, 2> killedBySide = {0, 0};
+
+	int targets = 0;
+	for(const StackAttackedInfo & attackedInfo : attackedInfos)
+	{
+		++targets;
+
+		ui8 side = attackedInfo.defender->side;
+		killedBySide.at(side) += attackedInfo.amountKilled;
+	}
+
+	for(ui8 side = 0; side < 2; side++)
+	{
+		if(killedBySide.at(side) > killedBySide.at(1-side))
+			setHeroAnimation(side, EHeroAnimType::DEFEAT);
+		else if(killedBySide.at(side) < killedBySide.at(1-side))
+			setHeroAnimation(side, EHeroAnimType::VICTORY);
+	}
+}
+
+void BattleInterface::stackAttacking( const StackAttackInfo & attackInfo )
+{
+	stacksController->stackAttacking(attackInfo);
+}
+
+void BattleInterface::newRoundFirst( int round )
+{
+	waitForAnimationCondition(EAnimationEvents::OPENING, false);
+}
+
+void BattleInterface::newRound(int number)
+{
+	console->addText(CGI->generaltexth->allTexts[412]);
+}
+
+void BattleInterface::giveCommand(EActionType action, BattleHex tile, si32 additional)
+{
+	const CStack * actor = nullptr;
+	if(action != EActionType::HERO_SPELL && action != EActionType::RETREAT && action != EActionType::SURRENDER)
+	{
+		actor = stacksController->getActiveStack();
+	}
+
+	auto side = curInt->cb->playerToSide(curInt->playerID);
+	if(!side)
+	{
+		logGlobal->error("Player %s is not in battle", curInt->playerID.getStr());
+		return;
+	}
+
+	auto ba = new BattleAction(); //is deleted in CPlayerInterface::stacksController->getActiveStack()()
+	ba->side = side.get();
+	ba->actionType = action;
+	ba->aimToHex(tile);
+	ba->actionSubtype = additional;
+
+	sendCommand(ba, actor);
+}
+
+void BattleInterface::sendCommand(BattleAction *& command, const CStack * actor)
+{
+	command->stackNumber = actor ? actor->unitId() : ((command->side == BattleSide::ATTACKER) ? -1 : -2);
+
+	if(!tacticsMode)
+	{
+		logGlobal->trace("Setting command for %s", (actor ? actor->nodeName() : "hero"));
+		myTurn = false;
+		stacksController->setActiveStack(nullptr);
+		givenCommand.setn(command);
+	}
+	else
+	{
+		curInt->cb->battleMakeTacticAction(command);
+		vstd::clear_pointer(command);
+		stacksController->setActiveStack(nullptr);
+		//next stack will be activated when action ends
+	}
+}
+
+const CGHeroInstance * BattleInterface::getActiveHero()
+{
+	const CStack *attacker = stacksController->getActiveStack();
+	if(!attacker)
+	{
+		return nullptr;
+	}
+
+	if(attacker->side == BattleSide::ATTACKER)
+	{
+		return attackingHeroInstance;
+	}
+
+	return defendingHeroInstance;
+}
+
+void BattleInterface::stackIsCatapulting(const CatapultAttack & ca)
+{
+	if (siegeController)
+		siegeController->stackIsCatapulting(ca);
+}
+
+void BattleInterface::gateStateChanged(const EGateState state)
+{
+	if (siegeController)
+		siegeController->gateStateChanged(state);
+}
+
+void BattleInterface::battleFinished(const BattleResult& br)
+{
+	assert(getAnimationCondition(EAnimationEvents::ACTION) == false);
+	waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	stacksController->setActiveStack(nullptr);
+
+	CCS->curh->set(Cursor::Map::POINTER);
+	curInt->waitWhileDialog();
+
+	if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-skip-battle-result"].Bool())
+	{
+		windowObject->close();
+		return;
+	}
+
+	GH.pushInt(std::make_shared<BattleResultWindow>(br, *(this->curInt)));
+	curInt->waitWhileDialog(); // Avoid freeze when AI end turn after battle. Check bug #1897
+	CPlayerInterface::battleInt = nullptr;
+}
+
+void BattleInterface::spellCast(const BattleSpellCast * sc)
+{
+	windowObject->blockUI(true);
+
+	const SpellID spellID = sc->spellID;
+	const CSpell * spell = spellID.toSpell();
+	auto targetedTile = sc->tile;
+
+	assert(spell);
+	if(!spell)
+		return;
+
+	const std::string & castSoundPath = spell->getCastSound();
+
+	if (!castSoundPath.empty())
+	{
+		auto group = spell->animationInfo.projectile.empty() ?
+					EAnimationEvents::HIT:
+					EAnimationEvents::BEFORE_HIT;//FIXME: recheck whether this should be on projectile spawning
+
+		executeOnAnimationCondition(group, true, [=]() {
+			CCS->soundh->playSound(castSoundPath);
+		});
+	}
+
+	if ( sc->activeCast )
+	{
+		const CStack * casterStack = curInt->cb->battleGetStackByID(sc->casterStack);
+
+		if(casterStack != nullptr )
+		{
+			executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]()
+			{
+				stacksController->addNewAnim(new CastAnimation(*this, casterStack, targetedTile, curInt->cb->battleGetStackByPos(targetedTile), spell));
+				displaySpellCast(spell, casterStack->getPosition());
+			});
+		}
+		else
+		{
+			auto hero = sc->side ? defendingHero : attackingHero;
+			assert(hero);
+
+			executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]()
+			{
+				stacksController->addNewAnim(new HeroCastAnimation(*this, hero, targetedTile, curInt->cb->battleGetStackByPos(targetedTile), spell));
+			});
+		}
+	}
+
+	executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+		displaySpellHit(spell, targetedTile);
+	});
+
+	//queuing affect animation
+	for(auto & elem : sc->affectedCres)
+	{
+		auto stack = curInt->cb->battleGetStackByID(elem, false);
+		assert(stack);
+		if(stack)
+		{
+			executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+				displaySpellEffect(spell, stack->getPosition());
+			});
+		}
+	}
+
+	for(auto & elem : sc->reflectedCres)
+	{
+		auto stack = curInt->cb->battleGetStackByID(elem, false);
+		assert(stack);
+		executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+			effectsController->displayEffect(EBattleEffect::MAGIC_MIRROR, stack->getPosition());
+		});
+	}
+
+	if (!sc->resistedCres.empty())
+	{
+		executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+			CCS->soundh->playSound("MAGICRES");
+		});
+	}
+
+	for(auto & elem : sc->resistedCres)
+	{
+		auto stack = curInt->cb->battleGetStackByID(elem, false);
+		assert(stack);
+		executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+			effectsController->displayEffect(EBattleEffect::RESISTANCE, stack->getPosition());
+		});
+	}
+
+	//mana absorption
+	if (sc->manaGained > 0)
+	{
+		Point leftHero = Point(15, 30);
+		Point rightHero = Point(755, 30);
+		bool side = sc->side;
+
+		executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=](){
+			stacksController->addNewAnim(new EffectAnimation(*this, side ? "SP07_A.DEF" : "SP07_B.DEF", leftHero));
+			stacksController->addNewAnim(new EffectAnimation(*this, side ? "SP07_B.DEF" : "SP07_A.DEF", rightHero));
+		});
+	}
+}
+
+void BattleInterface::battleStacksEffectsSet(const SetStackEffect & sse)
+{
+	if(stacksController->getActiveStack() != nullptr)
+		fieldController->redrawBackgroundWithHexes();
+}
+
+void BattleInterface::setHeroAnimation(ui8 side, EHeroAnimType phase)
+{
+	if(side == BattleSide::ATTACKER)
+	{
+		if(attackingHero)
+			attackingHero->setPhase(phase);
+	}
+	else
+	{
+		if(defendingHero)
+			defendingHero->setPhase(phase);
+	}
+}
+
+void BattleInterface::displayBattleLog(const std::vector<MetaString> & battleLog)
+{
+	for(const auto & line : battleLog)
+	{
+		std::string formatted = line.toString();
+		boost::algorithm::trim(formatted);
+		appendBattleLog(formatted);
+	}
+}
+
+void BattleInterface::displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit)
+{
+	for(const CSpell::TAnimation & animation : q)
+	{
+		if(animation.pause > 0)
+			stacksController->addNewAnim(new DummyAnimation(*this, animation.pause));
+
+		if (!animation.effectName.empty())
+		{
+			const CStack * destStack = getCurrentPlayerInterface()->cb->battleGetStackByPos(destinationTile, false);
+
+			if (destStack)
+				stacksController->addNewAnim(new ColorTransformAnimation(*this, destStack, animation.effectName, spell ));
+		}
+
+		if(!animation.resourceName.empty())
+		{
+			int flags = 0;
+
+			if (isHit)
+				flags |= EffectAnimation::FORCE_ON_TOP;
+
+			if (animation.verticalPosition == VerticalPosition::BOTTOM)
+				flags |= EffectAnimation::ALIGN_TO_BOTTOM;
+
+			if (!destinationTile.isValid())
+				flags |= EffectAnimation::SCREEN_FILL;
+
+			if (!destinationTile.isValid())
+				stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, flags));
+			else
+				stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, destinationTile, flags));
+		}
+	}
+}
+
+void BattleInterface::displaySpellCast(const CSpell * spell, BattleHex destinationTile)
+{
+	if(spell)
+		displaySpellAnimationQueue(spell, spell->animationInfo.cast, destinationTile, false);
+}
+
+void BattleInterface::displaySpellEffect(const CSpell * spell, BattleHex destinationTile)
+{
+	if(spell)
+		displaySpellAnimationQueue(spell, spell->animationInfo.affect, destinationTile, false);
+}
+
+void BattleInterface::displaySpellHit(const CSpell * spell, BattleHex destinationTile)
+{
+	if(spell)
+		displaySpellAnimationQueue(spell, spell->animationInfo.hit, destinationTile, true);
+}
+
+void BattleInterface::setAnimSpeed(int set)
+{
+	Settings speed = settings.write["battle"]["animationSpeed"];
+	speed->Float() = float(set) / 100;
+}
+
+int BattleInterface::getAnimSpeed() const
+{
+	if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-battle-speed"].isNull())
+		return static_cast<int>(vstd::round(settings["session"]["spectate-battle-speed"].Float() *100));
+
+	return static_cast<int>(vstd::round(settings["battle"]["animationSpeed"].Float() *100));
+}
+
+CPlayerInterface *BattleInterface::getCurrentPlayerInterface() const
+{
+	return curInt.get();
+}
+
+void BattleInterface::trySetActivePlayer( PlayerColor player )
+{
+	if ( attackerInt && attackerInt->playerID == player )
+		curInt = attackerInt;
+
+	if ( defenderInt && defenderInt->playerID == player )
+		curInt = defenderInt;
+}
+
+void BattleInterface::activateStack()
+{
+	stacksController->activateStack();
+
+	const CStack * s = stacksController->getActiveStack();
+	if(!s)
+		return;
+
+	myTurn = true;
+	windowObject->updateQueue();
+	windowObject->blockUI(false);
+	fieldController->redrawBackgroundWithHexes();
+	actionsController->activateStack();
+	GH.fakeMouseMove();
+}
+
+void BattleInterface::endAction(const BattleAction* action)
+{
+	const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber);
+
+	stacksController->endAction(action);
+	windowObject->updateQueue();
+
+	//stack ended movement in tactics phase -> select the next one
+	if (tacticsMode)
+		tacticNextStack(stack);
+
+	//we have activated next stack after sending request that has been just realized -> blockmap due to movement has changed
+	if(action->actionType == EActionType::HERO_SPELL)
+		fieldController->redrawBackgroundWithHexes();
+}
+
+void BattleInterface::appendBattleLog(const std::string & newEntry)
+{
+	console->addText(newEntry);
+}
+
+void BattleInterface::startAction(const BattleAction* action)
+{
+	if(action->actionType == EActionType::END_TACTIC_PHASE)
+	{
+		windowObject->tacticPhaseEnded();
+		return;
+	}
+
+	const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber);
+
+	if (stack)
+	{
+		windowObject->updateQueue();
+	}
+	else
+	{
+		assert(action->actionType == EActionType::HERO_SPELL); //only cast spell is valid action without acting stack number
+	}
+
+	stacksController->startAction(action);
+
+	if(action->actionType == EActionType::HERO_SPELL) //when hero casts spell
+		return;
+
+	if (!stack)
+	{
+		logGlobal->error("Something wrong with stackNumber in actionStarted. Stack number: %d", action->stackNumber);
+		return;
+	}
+
+	effectsController->startAction(action);
+}
+
+void BattleInterface::tacticPhaseEnd()
+{
+	stacksController->setActiveStack(nullptr);
+	tacticsMode = false;
+}
+
+static bool immobile(const CStack *s)
+{
+	return !s->Speed(0, true); //should bound stacks be immobile?
+}
+
+void BattleInterface::tacticNextStack(const CStack * current)
+{
+	if (!current)
+		current = stacksController->getActiveStack();
+
+	//no switching stacks when the current one is moving
+	assert(getAnimationCondition(EAnimationEvents::ACTION) == false);
+	waitForAnimationCondition(EAnimationEvents::ACTION, false);
+
+	TStacks stacksOfMine = tacticianInterface->cb->battleGetStacks(CBattleCallback::ONLY_MINE);
+	vstd::erase_if (stacksOfMine, &immobile);
+	if (stacksOfMine.empty())
+	{
+		tacticPhaseEnd();
+		return;
+	}
+
+	auto it = vstd::find(stacksOfMine, current);
+	if (it != stacksOfMine.end() && ++it != stacksOfMine.end())
+		stackActivated(*it);
+	else
+		stackActivated(stacksOfMine.front());
+
+}
+
+void BattleInterface::obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> oi)
+{
+	obstacleController->obstaclePlaced(oi);
+}
+
+const CGHeroInstance *BattleInterface::currentHero() const
+{
+	if (attackingHeroInstance && attackingHeroInstance->tempOwner == curInt->playerID)
+		return attackingHeroInstance;
+
+	if (defendingHeroInstance && defendingHeroInstance->tempOwner == curInt->playerID)
+		return defendingHeroInstance;
+
+	return nullptr;
+}
+
+InfoAboutHero BattleInterface::enemyHero() const
+{
+	InfoAboutHero ret;
+	if (attackingHeroInstance->tempOwner == curInt->playerID)
+		curInt->cb->getHeroInfo(defendingHeroInstance, ret);
+	else
+		curInt->cb->getHeroInfo(attackingHeroInstance, ret);
+
+	return ret;
+}
+
+void BattleInterface::requestAutofightingAIToTakeAction()
+{
+	assert(curInt->isAutoFightOn);
+
+	boost::thread aiThread([&]()
+	{
+		auto ba = std::make_unique<BattleAction>(curInt->autofightingAI->activeStack(stacksController->getActiveStack()));
+
+		if(curInt->cb->battleIsFinished())
+		{
+			return; // battle finished with spellcast
+		}
+
+		if (curInt->isAutoFightOn)
+		{
+			if (tacticsMode)
+			{
+				// Always end tactics mode. Player interface is blocked currently, so it's not possible that
+				// the AI can take any action except end tactics phase (AI actions won't be triggered)
+				//TODO implement the possibility that the AI will be triggered for further actions
+				//TODO any solution to merge tactics phase & normal phase in the way it is handled by the player and battle interface?
+				stacksController->setActiveStack(nullptr);
+				tacticsMode = false;
+			}
+			else
+			{
+				givenCommand.setn(ba.release());
+			}
+		}
+		else
+		{
+			boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
+			activateStack();
+		}
+	});
+
+	aiThread.detach();
+}
+
+void BattleInterface::castThisSpell(SpellID spellID)
+{
+	actionsController->castThisSpell(spellID);
+}
+
+void BattleInterface::setAnimationCondition( EAnimationEvents event, bool state)
+{
+	logAnim->debug("setAnimationCondition: %d -> %s", static_cast<int>(event), state ? "ON" : "OFF");
+
+	size_t index = static_cast<size_t>(event);
+	animationEvents[index].setn(state);
+
+	decltype(awaitingEvents) executingEvents;
+
+	for (auto it = awaitingEvents.begin(); it != awaitingEvents.end();)
+	{
+		if (it->event == event && it->eventState == state)
+		{
+			executingEvents.push_back(*it);
+			it = awaitingEvents.erase(it);
+		}
+		else
+			++it;
+	}
+
+	for (auto const & event : executingEvents)
+		event.action();
+}
+
+bool BattleInterface::getAnimationCondition( EAnimationEvents event)
+{
+	size_t index = static_cast<size_t>(event);
+	return animationEvents[index].get();
+}
+
+void BattleInterface::waitForAnimationCondition( EAnimationEvents event, bool state)
+{
+	auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim);
+	size_t index = static_cast<size_t>(event);
+	animationEvents[index].waitUntil(state);
+}
+
+void BattleInterface::executeOnAnimationCondition( EAnimationEvents event, bool state, const AwaitingAnimationAction & action)
+{
+	awaitingEvents.push_back({action, event, state});
+}

+ 226 - 0
client/battle/BattleInterface.h

@@ -0,0 +1,226 @@
+/*
+ * BattleInterface.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "BattleConstants.h"
+#include "../gui/CIntObject.h"
+#include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation
+#include "../../lib/CondSh.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CCreatureSet;
+class CGHeroInstance;
+class CStack;
+struct BattleResult;
+struct BattleSpellCast;
+struct CObstacleInstance;
+struct SetStackEffect;
+class BattleAction;
+class CGTownInstance;
+struct CatapultAttack;
+struct BattleTriggerEffect;
+struct BattleHex;
+struct InfoAboutHero;
+
+VCMI_LIB_NAMESPACE_END
+
+class BattleHero;
+class Canvas;
+class BattleResultWindow;
+class StackQueue;
+class CPlayerInterface;
+class ClickableHex;
+class CAnimation;
+struct BattleEffect;
+class IImage;
+class StackQueue;
+
+class BattleProjectileController;
+class BattleSiegeController;
+class BattleObstacleController;
+class BattleFieldController;
+class BattleRenderer;
+class BattleWindow;
+class BattleStacksController;
+class BattleActionsController;
+class BattleEffectsController;
+class BattleConsole;
+
+/// Small struct which contains information about the id of the attacked stack, the damage dealt,...
+struct StackAttackedInfo
+{
+	const CStack *defender;
+	const CStack *attacker;
+
+	int64_t  damageDealt;
+	uint32_t amountKilled;
+	SpellID spellEffect;
+
+	bool indirectAttack; //if true, stack was attacked indirectly - spell or ranged attack
+	bool killed; //if true, stack has been killed
+	bool rebirth; //if true, play rebirth animation after all
+	bool cloneKilled;
+	bool fireShield;
+};
+
+struct StackAttackInfo
+{
+	const CStack *attacker;
+	const CStack *defender;
+	std::vector< const CStack *> secondaryDefender;
+
+	SpellID spellEffect;
+	BattleHex tile;
+
+	bool indirectAttack;
+	bool lucky;
+	bool unlucky;
+	bool deathBlow;
+	bool lifeDrain;
+};
+
+/// Main class for battles, responsible for relaying information from server to various battle entities
+class BattleInterface
+{
+	using AwaitingAnimationAction = std::function<void()>;
+
+	struct AwaitingAnimationEvents {
+		AwaitingAnimationAction action;
+		EAnimationEvents event;
+		bool eventState;
+	};
+
+	/// Conditional variables that are set depending on ongoing animations on the battlefield
+	std::array< CondSh<bool>, static_cast<size_t>(EAnimationEvents::COUNT)> animationEvents;
+
+	/// List of events that are waiting to be triggered
+	std::vector<AwaitingAnimationEvents> awaitingEvents;
+
+	/// used during tactics mode, points to the interface of player with higher tactics (can be either attacker or defender in hot-seat), valid onloy for human players
+	std::shared_ptr<CPlayerInterface> tacticianInterface;
+
+	/// attacker interface, not null if attacker is human in our vcmiclient
+	std::shared_ptr<CPlayerInterface> attackerInt;
+
+	/// defender interface, not null if attacker is human in our vcmiclient
+	std::shared_ptr<CPlayerInterface> defenderInt;
+
+	void onIntroSoundPlayed();
+public:
+	/// copy of initial armies (for result window)
+	const CCreatureSet *army1;
+	const CCreatureSet *army2;
+
+	/// ID of channel on which battle opening sound is playing, or -1 if none
+	int battleIntroSoundChannel;
+
+	std::shared_ptr<BattleWindow> windowObject;
+	std::shared_ptr<BattleConsole> console;
+
+	/// currently active player interface
+	std::shared_ptr<CPlayerInterface> curInt;
+
+	const CGHeroInstance *attackingHeroInstance;
+	const CGHeroInstance *defendingHeroInstance;
+
+	bool tacticsMode;
+
+	std::unique_ptr<BattleProjectileController> projectilesController;
+	std::unique_ptr<BattleSiegeController> siegeController;
+	std::unique_ptr<BattleObstacleController> obstacleController;
+	std::unique_ptr<BattleFieldController> fieldController;
+	std::unique_ptr<BattleStacksController> stacksController;
+	std::unique_ptr<BattleActionsController> actionsController;
+	std::unique_ptr<BattleEffectsController> effectsController;
+
+	std::shared_ptr<BattleHero> attackingHero;
+	std::shared_ptr<BattleHero> defendingHero;
+
+	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)
+	int moveSoundHander; // sound handler used when moving a unit
+
+	BattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen, std::shared_ptr<CPlayerInterface> spectatorInt = nullptr);
+	~BattleInterface();
+
+	void trySetActivePlayer( PlayerColor player ); // if in hotseat, will activate interface of chosen player
+	void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all
+	void requestAutofightingAIToTakeAction();
+
+	void giveCommand(EActionType action, BattleHex tile = BattleHex(), si32 additional = -1);
+	void sendCommand(BattleAction *& command, const CStack * actor = nullptr);
+
+	const CGHeroInstance *getActiveHero(); //returns hero that can currently cast a spell
+
+	void showInterface(SDL_Surface * to);
+
+	void setHeroAnimation(ui8 side, EHeroAnimType phase);
+
+	void executeSpellCast(); //called when a hero casts a spell
+
+	void appendBattleLog(const std::string & newEntry);
+
+	void setPrintCellBorders(bool set); //if true, cell borders will be printed
+	void setPrintStackRange(bool set); //if true,range of active stack will be printed
+	void setPrintMouseShadow(bool set); //if true, hex under mouse will be shaded
+	void setAnimSpeed(int set); //speed of animation; range 1..100
+	int getAnimSpeed() const; //speed of animation; range 1..100
+	CPlayerInterface *getCurrentPlayerInterface() const;
+
+	void tacticNextStack(const CStack *current);
+	void tacticPhaseEnd();
+
+	/// sets condition to targeted state and executes any awaiting actions
+	void setAnimationCondition( EAnimationEvents event, bool state);
+
+	/// returns current state of condition
+	bool getAnimationCondition( EAnimationEvents event);
+
+	/// locks execution until selected condition reached targeted state
+	void waitForAnimationCondition( EAnimationEvents event, bool state);
+
+	/// adds action that will be executed one selected condition reached targeted state
+	void executeOnAnimationCondition( EAnimationEvents event, bool state, const AwaitingAnimationAction & action);
+
+	//call-ins
+	void startAction(const BattleAction* action);
+	void stackReset(const CStack * stack);
+	void stackAdded(const CStack * stack); //new stack appeared on battlefield
+	void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled
+	void stackActivated(const CStack *stack); //active stack has been changed
+	void stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance, bool teleport); //stack with id number moved to destHex
+	void stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos); //called when a certain amount of stacks has been attacked
+	void stackAttacking(const StackAttackInfo & attackInfo); //called when stack with id ID is attacking something on hex dest
+	void newRoundFirst( int round );
+	void newRound(int number); //caled when round is ended; number is the number of round
+	void stackIsCatapulting(const CatapultAttack & ca); //called when a stack is attacking walls
+	void battleFinished(const BattleResult& br); //called when battle is finished - battleresult window should be printed
+	void spellCast(const BattleSpellCast *sc); //called when a hero casts a spell
+	void battleStacksEffectsSet(const SetStackEffect & sse); //called when a specific effect is set to stacks
+	void castThisSpell(SpellID spellID); //called when player has chosen a spell from spellbook
+
+	void displayBattleLog(const std::vector<MetaString> & battleLog);
+
+	void displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit);
+	void displaySpellCast(const CSpell * spell, BattleHex destinationTile); //displays spell`s cast animation
+	void displaySpellEffect(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation
+	void displaySpellHit(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation
+
+	void endAction(const BattleAction* action);
+
+	void obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> oi);
+
+	void gateStateChanged(const EGateState state);
+
+	const CGHeroInstance *currentHero() const;
+	InfoAboutHero enemyHero() const;
+};

+ 323 - 288
client/battle/CBattleInterfaceClasses.cpp → client/battle/BattleInterfaceClasses.cpp

@@ -1,5 +1,5 @@
 /*
- * CBattleInterfaceClasses.cpp, part of VCMI engine
+ * BattleInterfaceClasses.cpp, part of VCMI engine
  *
  * Authors: listed in file AUTHORS in main folder
  *
@@ -8,11 +8,16 @@
  *
  */
 #include "StdInc.h"
-#include "CBattleInterfaceClasses.h"
+#include "BattleInterfaceClasses.h"
 
-#include "CBattleInterface.h"
+#include "BattleInterface.h"
+#include "BattleActionsController.h"
+#include "BattleRenderer.h"
+#include "BattleSiegeController.h"
+#include "BattleFieldController.h"
+#include "BattleStacksController.h"
+#include "BattleWindow.h"
 
-#include "../CBitmapHandler.h"
 #include "../CGameInfo.h"
 #include "../CMessage.h"
 #include "../CMusicHandler.h"
@@ -20,10 +25,12 @@
 #include "../CVideoHandler.h"
 #include "../Graphics.h"
 #include "../gui/CAnimation.h"
+#include "../gui/Canvas.h"
 #include "../gui/CCursorHandler.h"
 #include "../gui/CGuiHandler.h"
-#include "../gui/SDL_Extensions.h"
+#include "../widgets/AdventureMapClasses.h"
 #include "../widgets/Buttons.h"
+#include "../widgets/Images.h"
 #include "../widgets/TextControls.h"
 #include "../windows/CCreatureWindow.h"
 #include "../windows/CSpellWindow.h"
@@ -35,249 +42,340 @@
 #include "../../lib/CGameState.h"
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/CTownHandler.h"
+#include "../../lib/CHeroHandler.h"
 #include "../../lib/NetPacks.h"
 #include "../../lib/StartInfo.h"
 #include "../../lib/CondSh.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 
-void CBattleConsole::showAll(SDL_Surface * to)
+void BattleConsole::showAll(SDL_Surface * to)
 {
-	Point textPos(pos.x + pos.w/2, pos.y + 17);
+	CIntObject::showAll(to);
 
-	if(ingcAlter.size())
-	{
-		graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(ingcAlter, pos.w, FONT_SMALL), Colors::WHITE, textPos);
-	}
-	else if(alterTxt.size())
+	Point line1 (pos.x + pos.w/2, pos.y +  8);
+	Point line2 (pos.x + pos.w/2, pos.y + 24);
+
+	auto visibleText = getVisibleText();
+
+	if(visibleText.size() > 0)
+		graphics->fonts[FONT_SMALL]->renderTextCenter(to, visibleText[0], Colors::WHITE, line1);
+
+	if(visibleText.size() > 1)
+		graphics->fonts[FONT_SMALL]->renderTextCenter(to, visibleText[1], Colors::WHITE, line2);
+}
+
+std::vector<std::string> BattleConsole::getVisibleText()
+{
+	// high priority texts that hide battle log entries
+	for (auto const & text : {consoleText, hoverText} )
 	{
-		graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(alterTxt, pos.w, FONT_SMALL), Colors::WHITE, textPos);
+		if (text.empty())
+			continue;
+
+		auto result = CMessage::breakText(text, pos.w, FONT_SMALL);
+
+		if(result.size() > 2)
+			result.resize(2);
+		return result;
 	}
-	else if(texts.size())
+
+	// log is small enough to fit entirely - display it as such
+	if (logEntries.size() < 3)
+		return logEntries;
+
+	return { logEntries[scrollPosition - 1], logEntries[scrollPosition] };
+}
+
+std::vector<std::string> BattleConsole::splitText(const std::string &text)
+{
+	std::vector<std::string> lines;
+	std::vector<std::string> output;
+
+	boost::split(lines, text, boost::is_any_of("\n"));
+
+	for (auto const & line : lines)
 	{
-		if(texts.size()==1)
+		if (graphics->fonts[FONT_SMALL]->getStringWidth(text) < pos.w)
 		{
-			graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(texts[0], pos.w, FONT_SMALL), Colors::WHITE, textPos);
+			output.push_back(line);
 		}
 		else
 		{
-			graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(texts[lastShown - 1], pos.w, FONT_SMALL), Colors::WHITE, textPos);
-			textPos.y += 16;
-			graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(texts[lastShown], pos.w, FONT_SMALL), Colors::WHITE, textPos);
+			std::vector<std::string> substrings = CMessage::breakText(line, pos.w, FONT_SMALL);
+			output.insert(output.end(), substrings.begin(), substrings.end());
 		}
 	}
+	return output;
 }
 
-bool CBattleConsole::addText(const std::string & text)
+bool BattleConsole::addText(const std::string & text)
 {
 	logGlobal->trace("CBattleConsole message: %s", text);
-	if(text.size()>70)
-		return false; //text too long!
-	int firstInToken = 0;
-	for(size_t i = 0; i < text.size(); ++i) //tokenize
-	{
-		if(text[i] == 10)
-		{
-			texts.push_back( text.substr(firstInToken, i-firstInToken) );
-			firstInToken = (int)i+1;
-		}
-	}
 
-	texts.push_back( text.substr(firstInToken, text.size()) );
-	lastShown = (int)texts.size()-1;
+	auto newLines = splitText(text);
+
+	logEntries.insert(logEntries.end(), newLines.begin(), newLines.end());
+	scrollPosition = (int)logEntries.size()-1;
+	redraw();
 	return true;
 }
+void BattleConsole::scrollUp(ui32 by)
+{
+	if(scrollPosition > static_cast<int>(by))
+		scrollPosition -= by;
+	redraw();
+}
 
-void CBattleConsole::alterText(const std::string &text)
+void BattleConsole::scrollDown(ui32 by)
 {
-	//char buf[500];
-	//sprintf(buf, text.c_str());
-	//alterTxt = buf;
-	alterTxt = text;
+	if(scrollPosition + by < logEntries.size())
+		scrollPosition += by;
+	redraw();
+}
+
+BattleConsole::BattleConsole(std::shared_ptr<CPicture> backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size)
+	: scrollPosition(-1)
+	, enteringText(false)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	pos += objectPos;
+	pos.w = size.x;
+	pos.h = size.y;
+
+	background = std::make_shared<CPicture>(backgroundSource->getSurface(), Rect(imagePos, size), 0, 0 );
 }
 
-void CBattleConsole::eraseText(ui32 pos)
+void BattleConsole::deactivate()
 {
-	if(pos < texts.size())
+	if (enteringText)
+		LOCPLINT->cingconsole->endEnteringText(false);
+
+	CIntObject::deactivate();
+}
+
+void BattleConsole::setEnteringMode(bool on)
+{
+	consoleText.clear();
+
+	if (on)
+	{
+		assert(enteringText == false);
+		CSDL_Ext::startTextInput(&pos);
+	}
+	else
 	{
-		texts.erase(texts.begin() + pos);
-		if(lastShown == texts.size())
-			--lastShown;
+		assert(enteringText == true);
+		CSDL_Ext::stopTextInput();
 	}
+	enteringText = on;
+	redraw();
 }
 
-void CBattleConsole::changeTextAt(const std::string & text, ui32 pos)
+void BattleConsole::setEnteredText(const std::string & text)
 {
-	if(pos >= texts.size()) //no such pos
-		return;
-	texts[pos] = text;
+	assert(enteringText == true);
+	consoleText = text;
+	redraw();
 }
 
-void CBattleConsole::scrollUp(ui32 by)
+void BattleConsole::write(const std::string & Text)
 {
-	if(lastShown > static_cast<int>(by))
-		lastShown -= by;
+	hoverText = Text;
+	redraw();
 }
 
-void CBattleConsole::scrollDown(ui32 by)
+void BattleConsole::clearIfMatching(const std::string & Text)
 {
-	if(lastShown + by < texts.size())
-		lastShown += by;
+	if (hoverText == Text)
+		clear();
 }
 
-CBattleConsole::CBattleConsole() : lastShown(-1), alterTxt(""), whoSetAlter(0)
-{}
+void BattleConsole::clear()
+{
+	write({});
+}
 
-void CBattleHero::show(SDL_Surface * to)
+const CGHeroInstance * BattleHero::instance()
 {
-	auto flagFrame = flagAnimation->getImage(flagAnim, 0, true);
+	return hero;
+}
 
-	if(!flagFrame)
-		return;
+void BattleHero::render(Canvas & canvas)
+{
+	size_t groupIndex = static_cast<size_t>(phase);
 
-	//animation of flag
-	SDL_Rect temp_rect;
-	if(flip)
-	{
-		temp_rect = genRect(
-			flagFrame->height(),
-			flagFrame->width(),
-			pos.x + 61,
-			pos.y + 39);
+	auto flagFrame = flagAnimation->getImage(flagCurrentFrame, 0, true);
+	auto heroFrame = animation->getImage(currentFrame, groupIndex, true);
 
-	}
+	Point heroPosition = pos.center() - parent->pos.topLeft() - heroFrame->dimensions() / 2;
+	Point flagPosition = pos.center() - parent->pos.topLeft() - flagFrame->dimensions() / 2;
+
+	if(defender)
+		flagPosition += Point(-4, -41);
 	else
+		flagPosition += Point(4, -41);
+
+	canvas.draw(flagFrame, flagPosition);
+	canvas.draw(heroFrame, heroPosition);
+
+	flagCurrentFrame += currentSpeed;
+	currentFrame += currentSpeed;
+
+	if(flagCurrentFrame >= flagAnimation->size(0))
+		flagCurrentFrame -= flagAnimation->size(0);
+
+	if(currentFrame >= animation->size(groupIndex))
 	{
-		temp_rect = genRect(
-			flagFrame->height(),
-			flagFrame->width(),
-			pos.x + 72,
-			pos.y + 39);
+		currentFrame -= animation->size(groupIndex);
+		switchToNextPhase();
 	}
+}
 
-	flagFrame->draw(screen, &temp_rect, nullptr); //FIXME: why screen?
+void BattleHero::pause()
+{
+	currentSpeed = 0.f;
+}
 
-	//animation of hero
-	SDL_Rect rect = pos;
+void BattleHero::play()
+{
+	//FIXME: un-hardcode speed
+	currentSpeed = 0.25f;
+}
 
-	auto heroFrame = animation->getImage(currentFrame, phase, true);
-	if(!heroFrame)
-		return;
+float BattleHero::getFrame() const
+{
+	return currentFrame;
+}
 
-	heroFrame->draw(to, &rect, nullptr);
+void BattleHero::collectRenderableObjects(BattleRenderer & renderer)
+{
+	auto hex = defender ? BattleHex(GameConstants::BFIELD_WIDTH-1) : BattleHex(0);
 
-	if(++animCount >= 4)
+	renderer.insert(EBattleFieldLayer::HEROES, hex, [this](BattleRenderer::RendererRef canvas)
 	{
-		animCount = 0;
-		if(++flagAnim >= flagAnimation->size(0))
-			flagAnim = 0;
+		render(canvas);
+	});
+}
 
-		if(++currentFrame >= lastFrame)
-			switchToNextPhase();
-	}
+void BattleHero::onPhaseFinished(const std::function<void()> & callback)
+{
+	phaseFinishedCallback = callback;
 }
 
-void CBattleHero::setPhase(int newPhase)
+void BattleHero::setPhase(EHeroAnimType newPhase)
 {
 	nextPhase = newPhase;
 	switchToNextPhase(); //immediately switch to next phase and then restore idling phase
-	nextPhase = 0;
+	nextPhase = EHeroAnimType::HOLDING;
 }
 
-void CBattleHero::hover(bool on)
+void BattleHero::hover(bool on)
 {
-	//TODO: Make lines below work properly
+	//TODO: BROKEN CODE
 	if (on)
-		CCS->curh->changeGraphic(ECursor::COMBAT, 5);
+		CCS->curh->set(Cursor::Combat::HERO);
 	else
-		CCS->curh->changeGraphic(ECursor::COMBAT, 0);
+		CCS->curh->set(Cursor::Combat::POINTER);
 }
 
-void CBattleHero::clickLeft(tribool down, bool previousState)
+void BattleHero::clickLeft(tribool down, bool previousState)
 {
-	if(myOwner->spellDestSelectMode) //we are casting a spell
+	if(owner.actionsController->spellcastingModeActive()) //we are casting a spell
 		return;
 
 	if(boost::logic::indeterminate(down))
 		return;
 
-	if(!myHero || down || !myOwner->myTurn)
+	if(!hero || down || !owner.myTurn)
 		return;
 
-	if(myOwner->getCurrentPlayerInterface()->cb->battleCanCastSpell(myHero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions
+	if(owner.getCurrentPlayerInterface()->cb->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions
 	{
-		for(int it=0; it<GameConstants::BFIELD_SIZE; ++it) //do nothing when any hex is hovered - hero's animation overlaps battlefield
-		{
-			if(myOwner->bfield[it]->hovered && myOwner->bfield[it]->strictHovered)
-				return;
-		}
-		CCS->curh->changeGraphic(ECursor::ADVENTURE, 0);
+		BattleHex hoveredHex = owner.fieldController->getHoveredHex();
+		//do nothing when any hex is hovered - hero's animation overlaps battlefield
+		if ( hoveredHex != BattleHex::INVALID )
+			return;
 
-		GH.pushIntT<CSpellWindow>(myHero, myOwner->getCurrentPlayerInterface());
+		CCS->curh->set(Cursor::Map::POINTER);
+
+		GH.pushIntT<CSpellWindow>(hero, owner.getCurrentPlayerInterface());
 	}
 }
 
-void CBattleHero::clickRight(tribool down, bool previousState)
+void BattleHero::clickRight(tribool down, bool previousState)
 {
 	if(boost::logic::indeterminate(down))
 		return;
 
 	Point windowPosition;
-	windowPosition.x = (!flip) ? myOwner->pos.topLeft().x + 1 : myOwner->pos.topRight().x - 79;
-	windowPosition.y = myOwner->pos.y + 135;
+	windowPosition.x = (!defender) ? owner.fieldController->pos.topLeft().x + 1 : owner.fieldController->pos.topRight().x - 79;
+	windowPosition.y = owner.fieldController->pos.y + 135;
 
 	InfoAboutHero targetHero;
-	if(down && (myOwner->myTurn || settings["session"]["spectate"].Bool()))
+	if(down && (owner.myTurn || settings["session"]["spectate"].Bool()))
 	{
-		auto h = flip ? myOwner->defendingHeroInstance : myOwner->attackingHeroInstance;
+		auto h = defender ? owner.defendingHeroInstance : owner.attackingHeroInstance;
 		targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE);
-		GH.pushIntT<CHeroInfoWindow>(targetHero, &windowPosition);
+		GH.pushIntT<HeroInfoWindow>(targetHero, &windowPosition);
 	}
 }
 
-void CBattleHero::switchToNextPhase()
+void BattleHero::switchToNextPhase()
 {
-	if(phase != nextPhase)
-	{
-		phase = nextPhase;
-
-		firstFrame = 0;
-
-		lastFrame = static_cast<int>(animation->size(phase));
-	}
+	phase = nextPhase;
+	currentFrame = 0.f;
 
-	currentFrame = firstFrame;
+	auto copy = phaseFinishedCallback;
+	phaseFinishedCallback.clear();
+	copy();
 }
 
-CBattleHero::CBattleHero(const std::string & animationPath, bool flipG, PlayerColor player, const CGHeroInstance * hero, const CBattleInterface * owner):
-    flip(flipG),
-    myHero(hero),
-    myOwner(owner),
-    phase(1),
-    nextPhase(0),
-    flagAnim(0),
-    animCount(0)
+BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender):
+	defender(defender),
+	hero(hero),
+	owner(owner),
+	phase(EHeroAnimType::HOLDING),
+	nextPhase(EHeroAnimType::HOLDING),
+	currentSpeed(0.f),
+	currentFrame(0.f),
+	flagCurrentFrame(0.f)
 {
+	std::string animationPath;
+
+	if(!hero->type->battleImage.empty())
+		animationPath = hero->type->battleImage;
+	else
+	if(hero->sex)
+		animationPath = hero->type->heroClass->imageBattleFemale;
+	else
+		animationPath = hero->type->heroClass->imageBattleMale;
+
 	animation = std::make_shared<CAnimation>(animationPath);
 	animation->preload();
-	if(flipG)
+
+	pos.w = 64;
+	pos.h = 136;
+	pos.x = owner.fieldController->pos.x + (defender ? (owner.fieldController->pos.w - pos.w) : 0);
+	pos.y = owner.fieldController->pos.y;
+
+	if(defender)
 		animation->verticalFlip();
 
-	if(flip)
+	if(defender)
 		flagAnimation = std::make_shared<CAnimation>("CMFLAGR");
 	else
 		flagAnimation = std::make_shared<CAnimation>("CMFLAGL");
 
 	flagAnimation->preload();
-	flagAnimation->playerColored(player);
+	flagAnimation->playerColored(hero->tempOwner);
 
 	addUsedEvents(LCLICK | RCLICK | HOVER);
 
 	switchToNextPhase();
+	play();
 }
 
-CBattleHero::~CBattleHero() = default;
-
-CHeroInfoWindow::CHeroInfoWindow(const InfoAboutHero & hero, Point * position)
+HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position)
 	: CWindowObject(RCLICK_POPUP | SHADOW_DISABLED, "CHRPOP")
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
@@ -297,49 +395,46 @@ CHeroInfoWindow::CHeroInfoWindow(const InfoAboutHero & hero, Point * position)
 	icons.push_back(std::make_shared<CAnimImage>("PortraitsLarge", hero.portrait, 0, 10, 6));
 
 	//primary stats
-	labels.push_back(std::make_shared<CLabel>(9, 75, EFonts::FONT_TINY, EAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[380] + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 87, EFonts::FONT_TINY, EAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[381] + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 99, EFonts::FONT_TINY, EAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[382] + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 111, EFonts::FONT_TINY, EAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[383] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 75, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[380] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 87, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[381] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 99, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[382] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 111, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[383] + ":"));
 
-	labels.push_back(std::make_shared<CLabel>(69, 87, EFonts::FONT_TINY, EAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(attack)));
-	labels.push_back(std::make_shared<CLabel>(69, 99, EFonts::FONT_TINY, EAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(defense)));
-	labels.push_back(std::make_shared<CLabel>(69, 111, EFonts::FONT_TINY, EAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(power)));
-	labels.push_back(std::make_shared<CLabel>(69, 123, EFonts::FONT_TINY, EAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(knowledge)));
+	labels.push_back(std::make_shared<CLabel>(69, 87, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(attack)));
+	labels.push_back(std::make_shared<CLabel>(69, 99, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(defense)));
+	labels.push_back(std::make_shared<CLabel>(69, 111, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(power)));
+	labels.push_back(std::make_shared<CLabel>(69, 123, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(knowledge)));
 
 	//morale+luck
-	labels.push_back(std::make_shared<CLabel>(9, 131, EFonts::FONT_TINY, EAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[384] + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 143, EFonts::FONT_TINY, EAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[385] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 131, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[384] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 143, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[385] + ":"));
 
 	icons.push_back(std::make_shared<CAnimImage>("IMRL22", morale + 3, 0, 47, 131));
 	icons.push_back(std::make_shared<CAnimImage>("ILCK22", luck + 3, 0, 47, 143));
 
 	//spell points
-	labels.push_back(std::make_shared<CLabel>(39, 174, EFonts::FONT_TINY, EAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[387]));
-	labels.push_back(std::make_shared<CLabel>(39, 186, EFonts::FONT_TINY, EAlignment::CENTER, Colors::WHITE, std::to_string(currentSpellPoints) + "/" + std::to_string(maxSpellPoints)));
+	labels.push_back(std::make_shared<CLabel>(39, 174, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[387]));
+	labels.push_back(std::make_shared<CLabel>(39, 186, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(currentSpellPoints) + "/" + std::to_string(maxSpellPoints)));
 }
 
-CBattleOptionsWindow::CBattleOptionsWindow(const SDL_Rect & position, CBattleInterface *owner)
+BattleOptionsWindow::BattleOptionsWindow(BattleInterface & owner):
+	CWindowObject(PLAYER_COLORED, "comopbck.bmp")
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-	pos = position;
 
-	background = std::make_shared<CPicture>("comopbck.bmp");
-	background->colorize(owner->getCurrentPlayerInterface()->playerID);
-
-	auto viewGrid = std::make_shared<CToggleButton>(Point(25, 56), "sysopchk.def", CGI->generaltexth->zelp[427], [=](bool on){owner->setPrintCellBorders(on);} );
+	auto viewGrid = std::make_shared<CToggleButton>(Point(25, 56), "sysopchk.def", CGI->generaltexth->zelp[427], [&](bool on){owner.setPrintCellBorders(on);} );
 	viewGrid->setSelected(settings["battle"]["cellBorders"].Bool());
 	toggles.push_back(viewGrid);
 
-	auto movementShadow = std::make_shared<CToggleButton>(Point(25, 89), "sysopchk.def", CGI->generaltexth->zelp[428], [=](bool on){owner->setPrintStackRange(on);});
+	auto movementShadow = std::make_shared<CToggleButton>(Point(25, 89), "sysopchk.def", CGI->generaltexth->zelp[428], [&](bool on){owner.setPrintStackRange(on);});
 	movementShadow->setSelected(settings["battle"]["stackRange"].Bool());
 	toggles.push_back(movementShadow);
 
-	auto mouseShadow = std::make_shared<CToggleButton>(Point(25, 122), "sysopchk.def", CGI->generaltexth->zelp[429], [=](bool on){owner->setPrintMouseShadow(on);});
+	auto mouseShadow = std::make_shared<CToggleButton>(Point(25, 122), "sysopchk.def", CGI->generaltexth->zelp[429], [&](bool on){owner.setPrintMouseShadow(on);});
 	mouseShadow->setSelected(settings["battle"]["mouseShadow"].Bool());
 	toggles.push_back(mouseShadow);
 
-	animSpeeds = std::make_shared<CToggleGroup>([=](int value){ owner->setAnimSpeed(value);});
+	animSpeeds = std::make_shared<CToggleGroup>([&](int value){ owner.setAnimSpeed(value);});
 
 	std::shared_ptr<CToggleButton> toggle;
 	toggle = std::make_shared<CToggleButton>(Point( 28, 225), "sysopb9.def", CGI->generaltexth->zelp[422]);
@@ -351,7 +446,7 @@ CBattleOptionsWindow::CBattleOptionsWindow(const SDL_Rect & position, CBattleInt
 	toggle = std::make_shared<CToggleButton>(Point(156, 225), "sysob11.def", CGI->generaltexth->zelp[424]);
 	animSpeeds->addToggle(100, toggle);
 
-	animSpeeds->setSelected(owner->getAnimSpeed());
+	animSpeeds->setSelected(owner.getAnimSpeed());
 
 	setToDefault = std::make_shared<CButton>(Point(246, 359), "codefaul.def", CGI->generaltexth->zelp[393], [&](){ bDefaultf(); });
 	setToDefault->setImageOrder(1, 0, 2, 3);
@@ -359,42 +454,42 @@ CBattleOptionsWindow::CBattleOptionsWindow(const SDL_Rect & position, CBattleInt
 	exit->setImageOrder(1, 0, 2, 3);
 
 	//creating labels
-	labels.push_back(std::make_shared<CLabel>(242,  32, FONT_BIG,    CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[392]));//window title
-	labels.push_back(std::make_shared<CLabel>(122, 214, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[393]));//animation speed
-	labels.push_back(std::make_shared<CLabel>(122, 293, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[394]));//music volume
-	labels.push_back(std::make_shared<CLabel>(122, 359, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[395]));//effects' volume
-	labels.push_back(std::make_shared<CLabel>(353,  66, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[396]));//auto - combat options
-	labels.push_back(std::make_shared<CLabel>(353, 265, FONT_MEDIUM, CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[397]));//creature info
+	labels.push_back(std::make_shared<CLabel>(242,  32, FONT_BIG,    ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[392]));//window title
+	labels.push_back(std::make_shared<CLabel>(122, 214, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[393]));//animation speed
+	labels.push_back(std::make_shared<CLabel>(122, 293, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[394]));//music volume
+	labels.push_back(std::make_shared<CLabel>(122, 359, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[395]));//effects' volume
+	labels.push_back(std::make_shared<CLabel>(353,  66, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[396]));//auto - combat options
+	labels.push_back(std::make_shared<CLabel>(353, 265, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[397]));//creature info
 
 	//auto - combat options
-	labels.push_back(std::make_shared<CLabel>(283,  86, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[398]));//creatures
-	labels.push_back(std::make_shared<CLabel>(283, 116, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[399]));//spells
-	labels.push_back(std::make_shared<CLabel>(283, 146, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[400]));//catapult
-	labels.push_back(std::make_shared<CLabel>(283, 176, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[151]));//ballista
-	labels.push_back(std::make_shared<CLabel>(283, 206, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[401]));//first aid tent
+	labels.push_back(std::make_shared<CLabel>(283,  86, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[398]));//creatures
+	labels.push_back(std::make_shared<CLabel>(283, 116, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[399]));//spells
+	labels.push_back(std::make_shared<CLabel>(283, 146, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[400]));//catapult
+	labels.push_back(std::make_shared<CLabel>(283, 176, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[151]));//ballista
+	labels.push_back(std::make_shared<CLabel>(283, 206, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[401]));//first aid tent
 
 	//creature info
-	labels.push_back(std::make_shared<CLabel>(283, 285, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[402]));//all stats
-	labels.push_back(std::make_shared<CLabel>(283, 315, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[403]));//spells only
+	labels.push_back(std::make_shared<CLabel>(283, 285, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[402]));//all stats
+	labels.push_back(std::make_shared<CLabel>(283, 315, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[403]));//spells only
 
 	//general options
-	labels.push_back(std::make_shared<CLabel>(61,  57, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[404]));
-	labels.push_back(std::make_shared<CLabel>(61,  90, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[405]));
-	labels.push_back(std::make_shared<CLabel>(61, 123, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[406]));
-	labels.push_back(std::make_shared<CLabel>(61, 156, FONT_MEDIUM, TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[407]));
+	labels.push_back(std::make_shared<CLabel>(61,  57, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[404]));
+	labels.push_back(std::make_shared<CLabel>(61,  90, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[405]));
+	labels.push_back(std::make_shared<CLabel>(61, 123, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[406]));
+	labels.push_back(std::make_shared<CLabel>(61, 156, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[407]));
 }
 
-void CBattleOptionsWindow::bDefaultf()
+void BattleOptionsWindow::bDefaultf()
 {
 	//TODO: implement
 }
 
-void CBattleOptionsWindow::bExitf()
+void BattleOptionsWindow::bExitf()
 {
 	close();
 }
 
-CBattleResultWindow::CBattleResultWindow(const BattleResult & br, CPlayerInterface & _owner)
+BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner)
 	: owner(_owner)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
@@ -408,25 +503,25 @@ CBattleResultWindow::CBattleResultWindow(const BattleResult & br, CPlayerInterfa
 
 	if(br.winner == 0) //attacker won
 	{
-		labels.push_back(std::make_shared<CLabel>(59, 124, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410]));
+		labels.push_back(std::make_shared<CLabel>(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410]));
 	}
 	else
 	{
-		labels.push_back(std::make_shared<CLabel>(59, 124, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411]));
+		labels.push_back(std::make_shared<CLabel>(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411]));
 	}
 
 	if(br.winner == 1)
 	{
-		labels.push_back(std::make_shared<CLabel>(412, 124, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410]));
+		labels.push_back(std::make_shared<CLabel>(412, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410]));
 	}
 	else
 	{
-		labels.push_back(std::make_shared<CLabel>(408, 124, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411]));
+		labels.push_back(std::make_shared<CLabel>(408, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411]));
 	}
 
-	labels.push_back(std::make_shared<CLabel>(232, 302, FONT_BIG, CENTER, Colors::YELLOW,  CGI->generaltexth->allTexts[407]));
-	labels.push_back(std::make_shared<CLabel>(232, 332, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[408]));
-	labels.push_back(std::make_shared<CLabel>(232, 428, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[409]));
+	labels.push_back(std::make_shared<CLabel>(232, 302, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW,  CGI->generaltexth->allTexts[407]));
+	labels.push_back(std::make_shared<CLabel>(232, 332, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[408]));
+	labels.push_back(std::make_shared<CLabel>(232, 428, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[409]));
 
 	std::string sideNames[2] = {"N/A", "N/A"};
 
@@ -462,15 +557,15 @@ CBattleResultWindow::CBattleResultWindow(const BattleResult & br, CPlayerInterfa
 	}
 
 	//printing attacker and defender's names
-	labels.push_back(std::make_shared<CLabel>(89, 37, FONT_SMALL, TOPLEFT, Colors::WHITE, sideNames[0]));
-	labels.push_back(std::make_shared<CLabel>(381, 53, FONT_SMALL, BOTTOMRIGHT, Colors::WHITE, sideNames[1]));
+	labels.push_back(std::make_shared<CLabel>(89, 37, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, sideNames[0]));
+	labels.push_back(std::make_shared<CLabel>(381, 53, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, sideNames[1]));
 
 	//printing casualties
 	for(int step = 0; step < 2; ++step)
 	{
 		if(br.casualties[step].size()==0)
 		{
-			labels.push_back(std::make_shared<CLabel>(235, 360 + 97 * step, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[523]));
+			labels.push_back(std::make_shared<CLabel>(235, 360 + 97 * step, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[523]));
 		}
 		else
 		{
@@ -485,7 +580,7 @@ CBattleResultWindow::CBattleResultWindow(const BattleResult & br, CPlayerInterfa
 				icons.push_back(std::make_shared<CAnimImage>("CPRSMALL", creature->getIconIndex(), 0, xPos, yPos));
 				std::ostringstream amount;
 				amount<<elem.second;
-				labels.push_back(std::make_shared<CLabel>(xPos + 16, yPos + 42, FONT_SMALL, CENTER, Colors::WHITE, amount.str()));
+				labels.push_back(std::make_shared<CLabel>(xPos + 16, yPos + 42, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, amount.str()));
 				xPos += 42;
 			}
 		}
@@ -522,7 +617,7 @@ CBattleResultWindow::CBattleResultWindow(const BattleResult & br, CPlayerInterfa
 			boost::algorithm::replace_first(str, "%d", boost::lexical_cast<std::string>(br.exp[weAreAttacker ? 0 : 1]));
 		}
 
-		description = std::make_shared<CTextBox>(str, Rect(69, 203, 330, 68), 0, FONT_SMALL, CENTER, Colors::WHITE);
+		description = std::make_shared<CTextBox>(str, Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
 	}
 	else // we lose
 	{
@@ -550,31 +645,29 @@ CBattleResultWindow::CBattleResultWindow(const BattleResult & br, CPlayerInterfa
 		CCS->musich->playMusic(musicName, false, true);
 		CCS->videoh->open(videoName);
 
-		labels.push_back(std::make_shared<CLabel>(235, 235, FONT_SMALL, CENTER, Colors::WHITE, CGI->generaltexth->allTexts[text]));
+		labels.push_back(std::make_shared<CLabel>(235, 235, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[text]));
 	}
 }
 
-CBattleResultWindow::~CBattleResultWindow() = default;
-
-void CBattleResultWindow::activate()
+void BattleResultWindow::activate()
 {
 	owner.showingDialog->set(true);
 	CIntObject::activate();
 }
 
-void CBattleResultWindow::show(SDL_Surface * to)
+void BattleResultWindow::show(SDL_Surface * to)
 {
 	CIntObject::show(to);
 	CCS->videoh->update(pos.x + 107, pos.y + 70, screen, true, false);
 }
 
-void CBattleResultWindow::bExitf()
+void BattleResultWindow::bExitf()
 {
 	CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon
 
 	close();
 
-	if(dynamic_cast<CBattleInterface*>(GH.topInt().get()))
+	if(dynamic_cast<BattleWindow*>(GH.topInt().get()))
 		GH.popInts(1); //pop battle interface if present
 
 	//Result window and battle interface are gone. We requested all dialogs to be closed before opening the battle,
@@ -583,121 +676,56 @@ void CBattleResultWindow::bExitf()
 	CCS->videoh->close();
 }
 
-Point CClickableHex::getXYUnitAnim(BattleHex hexNum, const CStack * stack, CBattleInterface * cbi)
-{
-	assert(cbi);
-
-	Point ret(-500, -500); //returned value
-	if(stack && stack->initialPosition < 0) //creatures in turrets
-	{
-		switch(stack->initialPosition)
-		{
-		case -2: //keep
-			ret = cbi->siegeH->town->town->clientInfo.siegePositions[18];
-			break;
-		case -3: //lower turret
-			ret = cbi->siegeH->town->town->clientInfo.siegePositions[19];
-			break;
-		case -4: //upper turret
-			ret = cbi->siegeH->town->town->clientInfo.siegePositions[20];
-			break;
-		}
-	}
-	else
-	{
-		static const Point basePos(-190, -139); // position of creature in topleft corner
-		static const int imageShiftX = 30; // X offset to base pos for facing right stacks, negative for facing left
-
-		ret.x = basePos.x + 22 * ( (hexNum.getY() + 1)%2 ) + 44 * hexNum.getX();
-		ret.y = basePos.y + 42 * hexNum.getY();
-
-		if (stack)
-		{
-			if(cbi->creDir[stack->ID])
-				ret.x += imageShiftX;
-			else
-				ret.x -= imageShiftX;
-
-			//shifting position for double - hex creatures
-			if(stack->doubleWide())
-			{
-				if(stack->side == BattleSide::ATTACKER)
-				{
-					if(cbi->creDir[stack->ID])
-						ret.x -= 44;
-				}
-				else
-				{
-					if(!cbi->creDir[stack->ID])
-						ret.x += 44;
-				}
-			}
-		}
-	}
-	//returning
-	return ret + CPlayerInterface::battleInt->pos;
-}
-
-void CClickableHex::hover(bool on)
+void ClickableHex::hover(bool on)
 {
 	hovered = on;
 	//Hoverable::hover(on);
 	if(!on && setAlterText)
 	{
-		myInterface->console->alterTxt = std::string();
+		GH.statusbar->clear();
 		setAlterText = false;
 	}
 }
 
-CClickableHex::CClickableHex() : setAlterText(false), myNumber(-1), accessible(true), strictHovered(false), myInterface(nullptr)
+ClickableHex::ClickableHex() : setAlterText(false), myNumber(-1), strictHovered(false), myInterface(nullptr)
 {
 	addUsedEvents(LCLICK | RCLICK | HOVER | MOVE);
 }
 
-void CClickableHex::mouseMoved(const SDL_MouseMotionEvent &sEvent)
+void ClickableHex::mouseMoved(const SDL_MouseMotionEvent &sEvent)
 {
-	if(myInterface->cellShade)
-	{
-		if(CSDL_Ext::SDL_GetPixel(myInterface->cellShade, sEvent.x-pos.x, sEvent.y-pos.y) == 0) //hovered pixel is outside hex
-		{
-			strictHovered = false;
-		}
-		else //hovered pixel is inside hex
-		{
-			strictHovered = true;
-		}
-	}
+	strictHovered = myInterface->fieldController->isPixelInHex(Point(sEvent.x-pos.x, sEvent.y-pos.y));
 
 	if(hovered && strictHovered) //print attacked creature to console
 	{
 		const CStack * attackedStack = myInterface->getCurrentPlayerInterface()->cb->battleGetStackByPos(myNumber);
-		if(myInterface->console->alterTxt.size() == 0 &&attackedStack != nullptr &&
+		if( attackedStack != nullptr &&
 			attackedStack->owner != myInterface->getCurrentPlayerInterface()->playerID &&
 			attackedStack->alive())
 		{
 			MetaString text;
 			text.addTxt(MetaString::GENERAL_TXT, 220);
 			attackedStack->addNameReplacement(text);
-			myInterface->console->alterTxt = text.toString();
+			GH.statusbar->write(text.toString());
 			setAlterText = true;
 		}
 	}
 	else if(setAlterText)
 	{
-		myInterface->console->alterTxt = std::string();
+		GH.statusbar->clear();
 		setAlterText = false;
 	}
 }
 
-void CClickableHex::clickLeft(tribool down, bool previousState)
+void ClickableHex::clickLeft(tribool down, bool previousState)
 {
 	if(!down && hovered && strictHovered) //we've been really clicked!
 	{
-		myInterface->hexLclicked(myNumber);
+		myInterface->actionsController->handleHex(myNumber, LCLICK);
 	}
 }
 
-void CClickableHex::clickRight(tribool down, bool previousState)
+void ClickableHex::clickRight(tribool down, bool previousState)
 {
 	const CStack * myst = myInterface->getCurrentPlayerInterface()->cb->battleGetStackByPos(myNumber); //stack info
 	if(hovered && strictHovered && myst!=nullptr)
@@ -710,17 +738,17 @@ void CClickableHex::clickRight(tribool down, bool previousState)
 	}
 }
 
-CStackQueue::CStackQueue(bool Embedded, CBattleInterface * _owner)
+StackQueue::StackQueue(bool Embedded, BattleInterface & owner)
 	: embedded(Embedded),
-	owner(_owner)
+	owner(owner)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 	if(embedded)
 	{
-		pos.w = QUEUE_SIZE * 37;
-		pos.h = 46;
-		pos.x = screen->w/2 - pos.w/2;
-		pos.y = (screen->h - 600)/2 + 10;
+		pos.w = QUEUE_SIZE * 41;
+		pos.h = 49;
+		pos.x += parent->pos.w/2 - pos.w/2;
+		pos.y += 10;
 
 		icons = std::make_shared<CAnimation>("CPRSMALL");
 		stateIcons = std::make_shared<CAnimation>("VCMI/BATTLEQUEUE/STATESSMALL");
@@ -729,6 +757,8 @@ CStackQueue::CStackQueue(bool Embedded, CBattleInterface * _owner)
 	{
 		pos.w = 800;
 		pos.h = 85;
+		pos.x += 0;
+		pos.y -= pos.h;
 
 		background = std::make_shared<CFilledTexture>("DIBOXBCK", Rect(0, 0, pos.w, pos.h));
 
@@ -743,17 +773,22 @@ CStackQueue::CStackQueue(bool Embedded, CBattleInterface * _owner)
 	for (int i = 0; i < stackBoxes.size(); i++)
 	{
 		stackBoxes[i] = std::make_shared<StackBox>(this);
-		stackBoxes[i]->moveBy(Point(1 + (embedded ? 36 : 80) * i, 0));
+		stackBoxes[i]->moveBy(Point(1 + (embedded ? 41 : 80) * i, 0));
 	}
 }
 
-CStackQueue::~CStackQueue() = default;
+void StackQueue::show(SDL_Surface * to)
+{
+	if (embedded)
+		showAll(to);
+	CIntObject::show(to);
+}
 
-void CStackQueue::update()
+void StackQueue::update()
 {
 	std::vector<battle::Units> queueData;
 
-	owner->getCurrentPlayerInterface()->cb->battleGetTurnOrder(queueData, stackBoxes.size(), 0);
+	owner.getCurrentPlayerInterface()->cb->battleGetTurnOrder(queueData, stackBoxes.size(), 0);
 
 	size_t boxIndex = 0;
 
@@ -767,12 +802,12 @@ void CStackQueue::update()
 		stackBoxes[boxIndex]->setUnit(nullptr);
 }
 
-int32_t CStackQueue::getSiegeShooterIconID()
+int32_t StackQueue::getSiegeShooterIconID()
 {
-	return owner->siegeH->town->town->faction->index;
+	return owner.siegeController->getSiegedTown()->town->faction->index;
 }
 
-CStackQueue::StackBox::StackBox(CStackQueue * owner):
+StackQueue::StackBox::StackBox(StackQueue * owner):
 	owner(owner)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
@@ -784,12 +819,12 @@ CStackQueue::StackBox::StackBox(CStackQueue * owner):
 	if(owner->embedded)
 	{
 		icon = std::make_shared<CAnimImage>(owner->icons, 0, 0, 5, 2);
-		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 7, FONT_SMALL, CENTER, Colors::WHITE);
+		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 7, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
 	}
 	else
 	{
 		icon = std::make_shared<CAnimImage>(owner->icons, 0, 0, 9, 1);
-		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 8, FONT_MEDIUM, CENTER, Colors::WHITE);
+		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 8, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE);
 
 		int icon_x = pos.w - 17;
 		int icon_y = pos.h - 18;
@@ -799,7 +834,7 @@ CStackQueue::StackBox::StackBox(CStackQueue * owner):
 	}
 }
 
-void CStackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn)
+void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn)
 {
 	if(unit)
 	{

+ 87 - 48
client/battle/CBattleInterfaceClasses.h → client/battle/BattleInterfaceClasses.h

@@ -1,5 +1,5 @@
 /*
- * CBattleInterfaceClasses.h, part of VCMI engine
+ * BattleInterfaceClasses.h, part of VCMI engine
  *
  * Authors: listed in file AUTHORS in main folder
  *
@@ -9,7 +9,9 @@
  */
 #pragma once
 
+#include "BattleConstants.h"
 #include "../gui/CIntObject.h"
+#include "../../lib/FunctionList.h"
 #include "../../lib/battle/BattleHex.h"
 #include "../windows/CWindowObject.h"
 
@@ -26,8 +28,9 @@ class Unit;
 
 VCMI_LIB_NAMESPACE_END
 
+class Canvas;
 struct SDL_Surface;
-class CBattleInterface;
+class BattleInterface;
 class CPicture;
 class CFilledTexture;
 class CButton;
@@ -37,82 +40,121 @@ class CLabel;
 class CTextBox;
 class CAnimImage;
 class CPlayerInterface;
+class BattleRenderer;
 
 /// Class which shows the console at the bottom of the battle screen and manages the text of the console
-class CBattleConsole : public CIntObject
+class BattleConsole : public CIntObject, public IStatusBar
 {
 private:
-	std::vector< std::string > texts; //a place where texts are stored
-	int lastShown; //last shown line of text
+	std::shared_ptr<CPicture> background;
+
+	/// List of all texts added during battle, essentially - log of entire battle
+	std::vector< std::string > logEntries;
+
+	/// Current scrolling position, to allow showing older entries via scroll buttons
+	int scrollPosition;
+
+	/// current hover text set on mouse move, takes priority over log entries
+	std::string hoverText;
+
+	/// current text entered via in-game console, takes priority over both log entries and hover text
+	std::string consoleText;
+
+	/// if true then we are currently entering console text
+	bool enteringText;
+
+	/// splits text into individual strings for battle log
+	std::vector<std::string> splitText(const std::string &text);
+
+	/// select line(s) that will be visible in UI
+	std::vector<std::string> getVisibleText();
 public:
-	std::string alterTxt; //if it's not empty, this text is displayed
-	std::string ingcAlter; //alternative text set by in-game console - very important!
-	int whoSetAlter; //who set alter text; 0 - battle interface or none, 1 - button
-	CBattleConsole();
-	void showAll(SDL_Surface * to = 0) override;
+	BattleConsole(std::shared_ptr<CPicture> backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size);
+
+	void showAll(SDL_Surface * to) override;
+	void deactivate() override;
+
 	bool addText(const std::string &text); //adds text at the last position; returns false if failed (e.g. text longer than 70 characters)
-	void alterText(const std::string &text); //place string at alterTxt
-	void eraseText(ui32 pos); //erases added text at position pos
-	void changeTextAt(const std::string &text, ui32 pos); //if we have more than pos texts, pos-th is changed to given one
 	void scrollUp(ui32 by = 1); //scrolls console up by 'by' positions
 	void scrollDown(ui32 by = 1); //scrolls console up by 'by' positions
+
+	// IStatusBar interface
+	void write(const std::string & Text) override;
+	void clearIfMatching(const std::string & Text) override;
+	void clear() override;
+	void setEnteringMode(bool on) override;
+	void setEnteredText(const std::string & text) override;
 };
 
 /// Hero battle animation
-class CBattleHero : public CIntObject
+class BattleHero : public CIntObject
 {
-	void switchToNextPhase();
-public:
-	bool flip; //false if it's attacking hero, true otherwise
+	bool defender;
+
+	CFunctionList<void()> phaseFinishedCallback;
 
 	std::shared_ptr<CAnimation> animation;
 	std::shared_ptr<CAnimation> flagAnimation;
 
-	const CGHeroInstance * myHero; //this animation's hero instance
-	const CBattleInterface * myOwner; //battle interface to which this animation is assigned
-	int phase; //stage of animation
-	int nextPhase; //stage of animation to be set after current phase is fully displayed
-	int currentFrame, firstFrame, lastFrame; //frame of animation
+	const CGHeroInstance * hero; //this animation's hero instance
+	const BattleInterface & owner; //battle interface to which this animation is assigned
+
+	EHeroAnimType phase; //stage of animation
+	EHeroAnimType nextPhase; //stage of animation to be set after current phase is fully displayed
+
+	float currentSpeed;
+	float currentFrame; //frame of animation
+	float flagCurrentFrame;
+
+	void switchToNextPhase();
+
+	void render(Canvas & canvas); //prints next frame of animation to to
+public:
+	const CGHeroInstance * instance();
+
+	void setPhase(EHeroAnimType newPhase); //sets phase of hero animation
+
+	void collectRenderableObjects(BattleRenderer & renderer);
+
+	float getFrame() const;
+	void onPhaseFinished(const std::function<void()> &);
+
+	void pause();
+	void play();
 
-	size_t flagAnim;
-	ui8 animCount; //for flag animation
-	void show(SDL_Surface * to) override; //prints next frame of animation to to
-	void setPhase(int newPhase); //sets phase of hero animation
 	void hover(bool on) override;
 	void clickLeft(tribool down, bool previousState) override; //call-in
 	void clickRight(tribool down, bool previousState) override; //call-in
-	CBattleHero(const std::string & animationPath, bool filpG, PlayerColor player, const CGHeroInstance * hero, const CBattleInterface * owner);
-	~CBattleHero();
+	BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender);
 };
 
-class CHeroInfoWindow : public CWindowObject
+class HeroInfoWindow : public CWindowObject
 {
 private:
 	std::vector<std::shared_ptr<CLabel>> labels;
 	std::vector<std::shared_ptr<CAnimImage>> icons;
 public:
-	CHeroInfoWindow(const InfoAboutHero & hero, Point * position);
+	HeroInfoWindow(const InfoAboutHero & hero, Point * position);
 };
 
 /// Class which manages the battle options window
-class CBattleOptionsWindow : public WindowBase
+class BattleOptionsWindow : public CWindowObject
 {
 private:
-	std::shared_ptr<CPicture> background;
 	std::shared_ptr<CButton> setToDefault;
 	std::shared_ptr<CButton> exit;
 	std::shared_ptr<CToggleGroup> animSpeeds;
 	std::vector<std::shared_ptr<CLabel>> labels;
 	std::vector<std::shared_ptr<CToggleButton>> toggles;
 public:
-	CBattleOptionsWindow(const SDL_Rect & position, CBattleInterface * owner);
+	BattleOptionsWindow(BattleInterface & owner);
 
 	void bDefaultf(); //default button callback
 	void bExitf(); //exit button callback
 };
 
 /// Class which is responsible for showing the battle result window
-class CBattleResultWindow : public WindowBase
+class BattleResultWindow : public WindowBase
 {
 private:
 	std::shared_ptr<CPicture> background;
@@ -122,8 +164,7 @@ private:
 	std::shared_ptr<CTextBox> description;
 	CPlayerInterface & owner;
 public:
-	CBattleResultWindow(const BattleResult & br, CPlayerInterface & _owner);
-	~CBattleResultWindow();
+	BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner);
 
 	void bExitf(); //exit button callback
 
@@ -132,32 +173,29 @@ public:
 };
 
 /// Class which stands for a single hex field on a battlefield
-class CClickableHex : public CIntObject
+class ClickableHex : public CIntObject
 {
 private:
 	bool setAlterText; //if true, this hex has set alternative text in console and will clean it
 public:
 	ui32 myNumber; //number of hex in commonly used format
-	bool accessible; //if true, this hex is accessible for units
-	//CStack * ourStack;
 	bool strictHovered; //for determining if hex is hovered by mouse (this is different problem than hex's graphic hovering)
-	CBattleInterface * myInterface; //interface that owns me
-	static Point getXYUnitAnim(BattleHex hexNum, const CStack * creature, CBattleInterface * cbi); //returns (x, y) of left top corner of animation
+	BattleInterface * myInterface; //interface that owns me
 
 	//for user interactions
 	void hover (bool on) override;
 	void mouseMoved (const SDL_MouseMotionEvent &sEvent) override;
 	void clickLeft(tribool down, bool previousState) override;
 	void clickRight(tribool down, bool previousState) override;
-	CClickableHex();
+	ClickableHex();
 };
 
 /// Shows the stack queue
-class CStackQueue : public CIntObject
+class StackQueue : public CIntObject
 {
 	class StackBox : public CIntObject
 	{
-		CStackQueue * owner;
+		StackQueue * owner;
 	public:
 		std::shared_ptr<CPicture> background;
 		std::shared_ptr<CAnimImage> icon;
@@ -165,13 +203,13 @@ class CStackQueue : public CIntObject
 		std::shared_ptr<CAnimImage> stateIcon;
 
 		void setUnit(const battle::Unit * unit, size_t turn = 0);
-		StackBox(CStackQueue * owner);
+		StackBox(StackQueue * owner);
 	};
 
 	static const int QUEUE_SIZE = 10;
 	std::shared_ptr<CFilledTexture> background;
 	std::vector<std::shared_ptr<StackBox>> stackBoxes;
-	CBattleInterface * owner;
+	BattleInterface & owner;
 
 	std::shared_ptr<CAnimation> icons;
 	std::shared_ptr<CAnimation> stateIcons;
@@ -180,7 +218,8 @@ class CStackQueue : public CIntObject
 public:
 	const bool embedded;
 
-	CStackQueue(bool Embedded, CBattleInterface * _owner);
-	~CStackQueue();
+	StackQueue(bool Embedded, BattleInterface & owner);
 	void update();
+
+	void show(SDL_Surface * to) override;
 };

+ 178 - 0
client/battle/BattleObstacleController.cpp

@@ -0,0 +1,178 @@
+/*
+ * BattleObstacleController.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 "BattleObstacleController.h"
+
+#include "BattleInterface.h"
+#include "BattleFieldController.h"
+#include "BattleAnimationClasses.h"
+#include "BattleStacksController.h"
+#include "BattleRenderer.h"
+#include "CreatureAnimation.h"
+
+#include "../CMusicHandler.h"
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+#include "../gui/CAnimation.h"
+#include "../gui/Canvas.h"
+#include "../gui/CGuiHandler.h"
+
+#include "../../CCallback.h"
+#include "../../lib/battle/CObstacleInstance.h"
+#include "../../lib/ObstacleHandler.h"
+
+BattleObstacleController::BattleObstacleController(BattleInterface & owner):
+	owner(owner),
+	timePassed(0.f)
+{
+	auto obst = owner.curInt->cb->battleGetAllObstacles();
+	for(auto & elem : obst)
+	{
+		if ( elem->obstacleType == CObstacleInstance::MOAT )
+			continue; // handled by siege controller;
+		loadObstacleImage(*elem);
+	}
+}
+
+void BattleObstacleController::loadObstacleImage(const CObstacleInstance & oi)
+{
+	std::string animationName;
+
+	if (auto spellObstacle = dynamic_cast<const SpellCreatedObstacle*>(&oi))
+	{
+		animationName = spellObstacle->animation;
+	}
+	else
+	{
+		assert( oi.obstacleType == CObstacleInstance::USUAL || oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE);
+		animationName = oi.getInfo().animation;
+	}
+
+	if (animationsCache.count(animationName) == 0)
+	{
+		if (oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
+		{
+			// obstacle uses single bitmap image for animations
+			auto animation = std::make_shared<CAnimation>();
+			animation->setCustom(animationName, 0, 0);
+			animationsCache[animationName] = animation;
+			animation->preload();
+		}
+		else
+		{
+			auto animation = std::make_shared<CAnimation>(animationName);
+			animationsCache[animationName] = animation;
+			animation->preload();
+		}
+	}
+	obstacleAnimations[oi.uniqueID] = animationsCache[animationName];
+}
+
+void BattleObstacleController::obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> & obstacles)
+{
+	for (auto const & oi : obstacles)
+	{
+		auto spellObstacle = dynamic_cast<const SpellCreatedObstacle*>(oi.get());
+
+		if (!spellObstacle)
+		{
+			logGlobal->error("I don't know how to animate appearing obstacle of type %d", (int)oi->obstacleType);
+			continue;
+		}
+
+		auto animation = std::make_shared<CAnimation>(spellObstacle->appearAnimation);
+		animation->preload();
+
+		auto first = animation->getImage(0, 0);
+		if(!first)
+			continue;
+
+		//we assume here that effect graphics have the same size as the usual obstacle image
+		// -> if we know how to blit obstacle, let's blit the effect in the same place
+		Point whereTo = getObstaclePosition(first, *oi);
+		CCS->soundh->playSound( spellObstacle->appearSound );
+		owner.stacksController->addNewAnim(new EffectAnimation(owner, spellObstacle->appearAnimation, whereTo, oi->pos));
+
+		//so when multiple obstacles are added, they show up one after another
+		owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+
+		loadObstacleImage(*spellObstacle);
+	}
+}
+
+void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas)
+{
+	//Blit absolute obstacles
+	for(auto & oi : owner.curInt->cb->battleGetAllObstacles())
+	{
+		if(oi->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
+		{
+			auto img = getObstacleImage(*oi);
+			if(img)
+				canvas.draw(img, Point(oi->getInfo().width, oi->getInfo().height));
+		}
+	}
+}
+
+void BattleObstacleController::collectRenderableObjects(BattleRenderer & renderer)
+{
+	for (auto obstacle : owner.curInt->cb->battleGetAllObstacles())
+	{
+		if (obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
+			continue;
+
+		if (obstacle->obstacleType == CObstacleInstance::MOAT)
+			continue;
+
+		renderer.insert(EBattleFieldLayer::OBSTACLES, obstacle->pos, [this, obstacle]( BattleRenderer::RendererRef canvas ){
+			auto img = getObstacleImage(*obstacle);
+			if(img)
+			{
+				Point p = getObstaclePosition(img, *obstacle);
+				canvas.draw(img, p);
+			}
+		});
+	}
+}
+
+void BattleObstacleController::update()
+{
+	timePassed += GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+}
+
+std::shared_ptr<IImage> BattleObstacleController::getObstacleImage(const CObstacleInstance & oi)
+{
+	int framesCount = timePassed * AnimationControls::getObstaclesSpeed();
+	std::shared_ptr<CAnimation> animation;
+
+	// obstacle is not loaded yet, don't show anything
+	if (obstacleAnimations.count(oi.uniqueID) == 0)
+		return nullptr;
+
+	animation = obstacleAnimations[oi.uniqueID];
+	assert(animation);
+
+	if(animation)
+	{
+		int frameIndex = framesCount % animation->size(0);
+		return animation->getImage(frameIndex, 0);
+	}
+	return nullptr;
+}
+
+Point BattleObstacleController::getObstaclePosition(std::shared_ptr<IImage> image, const CObstacleInstance & obstacle)
+{
+	int offset = obstacle.getAnimationYOffset(image->height());
+
+	Rect r = owner.fieldController->hexPositionLocal(obstacle.pos);
+	r.y += 42 - image->height() + offset;
+
+	return r.topLeft();
+}

+ 60 - 0
client/battle/BattleObstacleController.h

@@ -0,0 +1,60 @@
+/*
+ * BattleObstacleController.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
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct BattleHex;
+struct CObstacleInstance;
+
+VCMI_LIB_NAMESPACE_END
+
+class IImage;
+class Canvas;
+class CAnimation;
+class BattleInterface;
+class BattleRenderer;
+struct Point;
+
+/// Controls all currently active projectiles on the battlefield
+/// (with exception of moat, which is apparently handled by siege controller)
+class BattleObstacleController
+{
+	BattleInterface & owner;
+
+	/// total time, in seconds, since start of battle. Used for animating obstacles
+	float timePassed;
+
+	/// cached animations of all obstacles in current battle
+	std::map<std::string, std::shared_ptr<CAnimation>> animationsCache;
+
+	/// list of all obstacles that are currently being rendered
+	std::map<si32, std::shared_ptr<CAnimation>> obstacleAnimations;
+
+	void loadObstacleImage(const CObstacleInstance & oi);
+
+	std::shared_ptr<IImage> getObstacleImage(const CObstacleInstance & oi);
+	Point getObstaclePosition(std::shared_ptr<IImage> image, const CObstacleInstance & obstacle);
+
+public:
+	BattleObstacleController(BattleInterface & owner);
+
+	/// called every frame
+	void update();
+
+	/// call-in from network pack, add newly placed obstacles with any required animations
+	void obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> & oi);
+
+	/// renders all "absolute" obstacles
+	void showAbsoluteObstacles(Canvas & canvas);
+
+	/// adds all non-"absolute" visible obstacles for rendering
+	void collectRenderableObjects(BattleRenderer & renderer);
+};

+ 370 - 0
client/battle/BattleProjectileController.cpp

@@ -0,0 +1,370 @@
+/*
+ * BattleProjectileController.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 "BattleProjectileController.h"
+
+#include "BattleInterface.h"
+#include "BattleSiegeController.h"
+#include "BattleStacksController.h"
+#include "CreatureAnimation.h"
+
+#include "../gui/Geometries.h"
+#include "../gui/CAnimation.h"
+#include "../gui/Canvas.h"
+#include "../gui/CGuiHandler.h"
+#include "../CGameInfo.h"
+
+#include "../../lib/CStack.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+
+static double calculateCatapultParabolaY(const Point & from, const Point & dest, int x)
+{
+	double facA = 0.005; // seems to be constant
+
+	// system of 2 linear equations, solutions of which are missing coefficients
+	// for quadratic equation a*x*x + b*x + c
+	double eq[2][3] = {
+		{ static_cast<double>(from.x), 1.0, from.y - facA*from.x*from.x },
+		{ static_cast<double>(dest.x), 1.0, dest.y - facA*dest.x*dest.x }
+	};
+
+	// solve system via determinants
+	double det  = eq[0][0] *eq[1][1] - eq[1][0] *eq[0][1];
+	double detB = eq[0][2] *eq[1][1] - eq[1][2] *eq[0][1];
+	double detC = eq[0][0] *eq[1][2] - eq[1][0] *eq[0][2];
+
+	double facB = detB / det;
+	double facC = detC / det;
+
+	return facA *pow(x, 2.0) + facB *x + facC;
+}
+
+void ProjectileMissile::show(Canvas & canvas)
+{
+	size_t group = reverse ? 1 : 0;
+	auto image = animation->getImage(frameNum, group, true);
+
+	if(image)
+	{
+		Point pos {
+			vstd::lerp(from.x, dest.x, progress) - image->width() / 2,
+			vstd::lerp(from.y, dest.y, progress) - image->height() / 2,
+		};
+
+		canvas.draw(image, pos);
+	}
+
+	float timePassed = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+	progress += timePassed * speed;
+}
+
+void ProjectileAnimatedMissile::show(Canvas & canvas)
+{
+	ProjectileMissile::show(canvas);
+	frameProgress += AnimationControls::getSpellEffectSpeed() * GH.mainFPSmng->getElapsedMilliseconds() / 1000;
+	size_t animationSize = animation->size(reverse ? 1 : 0);
+	while (frameProgress > animationSize)
+		frameProgress -= animationSize;
+
+	frameNum = std::floor(frameProgress);
+}
+
+void ProjectileCatapult::show(Canvas & canvas)
+{
+	auto image = animation->getImage(frameNum, 0, true);
+
+	if(image)
+	{
+		int posX = vstd::lerp(from.x, dest.x, progress);
+		int posY = calculateCatapultParabolaY(from, dest, posX);
+		Point pos(posX, posY);
+
+		canvas.draw(image, pos);
+
+		frameNum = (frameNum + 1) % animation->size(0);
+	}
+
+	float timePassed = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+	progress += timePassed * speed;
+}
+
+void ProjectileRay::show(Canvas & canvas)
+{
+	Point curr {
+		vstd::lerp(from.x, dest.x, progress),
+		vstd::lerp(from.y, dest.y, progress),
+	};
+
+	Point length = curr - from;
+
+	//select axis to draw ray on, we want angle to be less than 45 degrees so individual sub-rays won't overlap each other
+
+	if (std::abs(length.x) > std::abs(length.y)) // draw in horizontal axis
+	{
+		int y1 =  from.y - rayConfig.size() / 2;
+		int y2 =  curr.y - rayConfig.size() / 2;
+
+		int x1 = from.x;
+		int x2 = curr.x;
+
+		for (size_t i = 0; i < rayConfig.size(); ++i)
+		{
+			auto ray = rayConfig[i];
+			SDL_Color beginColor{ ray.r1, ray.g1, ray.b1, ray.a1};
+			SDL_Color endColor  { ray.r2, ray.g2, ray.b2, ray.a2};
+
+			canvas.drawLine(Point(x1, y1 + i), Point(x2, y2+i), beginColor, endColor);
+		}
+	}
+	else // draw in vertical axis
+	{
+		int x1 = from.x - rayConfig.size() / 2;
+		int x2 = curr.x - rayConfig.size() / 2;
+
+		int y1 = from.y;
+		int y2 = curr.y;
+
+		for (size_t i = 0; i < rayConfig.size(); ++i)
+		{
+			auto ray = rayConfig[i];
+			SDL_Color beginColor{ ray.r1, ray.g1, ray.b1, ray.a1};
+			SDL_Color endColor  { ray.r2, ray.g2, ray.b2, ray.a2};
+
+			canvas.drawLine(Point(x1 + i, y1), Point(x2 + i, y2), beginColor, endColor);
+		}
+	}
+
+	float timePassed = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+	progress += timePassed * speed;
+}
+
+BattleProjectileController::BattleProjectileController(BattleInterface & owner):
+	owner(owner)
+{}
+
+const CCreature & BattleProjectileController::getShooter(const CStack * stack) const
+{
+	const CCreature * creature = stack->getCreature();
+
+	if(creature->idNumber == CreatureID::ARROW_TOWERS)
+		creature = owner.siegeController->getTurretCreature();
+
+	if(creature->animation.missleFrameAngles.empty())
+	{
+		logAnim->error("Mod error: Creature '%s' on the Archer's tower is not a shooter. Mod should be fixed. Trying to use archer's data instead...", creature->nameSing);
+		creature = CGI->creh->objects[CreatureID::ARCHER];
+	}
+
+	return *creature;
+}
+
+bool BattleProjectileController::stackUsesRayProjectile(const CStack * stack) const
+{
+	return !getShooter(stack).animation.projectileRay.empty();
+}
+
+bool BattleProjectileController::stackUsesMissileProjectile(const CStack * stack) const
+{
+	return !getShooter(stack).animation.projectileImageName.empty();
+}
+
+void BattleProjectileController::initStackProjectile(const CStack * stack)
+{
+	if (!stackUsesMissileProjectile(stack))
+		return;
+
+	const CCreature & creature = getShooter(stack);
+	projectilesCache[creature.animation.projectileImageName] = createProjectileImage(creature.animation.projectileImageName);
+}
+
+std::shared_ptr<CAnimation> BattleProjectileController::createProjectileImage(const std::string & path )
+{
+	std::shared_ptr<CAnimation> projectile = std::make_shared<CAnimation>(path);
+	projectile->preload();
+
+	if(projectile->size(1) != 0)
+		logAnim->error("Expected empty group 1 in stack projectile");
+	else
+		projectile->createFlippedGroup(0, 1);
+
+	return projectile;
+}
+
+std::shared_ptr<CAnimation> BattleProjectileController::getProjectileImage(const CStack * stack)
+{
+	const CCreature & creature = getShooter(stack);
+	std::string imageName = creature.animation.projectileImageName;
+
+	if (!projectilesCache.count(imageName))
+		initStackProjectile(stack);
+
+	return projectilesCache[imageName];
+}
+
+void BattleProjectileController::emitStackProjectile(const CStack * stack)
+{
+	int stackID = stack ? stack->ID : -1;
+
+	for (auto projectile : projectiles)
+	{
+		if ( !projectile->playing && projectile->shooterID == stackID)
+		{
+			projectile->playing = true;
+			return;
+		}
+	}
+}
+
+void BattleProjectileController::showProjectiles(Canvas & canvas)
+{
+	for ( auto projectile: projectiles)
+	{
+		if ( projectile->playing )
+			projectile->show(canvas);
+	}
+
+	vstd::erase_if(projectiles, [&](const std::shared_ptr<ProjectileBase> & projectile){
+		return projectile->progress > 1.0f;
+	});
+}
+
+bool BattleProjectileController::hasActiveProjectile(const CStack * stack, bool emittedOnly) const
+{
+	int stackID = stack ? stack->ID : -1;
+
+	for(auto const & instance : projectiles)
+	{
+		if(instance->shooterID == stackID && (instance->playing || !emittedOnly))
+		{
+			return true;
+		}
+	}
+	return false;
+}
+
+float BattleProjectileController::computeProjectileFlightTime( Point from, Point dest, double animSpeed)
+{
+	float distanceSquared = (dest.x - from.x) * (dest.x - from.x) + (dest.y - from.y) * (dest.y - from.y);
+	float distance = sqrt(distanceSquared);
+
+	assert(distance > 1.f);
+
+	return animSpeed / std::max( 1.f, distance);
+}
+
+int BattleProjectileController::computeProjectileFrameID( Point from, Point dest, const CStack * stack)
+{
+	const CCreature & creature = getShooter(stack);
+
+	auto & angles = creature.animation.missleFrameAngles;
+	auto animation = getProjectileImage(stack);
+
+	// only frames below maxFrame are usable: anything  higher is either no present or we don't know when it should be used
+	size_t maxFrame = std::min<size_t>(angles.size(), animation->size(0));
+
+	assert(maxFrame > 0);
+	double projectileAngle = -atan2(dest.y - from.y, std::abs(dest.x - from.x));
+
+	// values in angles array indicate position from which this frame was rendered, in degrees.
+	// possible range is 90 ... -90, where projectile for +90 will be used for shooting upwards, +0 for shots towards right and -90 for downwards shots
+	// find frame that has closest angle to one that we need for this shot
+	int bestID = 0;
+	double bestDiff = fabs( angles[0] / 180 * M_PI - projectileAngle );
+
+	for (int i=1; i<maxFrame; i++)
+	{
+		double currentDiff = fabs( angles[i] / 180 * M_PI - projectileAngle );
+		if (currentDiff < bestDiff)
+		{
+			bestID = i;
+			bestDiff = currentDiff;
+		}
+	}
+	return bestID;
+}
+
+void BattleProjectileController::createCatapultProjectile(const CStack * shooter, Point from, Point dest)
+{
+	auto catapultProjectile       = new ProjectileCatapult();
+
+	catapultProjectile->animation = getProjectileImage(shooter);
+	catapultProjectile->frameNum  = 0;
+	catapultProjectile->progress  = 0;
+	catapultProjectile->speed     = computeProjectileFlightTime(from, dest, AnimationControls::getCatapultSpeed());
+	catapultProjectile->from      = from;
+	catapultProjectile->dest      = dest;
+	catapultProjectile->shooterID = shooter->ID;
+	catapultProjectile->playing   = false;
+
+	projectiles.push_back(std::shared_ptr<ProjectileBase>(catapultProjectile));
+}
+
+void BattleProjectileController::createProjectile(const CStack * shooter, Point from, Point dest)
+{
+	const CCreature & shooterInfo = getShooter(shooter);
+
+	std::shared_ptr<ProjectileBase> projectile;
+	if (stackUsesRayProjectile(shooter) && stackUsesMissileProjectile(shooter))
+	{
+		logAnim->error("Mod error: Creature '%s' has both missile and ray projectiles configured. Mod should be fixed. Using ray projectile configuration...", shooterInfo.nameSing);
+	}
+
+	if (stackUsesRayProjectile(shooter))
+	{
+		auto rayProjectile = new ProjectileRay();
+		projectile.reset(rayProjectile);
+
+		rayProjectile->rayConfig = shooterInfo.animation.projectileRay;
+	}
+	else if (stackUsesMissileProjectile(shooter))
+	{
+		auto missileProjectile = new ProjectileMissile();
+		projectile.reset(missileProjectile);
+
+		missileProjectile->animation = getProjectileImage(shooter);
+		missileProjectile->reverse  = !owner.stacksController->facingRight(shooter);
+		missileProjectile->frameNum = computeProjectileFrameID(from, dest, shooter);
+	}
+
+	projectile->speed     = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed());
+	projectile->from      = from;
+	projectile->dest      = dest;
+	projectile->shooterID = shooter->ID;
+	projectile->progress  = 0;
+	projectile->playing   = false;
+
+	projectiles.push_back(projectile);
+}
+
+void BattleProjectileController::createSpellProjectile(const CStack * shooter, Point from, Point dest, const CSpell * spell)
+{
+	double projectileAngle = std::abs(atan2(dest.x - from.x, dest.y - from.y));
+	std::string animToDisplay = spell->animationInfo.selectProjectile(projectileAngle);
+
+	assert(!animToDisplay.empty());
+
+	if(!animToDisplay.empty())
+	{
+		auto projectile = new ProjectileAnimatedMissile();
+
+		projectile->animation     = createProjectileImage(animToDisplay);
+		projectile->frameProgress = 0;
+		projectile->frameNum      = 0;
+		projectile->reverse       = from.x > dest.x;
+		projectile->from          = from;
+		projectile->dest          = dest;
+		projectile->shooterID     = shooter ? shooter->ID : -1;
+		projectile->progress      = 0;
+		projectile->speed         = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed());
+		projectile->playing       = false;
+
+		projectiles.push_back(std::shared_ptr<ProjectileBase>(projectile));
+	}
+}

+ 118 - 0
client/battle/BattleProjectileController.h

@@ -0,0 +1,118 @@
+/*
+ * BattleSiegeController.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/CCreatureHandler.h"
+#include "../gui/Geometries.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CStack;
+class CSpell;
+
+VCMI_LIB_NAMESPACE_END
+
+struct Point;
+class CAnimation;
+class Canvas;
+class BattleInterface;
+
+/// Base class for projectiles
+struct ProjectileBase
+{
+	virtual ~ProjectileBase() = default;
+	virtual void show(Canvas & canvas) =  0;
+
+	Point from; // initial position on the screen
+	Point dest; // target position on the screen
+
+	float progress; // current position of projectile on from->dest line
+	float speed;    // how much progress is gained per second
+	int shooterID;  // ID of shooter stack
+	bool playing;   // if set to true, projectile animation is playing, e.g. flying to target
+};
+
+/// Projectile for most shooters - render pre-selected frame moving in straight line from origin to destination
+struct ProjectileMissile : ProjectileBase
+{
+	void show(Canvas & canvas) override;
+
+	std::shared_ptr<CAnimation> animation;
+	int frameNum;  // frame to display from projectile animation
+	bool reverse;  // if true, projectile will be flipped by vertical axis
+};
+
+/// Projectile for spell - render animation moving in straight line from origin to destination
+struct ProjectileAnimatedMissile : ProjectileMissile
+{
+	void show(Canvas & canvas) override;
+	float frameProgress;
+};
+
+/// Projectile for catapult - render spinning projectile moving at parabolic trajectory to its destination
+struct ProjectileCatapult : ProjectileBase
+{
+	void show(Canvas & canvas) override;
+
+	std::shared_ptr<CAnimation> animation;
+	int frameNum;  // frame to display from projectile animation
+};
+
+/// Projectile for mages/evil eye - render ray expanding from origin position to destination
+struct ProjectileRay : ProjectileBase
+{
+	void show(Canvas & canvas) override;
+
+	std::vector<CCreature::CreatureAnimation::RayColor> rayConfig;
+};
+
+/// Class that manages all ongoing projectiles in the game
+/// ... even though in H3 only 1 projectile can be on screen at any point of time
+class BattleProjectileController
+{
+	BattleInterface & owner;
+
+	/// all projectiles loaded during current battle
+	std::map<std::string, std::shared_ptr<CAnimation>> projectilesCache;
+
+	/// projectiles currently flying on battlefield
+	std::vector<std::shared_ptr<ProjectileBase>> projectiles;
+
+	std::shared_ptr<CAnimation> getProjectileImage(const CStack * stack);
+	std::shared_ptr<CAnimation> createProjectileImage(const std::string & path );
+	void initStackProjectile(const CStack * stack);
+
+	bool stackUsesRayProjectile(const CStack * stack) const;
+	bool stackUsesMissileProjectile(const CStack * stack) const;
+
+	void showProjectile(Canvas & canvas, std::shared_ptr<ProjectileBase> projectile);
+
+	const CCreature & getShooter(const CStack * stack) const;
+
+	int computeProjectileFrameID( Point from, Point dest, const CStack * stack);
+	float computeProjectileFlightTime( Point from, Point dest, double speed);
+
+public:
+	BattleProjectileController(BattleInterface & owner);
+
+	/// renders all currently active projectiles
+	void showProjectiles(Canvas & canvas);
+
+	/// returns true if stack has projectile that is yet to hit target
+	bool hasActiveProjectile(const CStack * stack, bool emittedOnly) const;
+
+	/// starts rendering previously created projectile
+	void emitStackProjectile(const CStack * stack);
+
+	/// creates (but not emits!) projectile and initializes it based on parameters
+	void createProjectile(const CStack * shooter, Point from, Point dest);
+	void createSpellProjectile(const CStack * shooter, Point from, Point dest, const CSpell * spell);
+	void createCatapultProjectile(const CStack * shooter, Point from, Point dest);
+};

+ 76 - 0
client/battle/BattleRenderer.cpp

@@ -0,0 +1,76 @@
+/*
+ * BattleFieldController.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 "BattleRenderer.h"
+
+#include "BattleInterface.h"
+#include "BattleInterfaceClasses.h"
+#include "BattleEffectsController.h"
+#include "BattleWindow.h"
+#include "BattleSiegeController.h"
+#include "BattleStacksController.h"
+#include "BattleObstacleController.h"
+
+void BattleRenderer::collectObjects()
+{
+	owner.effectsController->collectRenderableObjects(*this);
+	owner.obstacleController->collectRenderableObjects(*this);
+	owner.stacksController->collectRenderableObjects(*this);
+	if (owner.siegeController)
+		owner.siegeController->collectRenderableObjects(*this);
+	if (owner.defendingHero)
+		owner.defendingHero->collectRenderableObjects(*this);
+	if (owner.attackingHero)
+		owner.attackingHero->collectRenderableObjects(*this);
+}
+
+void BattleRenderer::sortObjects()
+{
+	auto getRow = [](const RenderableInstance & object) -> int
+	{
+		if (object.tile.isValid())
+			return object.tile.getY();
+
+		if ( object.tile == BattleHex::HEX_BEFORE_ALL )
+			return -1;
+
+		assert( object.tile == BattleHex::HEX_AFTER_ALL || object.tile == BattleHex::INVALID);
+		return GameConstants::BFIELD_HEIGHT;
+	};
+
+	std::stable_sort(objects.begin(), objects.end(), [&](const RenderableInstance & left, const RenderableInstance & right){
+		if ( getRow(left) != getRow(right))
+			return getRow(left) < getRow(right);
+		return left.layer < right.layer;
+	});
+}
+
+void BattleRenderer::renderObjects(BattleRenderer::RendererRef targetCanvas)
+{
+	for (auto const & object : objects)
+		object.functor(targetCanvas);
+}
+
+BattleRenderer::BattleRenderer(BattleInterface & owner):
+	owner(owner)
+{
+}
+
+void BattleRenderer::insert(EBattleFieldLayer layer, BattleHex tile, BattleRenderer::RenderFunctor functor)
+{
+	objects.push_back({functor, layer, tile});
+}
+
+void BattleRenderer::execute(BattleRenderer::RendererRef targetCanvas)
+{
+	collectObjects();
+	sortObjects();
+	renderObjects(targetCanvas);
+}

+ 54 - 0
client/battle/BattleRenderer.h

@@ -0,0 +1,54 @@
+/*
+ * BattleFieldController.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/BattleHex.h"
+
+class Canvas;
+class BattleInterface;
+
+enum class EBattleFieldLayer {
+					   // confirmed ordering requirements:
+	OBSTACLES     = 0,
+	CORPSES       = 0,
+	WALLS         = 1,
+	HEROES        = 2,
+	STACKS        = 2, // after corpses, obstacles, walls
+	BATTLEMENTS   = 3, // after stacks
+	STACK_AMOUNTS = 3, // after stacks, obstacles, corpses
+	EFFECTS       = 4, // after obstacles, battlements
+};
+
+class BattleRenderer
+{
+public:
+	using RendererRef = Canvas &;
+	using RenderFunctor = std::function<void(RendererRef)>;
+
+private:
+	BattleInterface & owner;
+
+	struct RenderableInstance
+	{
+		RenderFunctor functor;
+		EBattleFieldLayer layer;
+		BattleHex tile;
+	};
+	std::vector<RenderableInstance> objects;
+
+	void collectObjects();
+	void sortObjects();
+	void renderObjects(RendererRef targetCanvas);
+public:
+	BattleRenderer(BattleInterface & owner);
+
+	void insert(EBattleFieldLayer layer, BattleHex tile, RenderFunctor functor);
+	void execute(RendererRef targetCanvas);
+};

+ 371 - 0
client/battle/BattleSiegeController.cpp

@@ -0,0 +1,371 @@
+/*
+ * BattleSiegeController.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 "BattleSiegeController.h"
+
+#include "BattleAnimationClasses.h"
+#include "BattleInterface.h"
+#include "BattleInterfaceClasses.h"
+#include "BattleStacksController.h"
+#include "BattleFieldController.h"
+#include "BattleRenderer.h"
+
+#include "../CMusicHandler.h"
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+#include "../gui/CAnimation.h"
+#include "../gui/Canvas.h"
+
+#include "../../CCallback.h"
+#include "../../lib/NetPacks.h"
+#include "../../lib/CStack.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+
+std::string BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual what, EWallState::EWallState state) const
+{
+	auto getImageIndex = [&]() -> int
+	{
+		switch (state)
+		{
+		case EWallState::INTACT :
+			return 1;
+		case EWallState::DAMAGED :
+			// towers don't have separate image here - INTACT and DAMAGED is 1, DESTROYED is 2
+			if(what == EWallVisual::KEEP || what == EWallVisual::BOTTOM_TOWER || what == EWallVisual::UPPER_TOWER)
+				return 1;
+			else
+				return 2;
+		case EWallState::DESTROYED :
+			if (what == EWallVisual::KEEP || what == EWallVisual::BOTTOM_TOWER || what == EWallVisual::UPPER_TOWER)
+				return 2;
+			else
+				return 3;
+		}
+		return 1;
+	};
+
+	const std::string & prefix = town->town->clientInfo.siegePrefix;
+	std::string addit = boost::lexical_cast<std::string>(getImageIndex());
+
+	switch(what)
+	{
+	case EWallVisual::BACKGROUND_WALL:
+		{
+			switch(town->town->faction->index)
+			{
+			case ETownType::RAMPART:
+			case ETownType::NECROPOLIS:
+			case ETownType::DUNGEON:
+			case ETownType::STRONGHOLD:
+				return prefix + "TPW1.BMP";
+			default:
+				return prefix + "TPWL.BMP";
+			}
+		}
+	case EWallVisual::KEEP:
+		return prefix + "MAN" + addit + ".BMP";
+	case EWallVisual::BOTTOM_TOWER:
+		return prefix + "TW1" + addit + ".BMP";
+	case EWallVisual::BOTTOM_WALL:
+		return prefix + "WA1" + addit + ".BMP";
+	case EWallVisual::WALL_BELLOW_GATE:
+		return prefix + "WA3" + addit + ".BMP";
+	case EWallVisual::WALL_OVER_GATE:
+		return prefix + "WA4" + addit + ".BMP";
+	case EWallVisual::UPPER_WALL:
+		return prefix + "WA6" + addit + ".BMP";
+	case EWallVisual::UPPER_TOWER:
+		return prefix + "TW2" + addit + ".BMP";
+	case EWallVisual::GATE:
+		return prefix + "DRW" + addit + ".BMP";
+	case EWallVisual::GATE_ARCH:
+		return prefix + "ARCH.BMP";
+	case EWallVisual::BOTTOM_STATIC_WALL:
+		return prefix + "WA2.BMP";
+	case EWallVisual::UPPER_STATIC_WALL:
+		return prefix + "WA5.BMP";
+	case EWallVisual::MOAT:
+		return prefix + "MOAT.BMP";
+	case EWallVisual::MOAT_BANK:
+		return prefix + "MLIP.BMP";
+	case EWallVisual::KEEP_BATTLEMENT:
+		return prefix + "MANC.BMP";
+	case EWallVisual::BOTTOM_BATTLEMENT:
+		return prefix + "TW1C.BMP";
+	case EWallVisual::UPPER_BATTLEMENT:
+		return prefix + "TW2C.BMP";
+	default:
+		return "";
+	}
+}
+
+void BattleSiegeController::showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what)
+{
+	auto & ci = town->town->clientInfo;
+	auto const & pos = ci.siegePositions[what];
+
+	if ( wallPieceImages[what])
+		canvas.draw(wallPieceImages[what], Point(pos.x, pos.y));
+}
+
+std::string BattleSiegeController::getBattleBackgroundName() const
+{
+	const std::string & prefix = town->town->clientInfo.siegePrefix;
+	return prefix + "BACK.BMP";
+}
+
+bool BattleSiegeController::getWallPieceExistance(EWallVisual::EWallVisual what) const
+{
+	//FIXME: use this instead of buildings test?
+	//ui8 siegeLevel = owner.curInt->cb->battleGetSiegeLevel();
+
+	switch (what)
+	{
+	case EWallVisual::MOAT:              return town->hasBuilt(BuildingID::CITADEL) && town->town->faction->index != ETownType::TOWER;
+	case EWallVisual::MOAT_BANK:         return town->hasBuilt(BuildingID::CITADEL) && town->town->faction->index != ETownType::TOWER && town->town->faction->index != ETownType::NECROPOLIS;
+	case EWallVisual::KEEP_BATTLEMENT:   return town->hasBuilt(BuildingID::CITADEL) && EWallState::EWallState(owner.curInt->cb->battleGetWallState(EWallPart::KEEP)) != EWallState::DESTROYED;
+	case EWallVisual::UPPER_BATTLEMENT:  return town->hasBuilt(BuildingID::CASTLE) && EWallState::EWallState(owner.curInt->cb->battleGetWallState(EWallPart::UPPER_TOWER)) != EWallState::DESTROYED;
+	case EWallVisual::BOTTOM_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && EWallState::EWallState(owner.curInt->cb->battleGetWallState(EWallPart::BOTTOM_TOWER)) != EWallState::DESTROYED;
+	default:                             return true;
+	}
+}
+
+BattleHex BattleSiegeController::getWallPiecePosition(EWallVisual::EWallVisual what) const
+{
+	static const std::array<BattleHex, 18> wallsPositions = {
+		BattleHex::INVALID,        // BACKGROUND,         // handled separately
+		BattleHex::HEX_BEFORE_ALL, // BACKGROUND_WALL,
+		135,                       // KEEP,
+		BattleHex::HEX_AFTER_ALL,  // BOTTOM_TOWER,
+		182,                       // BOTTOM_WALL,
+		130,                       // WALL_BELLOW_GATE,
+		62,                        // WALL_OVER_GATE,
+		12,                        // UPPER_WALL,
+		BattleHex::HEX_BEFORE_ALL, // UPPER_TOWER,
+		BattleHex::HEX_BEFORE_ALL, // GATE,               // 94
+		112,                       // GATE_ARCH,
+		165,                       // BOTTOM_STATIC_WALL,
+		45,                        // UPPER_STATIC_WALL,
+		BattleHex::INVALID,        // MOAT,               // printed as absolute obstacle
+		BattleHex::INVALID,        // MOAT_BANK,          // printed as absolute obstacle
+		135,                       // KEEP_BATTLEMENT,
+		BattleHex::HEX_AFTER_ALL,  // BOTTOM_BATTLEMENT,
+		BattleHex::HEX_BEFORE_ALL, // UPPER_BATTLEMENT,
+	};
+
+	return wallsPositions[what];
+}
+
+BattleSiegeController::BattleSiegeController(BattleInterface & owner, const CGTownInstance *siegeTown):
+	owner(owner),
+	town(siegeTown)
+{
+	assert(owner.fieldController.get() == nullptr); // must be created after this
+
+	for (int g = 0; g < wallPieceImages.size(); ++g)
+	{
+		if ( g == EWallVisual::GATE ) // gate is initially closed and has no image to display in this state
+			continue;
+
+		if ( !getWallPieceExistance(EWallVisual::EWallVisual(g)) )
+			continue;
+
+		wallPieceImages[g] = IImage::createFromFile(getWallPieceImageName(EWallVisual::EWallVisual(g), EWallState::INTACT));
+	}
+}
+
+const CCreature *BattleSiegeController::getTurretCreature() const
+{
+	return CGI->creh->objects[town->town->clientInfo.siegeShooter];
+}
+
+Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) const
+{
+	// Turret positions are read out of the config/wall_pos.txt
+	int posID = 0;
+	switch (position)
+	{
+	case BattleHex::CASTLE_CENTRAL_TOWER: // keep creature
+		posID = EWallVisual::CREATURE_KEEP;
+		break;
+	case BattleHex::CASTLE_BOTTOM_TOWER: // bottom creature
+		posID = EWallVisual::CREATURE_BOTTOM_TOWER;
+		break;
+	case BattleHex::CASTLE_UPPER_TOWER: // upper creature
+		posID = EWallVisual::CREATURE_UPPER_TOWER;
+		break;
+	}
+
+	if (posID != 0)
+	{
+		return {
+			town->town->clientInfo.siegePositions[posID].x,
+			town->town->clientInfo.siegePositions[posID].y
+		};
+	}
+
+	assert(0);
+	return Point(0,0);
+}
+
+void BattleSiegeController::gateStateChanged(const EGateState state)
+{
+	auto oldState = owner.curInt->cb->battleGetGateState();
+	bool playSound = false;
+	auto stateId = EWallState::NONE;
+	switch(state)
+	{
+	case EGateState::CLOSED:
+		if (oldState != EGateState::BLOCKED)
+			playSound = true;
+		break;
+	case EGateState::BLOCKED:
+		if (oldState != EGateState::CLOSED)
+			playSound = true;
+		break;
+	case EGateState::OPENED:
+		playSound = true;
+		stateId = EWallState::DAMAGED;
+		break;
+	case EGateState::DESTROYED:
+		stateId = EWallState::DESTROYED;
+		break;
+	}
+
+	if (oldState != EGateState::NONE && oldState != EGateState::CLOSED && oldState != EGateState::BLOCKED)
+		wallPieceImages[EWallVisual::GATE] = nullptr;
+
+	if (stateId != EWallState::NONE)
+		wallPieceImages[EWallVisual::GATE] = IImage::createFromFile(getWallPieceImageName(EWallVisual::GATE,  stateId));
+
+	if (playSound)
+		CCS->soundh->playSound(soundBase::DRAWBRG);
+}
+
+void BattleSiegeController::showAbsoluteObstacles(Canvas & canvas)
+{
+	if (getWallPieceExistance(EWallVisual::MOAT))
+		showWallPiece(canvas, EWallVisual::MOAT);
+
+	if (getWallPieceExistance(EWallVisual::MOAT_BANK))
+		showWallPiece(canvas, EWallVisual::MOAT_BANK);
+}
+
+BattleHex BattleSiegeController::getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const
+{
+	switch(wallPiece)
+	{
+	case EWallVisual::KEEP_BATTLEMENT:   return BattleHex::CASTLE_CENTRAL_TOWER;
+	case EWallVisual::BOTTOM_BATTLEMENT: return BattleHex::CASTLE_BOTTOM_TOWER;
+	case EWallVisual::UPPER_BATTLEMENT:  return BattleHex::CASTLE_UPPER_TOWER;
+	}
+	assert(0);
+	return BattleHex::INVALID;
+}
+
+const CStack * BattleSiegeController::getTurretStack(EWallVisual::EWallVisual wallPiece) const
+{
+	for (auto & stack : owner.curInt->cb->battleGetAllStacks(true))
+	{
+		if ( stack->initialPosition == getTurretBattleHex(wallPiece))
+			return stack;
+	}
+	assert(0);
+	return nullptr;
+}
+
+void BattleSiegeController::collectRenderableObjects(BattleRenderer & renderer)
+{
+	for (int i = EWallVisual::WALL_FIRST; i <= EWallVisual::WALL_LAST; ++i)
+	{
+		auto wallPiece = EWallVisual::EWallVisual(i);
+
+		if ( !getWallPieceExistance(wallPiece))
+			continue;
+
+		if ( getWallPiecePosition(wallPiece) == BattleHex::INVALID)
+			continue;
+
+		if (wallPiece == EWallVisual::KEEP_BATTLEMENT ||
+			wallPiece == EWallVisual::BOTTOM_BATTLEMENT ||
+			wallPiece == EWallVisual::UPPER_BATTLEMENT)
+		{
+			renderer.insert( EBattleFieldLayer::STACKS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
+				owner.stacksController->showStack(canvas, getTurretStack(wallPiece));
+			});
+			renderer.insert( EBattleFieldLayer::BATTLEMENTS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
+				showWallPiece(canvas, wallPiece);
+			});
+		}
+		renderer.insert( EBattleFieldLayer::WALLS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
+			showWallPiece(canvas, wallPiece);
+		});
+
+
+	}
+}
+
+bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const
+{
+	if (owner.tacticsMode)
+		return false;
+
+	auto wallPart = owner.curInt->cb->battleHexToWallPart(hex);
+	if (!owner.curInt->cb->isWallPartPotentiallyAttackable(wallPart))
+		return false;
+
+	auto state = owner.curInt->cb->battleGetWallState(static_cast<int>(wallPart));
+	return state != EWallState::DESTROYED && state != EWallState::NONE;
+}
+
+void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca)
+{
+	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+
+	if (ca.attacker != -1)
+	{
+		const CStack *stack = owner.curInt->cb->battleGetStackByID(ca.attacker);
+		for (auto attackInfo : ca.attackedParts)
+		{
+			owner.stacksController->addNewAnim(new CatapultAnimation(owner, stack, attackInfo.destinationTile, nullptr, attackInfo.damageDealt));
+		}
+	}
+	else
+	{
+		std::vector<Point> positions;
+
+		//no attacker stack, assume spell-related (earthquake) - only hit animation
+		for (auto attackInfo : ca.attackedParts)
+			positions.push_back(owner.stacksController->getStackPositionAtHex(attackInfo.destinationTile, nullptr) + Point(99, 120));
+
+		CCS->soundh->playSound( "WALLHIT" );
+		owner.stacksController->addNewAnim(new EffectAnimation(owner, "SGEXPL.DEF", positions));
+	}
+
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.setAnimationCondition(EAnimationEvents::HIT, false);
+
+	for (auto attackInfo : ca.attackedParts)
+	{
+		int wallId = attackInfo.attackedPart + EWallVisual::DESTRUCTIBLE_FIRST;
+		//gate state changing handled separately
+		if (wallId == EWallVisual::GATE)
+			continue;
+
+		auto wallState = EWallState::EWallState(owner.curInt->cb->battleGetWallState(attackInfo.attackedPart));
+
+		wallPieceImages[wallId] = IImage::createFromFile(getWallPieceImageName(EWallVisual::EWallVisual(wallId), wallState));
+	}
+}
+
+const CGTownInstance *BattleSiegeController::getSiegedTown() const
+{
+	return town;
+}

+ 110 - 0
client/battle/BattleSiegeController.h

@@ -0,0 +1,110 @@
+/*
+ * BattleObstacleController.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/GameConstants.h"
+#include "../../lib/battle/BattleHex.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct CatapultAttack;
+class CCreature;
+class CStack;
+class CGTownInstance;
+
+VCMI_LIB_NAMESPACE_END
+
+struct Point;
+class Canvas;
+class BattleInterface;
+class BattleRenderer;
+class IImage;
+
+namespace EWallVisual
+{
+	enum EWallVisual
+	{
+		BACKGROUND,
+		BACKGROUND_WALL,
+
+		KEEP,
+		BOTTOM_TOWER,
+		BOTTOM_WALL,
+		WALL_BELLOW_GATE,
+		WALL_OVER_GATE,
+		UPPER_WALL,
+		UPPER_TOWER,
+		GATE,
+
+		GATE_ARCH,
+		BOTTOM_STATIC_WALL,
+		UPPER_STATIC_WALL,
+		MOAT,
+		MOAT_BANK,
+		KEEP_BATTLEMENT,
+		BOTTOM_BATTLEMENT,
+		UPPER_BATTLEMENT,
+
+		CREATURE_KEEP,
+		CREATURE_BOTTOM_TOWER,
+		CREATURE_UPPER_TOWER,
+
+		WALL_FIRST = BACKGROUND_WALL,
+		WALL_LAST  = UPPER_BATTLEMENT,
+
+		// these entries are mapped to EWallPart enum
+		DESTRUCTIBLE_FIRST = KEEP,
+		DESTRUCTIBLE_LAST = GATE,
+	};
+}
+
+class BattleSiegeController
+{
+	BattleInterface & owner;
+
+	/// besieged town
+	const CGTownInstance *town;
+
+	/// sections of castle walls, in their currently visible state
+	std::array<std::shared_ptr<IImage>, EWallVisual::WALL_LAST + 1> wallPieceImages;
+
+	/// return URI for image for a wall piece
+	std::string getWallPieceImageName(EWallVisual::EWallVisual what, EWallState::EWallState state) const;
+
+	/// returns BattleHex to which chosen wall piece is bound
+	BattleHex getWallPiecePosition(EWallVisual::EWallVisual what) const;
+
+	/// returns true if chosen wall piece should be present in current battle
+	bool getWallPieceExistance(EWallVisual::EWallVisual what) const;
+
+	void showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what);
+
+	BattleHex getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const;
+	const CStack * getTurretStack(EWallVisual::EWallVisual wallPiece) const;
+
+public:
+	BattleSiegeController(BattleInterface & owner, const CGTownInstance *siegeTown);
+
+	/// call-ins from server
+	void gateStateChanged(const EGateState state);
+	void stackIsCatapulting(const CatapultAttack & ca);
+
+	/// call-ins from other battle controllers
+	void showAbsoluteObstacles(Canvas & canvas);
+	void collectRenderableObjects(BattleRenderer & renderer);
+
+	/// queries from other battle controllers
+	bool isAttackableByCatapult(BattleHex hex) const;
+	std::string getBattleBackgroundName() const;
+	const CCreature *getTurretCreature() const;
+	Point getTurretCreaturePosition( BattleHex position ) const;
+
+	const CGTownInstance *getSiegedTown() const;
+};

+ 941 - 0
client/battle/BattleStacksController.cpp

@@ -0,0 +1,941 @@
+/*
+ * BattleStacksController.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 "BattleStacksController.h"
+
+#include "BattleSiegeController.h"
+#include "BattleInterfaceClasses.h"
+#include "BattleInterface.h"
+#include "BattleActionsController.h"
+#include "BattleAnimationClasses.h"
+#include "BattleFieldController.h"
+#include "BattleEffectsController.h"
+#include "BattleProjectileController.h"
+#include "BattleWindow.h"
+#include "BattleRenderer.h"
+#include "CreatureAnimation.h"
+
+#include "../CPlayerInterface.h"
+#include "../CMusicHandler.h"
+#include "../CGameInfo.h"
+#include "../gui/CAnimation.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/Canvas.h"
+#include "../../lib/spells/ISpellMechanics.h"
+
+#include "../../CCallback.h"
+#include "../../lib/battle/BattleHex.h"
+#include "../../lib/CGameState.h"
+#include "../../lib/CStack.h"
+#include "../../lib/CondSh.h"
+
+static void onAnimationFinished(const CStack *stack, std::weak_ptr<CreatureAnimation> anim)
+{
+	std::shared_ptr<CreatureAnimation> animation = anim.lock();
+	if(!animation)
+		return;
+
+	if (!stack->isFrozen() && animation->getType() == ECreatureAnimType::FROZEN)
+		animation->setType(ECreatureAnimType::HOLDING);
+
+	if (animation->isIdle())
+	{
+		const CCreature *creature = stack->getCreature();
+
+		if (stack->isFrozen())
+			animation->setType(ECreatureAnimType::FROZEN);
+		else
+		if (animation->framesInGroup(ECreatureAnimType::MOUSEON) > 0)
+		{
+			if (CRandomGenerator::getDefault().nextDouble(99.0) < creature->animation.timeBetweenFidgets *10)
+				animation->playOnce(ECreatureAnimType::MOUSEON);
+			else
+				animation->setType(ECreatureAnimType::HOLDING);
+		}
+		else
+		{
+			animation->setType(ECreatureAnimType::HOLDING);
+		}
+	}
+	// always reset callback
+	animation->onAnimationReset += std::bind(&onAnimationFinished, stack, anim);
+}
+
+BattleStacksController::BattleStacksController(BattleInterface & owner):
+	owner(owner),
+	activeStack(nullptr),
+	stackToActivate(nullptr),
+	selectedStack(nullptr),
+	stackCanCastSpell(false),
+	creatureSpellToCast(uint32_t(-1)),
+	animIDhelper(0)
+{
+	//preparing graphics for displaying amounts of creatures
+	amountNormal     = IImage::createFromFile("CMNUMWIN.BMP");
+	amountPositive   = IImage::createFromFile("CMNUMWIN.BMP");
+	amountNegative   = IImage::createFromFile("CMNUMWIN.BMP");
+	amountEffNeutral = IImage::createFromFile("CMNUMWIN.BMP");
+
+	static const auto shifterNormal   = ColorFilter::genRangeShifter( 0,0,0, 0.6, 0.2, 1.0 );
+	static const auto shifterPositive = ColorFilter::genRangeShifter( 0,0,0, 0.2, 1.0, 0.2 );
+	static const auto shifterNegative = ColorFilter::genRangeShifter( 0,0,0, 1.0, 0.2, 0.2 );
+	static const auto shifterNeutral  = ColorFilter::genRangeShifter( 0,0,0, 1.0, 1.0, 0.2 );
+
+	amountNormal->adjustPalette(shifterNormal);
+	amountPositive->adjustPalette(shifterPositive);
+	amountNegative->adjustPalette(shifterNegative);
+	amountEffNeutral->adjustPalette(shifterNeutral);
+
+	//Restore border color {255, 231, 132, 255} to its original state
+	amountNormal->resetPalette(26);
+	amountPositive->resetPalette(26);
+	amountNegative->resetPalette(26);
+	amountEffNeutral->resetPalette(26);
+
+	std::vector<const CStack*> stacks = owner.curInt->cb->battleGetAllStacks(true);
+	for(const CStack * s : stacks)
+	{
+		stackAdded(s, true);
+	}
+}
+
+BattleHex BattleStacksController::getStackCurrentPosition(const CStack * stack) const
+{
+	if ( !stackAnimation.at(stack->ID)->isMoving())
+		return stack->getPosition();
+
+	if (stack->hasBonusOfType(Bonus::FLYING) && stackAnimation.at(stack->ID)->getType() == ECreatureAnimType::MOVING )
+		return BattleHex::HEX_AFTER_ALL;
+
+	for (auto & anim : currentAnimations)
+	{
+		// certainly ugly workaround but fixes quite annoying bug
+		// stack position will be updated only *after* movement is finished
+		// before this - stack is always at its initial position. Thus we need to find
+		// its current position. Which can be found only in this class
+		if (StackMoveAnimation *move = dynamic_cast<StackMoveAnimation*>(anim))
+		{
+			if (move->stack == stack)
+				return std::max(move->prevHex, move->nextHex);
+		}
+	}
+	return stack->getPosition();
+}
+
+void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer)
+{
+	auto stacks = owner.curInt->cb->battleGetAllStacks(false);
+
+	for (auto stack : stacks)
+	{
+		if (stackAnimation.find(stack->ID) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks
+			continue;
+
+		//FIXME: hack to ignore ghost stacks
+		if ((stackAnimation[stack->ID]->getType() == ECreatureAnimType::DEAD || stackAnimation[stack->ID]->getType() == ECreatureAnimType::HOLDING) && stack->isGhost())
+			continue;
+
+		auto layer = stackAnimation[stack->ID]->isDead() ? EBattleFieldLayer::CORPSES : EBattleFieldLayer::STACKS;
+		auto location = getStackCurrentPosition(stack);
+
+		renderer.insert(layer, location, [this, stack]( BattleRenderer::RendererRef renderer ){
+			showStack(renderer, stack);
+		});
+
+		if (stackNeedsAmountBox(stack))
+		{
+			renderer.insert(EBattleFieldLayer::STACK_AMOUNTS, location, [this, stack]( BattleRenderer::RendererRef renderer ){
+				showStackAmountBox(renderer, stack);
+			});
+		}
+	}
+}
+
+void BattleStacksController::stackReset(const CStack * stack)
+{
+	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+
+	//reset orientation?
+	//stackFacingRight[stack->ID] = stack->side == BattleSide::ATTACKER;
+
+	auto iter = stackAnimation.find(stack->ID);
+
+	if(iter == stackAnimation.end())
+	{
+		logGlobal->error("Unit %d have no animation", stack->ID);
+		return;
+	}
+
+	auto animation = iter->second;
+
+	if(stack->alive() && animation->isDeadOrDying())
+	{
+		owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=]()
+		{
+			addNewAnim(new ResurrectionAnimation(owner, stack));
+		});
+	}
+}
+
+void BattleStacksController::stackAdded(const CStack * stack, bool instant)
+{
+	// Tower shooters have only their upper half visible
+	static const int turretCreatureAnimationHeight = 225;
+
+	stackFacingRight[stack->ID] = stack->side == BattleSide::ATTACKER; // must be set before getting stack position
+
+	Point coords = getStackPositionAtHex(stack->getPosition(), stack);
+
+	if(stack->initialPosition < 0) //turret
+	{
+		assert(owner.siegeController);
+
+		const CCreature *turretCreature = owner.siegeController->getTurretCreature();
+
+		stackAnimation[stack->ID] = AnimationControls::getAnimation(turretCreature);
+		stackAnimation[stack->ID]->pos.h = turretCreatureAnimationHeight;
+
+		coords = owner.siegeController->getTurretCreaturePosition(stack->initialPosition);
+	}
+	else
+	{
+		stackAnimation[stack->ID] = AnimationControls::getAnimation(stack->getCreature());
+		stackAnimation[stack->ID]->onAnimationReset += std::bind(&onAnimationFinished, stack, stackAnimation[stack->ID]);
+		stackAnimation[stack->ID]->pos.h = stackAnimation[stack->ID]->getHeight();
+	}
+	stackAnimation[stack->ID]->pos.x = coords.x;
+	stackAnimation[stack->ID]->pos.y = coords.y;
+	stackAnimation[stack->ID]->pos.w = stackAnimation[stack->ID]->getWidth();
+	stackAnimation[stack->ID]->setType(ECreatureAnimType::HOLDING);
+
+	if (!instant)
+	{
+		// immediately make stack transparent, giving correct shifter time to start
+		auto shifterFade = ColorFilter::genAlphaShifter(0);
+		setStackColorFilter(shifterFade, stack, nullptr, true);
+
+		owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=]()
+		{
+			addNewAnim(new ColorTransformAnimation(owner, stack, "summonFadeIn", nullptr));
+			if (stack->isClone())
+				addNewAnim(new ColorTransformAnimation(owner, stack, "cloning", SpellID(SpellID::CLONE).toSpell() ));
+		});
+	}
+}
+
+void BattleStacksController::setActiveStack(const CStack *stack)
+{
+	if (activeStack) // update UI
+		stackAnimation[activeStack->ID]->setBorderColor(AnimationControls::getNoBorder());
+
+	activeStack = stack;
+
+	if (activeStack) // update UI
+		stackAnimation[activeStack->ID]->setBorderColor(AnimationControls::getGoldBorder());
+
+	owner.windowObject->blockUI(activeStack == nullptr);
+}
+
+bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const
+{
+	BattleHex currentActionTarget;
+	if(owner.curInt->curAction)
+	{
+		auto target = owner.curInt->curAction->getTarget(owner.curInt->cb.get());
+		if(!target.empty())
+			currentActionTarget = target.at(0).hexValue;
+	}
+
+	//do not show box for singular war machines, stacked war machines with box shown are supported as extension feature
+	if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON) && stack->getCount() == 1)
+		return false;
+
+	if(!stack->alive())
+		return false;
+
+	//hide box when target is going to die anyway - do not display "0 creatures"
+	if(stack->getCount() == 0)
+		return false;
+
+	// if stack has any ongoing animation - hide the box
+	for(auto anim : currentAnimations)
+	{
+		auto stackAnimation = dynamic_cast<BattleStackAnimation*>(anim);
+		if(stackAnimation && (stackAnimation->stack->ID == stack->ID))
+			return false;
+	}
+
+	return true;
+}
+
+std::shared_ptr<IImage> BattleStacksController::getStackAmountBox(const CStack * stack)
+{
+	std::vector<si32> activeSpells = stack->activeSpells();
+
+	if ( activeSpells.empty())
+		return amountNormal;
+
+	int effectsPositivness = 0;
+
+	for ( auto const & spellID : activeSpells)
+		effectsPositivness += CGI->spellh->objects.at(spellID)->positiveness;
+
+	if (effectsPositivness > 0)
+		return amountPositive;
+
+	if (effectsPositivness < 0)
+		return amountNegative;
+
+	return amountEffNeutral;
+}
+
+void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack * stack)
+{
+	//blitting amount background box
+	auto amountBG = getStackAmountBox(stack);
+
+	const int sideShift = stack->side == BattleSide::ATTACKER ? 1 : -1;
+	const int reverseSideShift = stack->side == BattleSide::ATTACKER ? -1 : 1;
+	const BattleHex nextPos = stack->getPosition() + sideShift;
+	const bool edge = stack->getPosition() % GameConstants::BFIELD_WIDTH == (stack->side == BattleSide::ATTACKER ? GameConstants::BFIELD_WIDTH - 2 : 1);
+	const bool moveInside = !edge && !owner.fieldController->stackCountOutsideHex(nextPos);
+
+	int xAdd = (stack->side == BattleSide::ATTACKER ? 220 : 202) +
+			(stack->doubleWide() ? 44 : 0) * sideShift +
+			(moveInside ? amountBG->width() + 10 : 0) * reverseSideShift;
+	int yAdd = 260 + ((stack->side == BattleSide::ATTACKER || moveInside) ? 0 : -15);
+
+	canvas.draw(amountBG, stackAnimation[stack->ID]->pos.topLeft() + Point(xAdd, yAdd));
+
+	//blitting amount
+	Point textPos = stackAnimation[stack->ID]->pos.topLeft() + amountBG->dimensions()/2 + Point(xAdd, yAdd);
+
+	canvas.drawText(textPos, EFonts::FONT_TINY, Colors::WHITE, ETextAlignment::CENTER, makeNumberShort(stack->getCount()));
+}
+
+void BattleStacksController::showStack(Canvas & canvas, const CStack * stack)
+{
+	ColorFilter fullFilter = ColorFilter::genEmptyShifter();
+	for (auto const & filter : stackFilterEffects)
+	{
+		if (filter.target == stack)
+			fullFilter = ColorFilter::genCombined(fullFilter, filter.effect);
+	}
+
+	bool stackHasProjectile = owner.projectilesController->hasActiveProjectile(stack, true);
+
+	if (stackHasProjectile)
+		stackAnimation[stack->ID]->pause();
+	else
+		stackAnimation[stack->ID]->play();
+
+	stackAnimation[stack->ID]->nextFrame(canvas, fullFilter, facingRight(stack)); // do actual blit
+	stackAnimation[stack->ID]->incrementFrame(float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000);
+}
+
+void BattleStacksController::update()
+{
+	updateHoveredStacks();
+	updateBattleAnimations();
+}
+
+void BattleStacksController::initializeBattleAnimations()
+{
+	auto copiedVector = currentAnimations;
+	for (auto & elem : copiedVector)
+		if (elem && !elem->isInitialized())
+			elem->tryInitialize();
+}
+
+void BattleStacksController::stepFrameBattleAnimations()
+{
+	// operate on copy - to prevent potential iterator invalidation due to push_back's
+	// FIXME? : remove remaining calls to addNewAnim from BattleAnimation::nextFrame (only Catapult explosion at the time of writing)
+	auto copiedVector = currentAnimations;
+	for (auto & elem : copiedVector)
+		if (elem && elem->isInitialized())
+			elem->nextFrame();
+}
+
+void BattleStacksController::updateBattleAnimations()
+{
+	bool hadAnimations = !currentAnimations.empty();
+	initializeBattleAnimations();
+	stepFrameBattleAnimations();
+	vstd::erase(currentAnimations, nullptr);
+
+	if (hadAnimations && currentAnimations.empty())
+		owner.setAnimationCondition(EAnimationEvents::ACTION, false);
+
+	initializeBattleAnimations();
+}
+
+void BattleStacksController::addNewAnim(BattleAnimation *anim)
+{
+	currentAnimations.push_back(anim);
+	owner.setAnimationCondition(EAnimationEvents::ACTION, true);
+}
+
+void BattleStacksController::stackRemoved(uint32_t stackID)
+{
+	if (getActiveStack() && getActiveStack()->ID == stackID)
+	{
+		BattleAction *action = new BattleAction();
+		action->side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false;
+		action->actionType = EActionType::CANCEL;
+		action->stackNumber = getActiveStack()->ID;
+		owner.givenCommand.setn(action);
+		setActiveStack(nullptr);
+	}
+}
+
+void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos)
+{
+	owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+		// remove any potentially erased petrification effect
+		removeExpiredColorFilters();
+	});
+
+	for(auto & attackedInfo : attackedInfos)
+	{
+		if (!attackedInfo.attacker)
+			continue;
+
+		// In H3, attacked stack will not reverse on ranged attack
+		if (attackedInfo.indirectAttack)
+			continue;
+
+		// Another type of indirect attack - dragon breath
+		if (!CStack::isMeleeAttackPossible(attackedInfo.attacker, attackedInfo.defender))
+			continue;
+
+		// defender need to face in direction opposited to out attacker
+		bool needsReverse = shouldAttackFacingRight(attackedInfo.attacker, attackedInfo.defender) == facingRight(attackedInfo.defender);
+
+		// FIXME: this check is better, however not usable since stacksAreAttacked is called after net pack is applyed - petrification is already removed
+		// if (needsReverse && !attackedInfo.defender->isFrozen())
+		if (needsReverse && stackAnimation[attackedInfo.defender->ID]->getType() != ECreatureAnimType::FROZEN)
+		{
+			owner.executeOnAnimationCondition(EAnimationEvents::MOVEMENT, true, [=]()
+			{
+				addNewAnim(new ReverseAnimation(owner, attackedInfo.defender, attackedInfo.defender->getPosition()));
+			});
+		}
+	}
+
+	for(auto & attackedInfo : attackedInfos)
+	{
+		bool useDeathAnim   = attackedInfo.killed;
+		bool useDefenceAnim = attackedInfo.defender->defendingAnim && !attackedInfo.indirectAttack && !attackedInfo.killed;
+
+		EAnimationEvents usedEvent = useDefenceAnim ? EAnimationEvents::ATTACK : EAnimationEvents::HIT;
+
+		owner.executeOnAnimationCondition(usedEvent, true, [=]()
+		{
+			if (useDeathAnim)
+				addNewAnim(new DeathAnimation(owner, attackedInfo.defender, attackedInfo.indirectAttack));
+			else if(useDefenceAnim)
+				addNewAnim(new DefenceAnimation(owner, attackedInfo.defender));
+			else
+				addNewAnim(new HittedAnimation(owner, attackedInfo.defender));
+
+			if (attackedInfo.fireShield)
+				owner.effectsController->displayEffect(EBattleEffect::FIRE_SHIELD, "FIRESHIE", attackedInfo.attacker->getPosition());
+
+			if (attackedInfo.spellEffect != SpellID::NONE)
+			{
+				auto spell = attackedInfo.spellEffect.toSpell();
+				if (!spell->getCastSound().empty())
+					CCS->soundh->playSound(spell->getCastSound());
+
+
+				owner.displaySpellEffect(spell, attackedInfo.defender->getPosition());
+			}
+		});
+	}
+
+	for (auto & attackedInfo : attackedInfos)
+	{
+		if (attackedInfo.rebirth)
+		{
+			owner.executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=](){
+				owner.effectsController->displayEffect(EBattleEffect::RESURRECT, "RESURECT", attackedInfo.defender->getPosition());
+				addNewAnim(new ResurrectionAnimation(owner, attackedInfo.defender));
+			});
+		}
+
+		if (attackedInfo.killed && attackedInfo.defender->summoned)
+		{
+			owner.executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=](){
+				addNewAnim(new ColorTransformAnimation(owner, attackedInfo.defender, "summonFadeOut", nullptr));
+				stackRemoved(attackedInfo.defender->ID);
+			});
+		}
+	}
+	executeAttackAnimations();
+}
+
+void BattleStacksController::stackTeleported(const CStack *stack, std::vector<BattleHex> destHex, int distance)
+{
+	assert(destHex.size() > 0);
+	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+
+	owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+		addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeOut", nullptr) );
+	});
+
+	owner.executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=](){
+		stackAnimation[stack->ID]->pos.moveTo(getStackPositionAtHex(destHex.back(), stack));
+		addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeIn", nullptr) );
+	});
+
+	// animations will be executed by spell
+}
+
+void BattleStacksController::stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance)
+{
+	assert(destHex.size() > 0);
+	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+
+	bool stackTeleports = stack->hasBonus(Selector::typeSubtype(Bonus::FLYING, 1));
+	owner.setAnimationCondition(EAnimationEvents::MOVEMENT, true);
+
+	auto enqueMoveEnd = [&](){
+		addNewAnim(new MovementEndAnimation(owner, stack, destHex.back()));
+		owner.executeOnAnimationCondition(EAnimationEvents::ACTION, false, [&](){
+			owner.setAnimationCondition(EAnimationEvents::MOVEMENT, false);
+		});
+	};
+
+	auto enqueMove = [&](){
+		if (!stackTeleports)
+		{
+			addNewAnim(new MovementAnimation(owner, stack, destHex, distance));
+			owner.executeOnAnimationCondition(EAnimationEvents::ACTION, false, enqueMoveEnd);
+		}
+		else
+			enqueMoveEnd();
+	};
+
+	auto enqueMoveStart = [&](){
+		addNewAnim(new MovementStartAnimation(owner, stack));
+		owner.executeOnAnimationCondition(EAnimationEvents::ACTION, false, enqueMove);
+	};
+
+	if(shouldRotate(stack, stack->getPosition(), destHex[0]))
+	{
+		addNewAnim(new ReverseAnimation(owner, stack, stack->getPosition()));
+		owner.executeOnAnimationCondition(EAnimationEvents::ACTION, false, enqueMoveStart);
+	}
+	else
+		enqueMoveStart();
+
+	owner.waitForAnimationCondition(EAnimationEvents::MOVEMENT, false);
+}
+
+bool BattleStacksController::shouldAttackFacingRight(const CStack * attacker, const CStack * defender)
+{
+	bool mustReverse = owner.curInt->cb->isToReverse(
+				attacker->getPosition(),
+				attacker,
+				defender);
+
+	if (attacker->side == BattleSide::ATTACKER)
+		return !mustReverse;
+	else
+		return mustReverse;
+}
+
+void BattleStacksController::stackAttacking( const StackAttackInfo & info )
+{
+	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+
+	auto attacker    = info.attacker;
+	auto defender    = info.defender;
+	auto tile        = info.tile;
+	auto spellEffect = info.spellEffect;
+	auto multiAttack = !info.secondaryDefender.empty();
+	bool needsReverse = false;
+
+	if (info.indirectAttack)
+	{
+		needsReverse = shouldRotate(attacker, attacker->position, info.tile);
+	}
+	else
+	{
+		needsReverse = shouldAttackFacingRight(attacker, defender) != facingRight(attacker);
+	}
+
+	if (needsReverse)
+	{
+		owner.executeOnAnimationCondition(EAnimationEvents::MOVEMENT, true, [=]()
+		{
+			addNewAnim(new ReverseAnimation(owner, attacker, attacker->getPosition()));
+		});
+	}
+
+	if(info.lucky)
+	{
+		owner.executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() {
+			owner.appendBattleLog(info.attacker->formatGeneralMessage(-45));
+			owner.effectsController->displayEffect(EBattleEffect::GOOD_LUCK, "GOODLUCK", attacker->getPosition());
+		});
+	}
+
+	if(info.unlucky)
+	{
+		owner.executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() {
+			owner.appendBattleLog(info.attacker->formatGeneralMessage(-44));
+			owner.effectsController->displayEffect(EBattleEffect::BAD_LUCK, "BADLUCK", attacker->getPosition());
+		});
+	}
+
+	if(info.deathBlow)
+	{
+		owner.executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() {
+			owner.appendBattleLog(info.attacker->formatGeneralMessage(365));
+			owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, "DEATHBLO", defender->getPosition());
+		});
+
+		for(auto elem : info.secondaryDefender)
+		{
+			owner.executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() {
+				owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, elem->getPosition());
+			});
+		}
+	}
+
+	owner.executeOnAnimationCondition(EAnimationEvents::ATTACK, true, [=]()
+	{
+		if (info.indirectAttack)
+		{
+			addNewAnim(new ShootingAnimation(owner, attacker, tile, defender));
+		}
+		else
+		{
+			addNewAnim(new MeleeAttackAnimation(owner, attacker, tile, defender, multiAttack));
+		}
+	});
+
+	if (info.spellEffect != SpellID::NONE)
+	{
+		owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=]()
+		{
+			owner.displaySpellHit(spellEffect.toSpell(), tile);
+		});
+	}
+
+	if (info.lifeDrain)
+	{
+		owner.executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=]()
+		{
+			owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, "DRAINLIF", attacker->getPosition());
+		});
+	}
+
+	//return, animation playback will be handled by stacksAreAttacked
+}
+
+void BattleStacksController::executeAttackAnimations()
+{
+	owner.setAnimationCondition(EAnimationEvents::MOVEMENT, true);
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.setAnimationCondition(EAnimationEvents::MOVEMENT, false);
+
+	owner.setAnimationCondition(EAnimationEvents::BEFORE_HIT, true);
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.setAnimationCondition(EAnimationEvents::BEFORE_HIT, false);
+
+	owner.setAnimationCondition(EAnimationEvents::ATTACK, true);
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.setAnimationCondition(EAnimationEvents::ATTACK, false);
+
+	// Note that HIT event can also be emitted by attack animation
+	owner.setAnimationCondition(EAnimationEvents::HIT, true);
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.setAnimationCondition(EAnimationEvents::HIT, false);
+
+	owner.setAnimationCondition(EAnimationEvents::AFTER_HIT, true);
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.setAnimationCondition(EAnimationEvents::AFTER_HIT, false);
+
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+}
+
+bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const
+{
+	Point begPosition = getStackPositionAtHex(oldPos,stack);
+	Point endPosition = getStackPositionAtHex(nextHex, stack);
+
+	if((begPosition.x > endPosition.x) && facingRight(stack))
+		return true;
+	else if((begPosition.x < endPosition.x) && !facingRight(stack))
+		return true;
+
+	return false;
+}
+
+void BattleStacksController::endAction(const BattleAction* action)
+{
+	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+
+	//check if we should reverse stacks
+	TStacks stacks = owner.curInt->cb->battleGetStacks(CBattleCallback::MINE_AND_ENEMY);
+
+	for (const CStack *s : stacks)
+	{
+		bool shouldFaceRight  = s && s->side == BattleSide::ATTACKER;
+
+		if (s && facingRight(s) != shouldFaceRight && s->alive() && stackAnimation[s->ID]->isIdle())
+		{
+			addNewAnim(new ReverseAnimation(owner, s, s->getPosition()));
+		}
+	}
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+
+	//Ensure that all animation flags were reset
+	assert(owner.getAnimationCondition(EAnimationEvents::OPENING) == false);
+	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+	assert(owner.getAnimationCondition(EAnimationEvents::MOVEMENT) == false);
+	assert(owner.getAnimationCondition(EAnimationEvents::ATTACK) == false);
+	assert(owner.getAnimationCondition(EAnimationEvents::HIT) == false);
+
+	owner.windowObject->blockUI(activeStack == nullptr);
+	removeExpiredColorFilters();
+}
+
+void BattleStacksController::startAction(const BattleAction* action)
+{
+	removeExpiredColorFilters();
+}
+
+void BattleStacksController::stackActivated(const CStack *stack)
+{
+	stackToActivate = stack;
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.activateStack();
+}
+
+void BattleStacksController::activateStack()
+{
+	if ( !currentAnimations.empty())
+		return;
+
+	if ( !stackToActivate)
+		return;
+
+	owner.trySetActivePlayer(stackToActivate->owner);
+
+	setActiveStack(stackToActivate);
+	stackToActivate = nullptr;
+
+	const CStack * s = getActiveStack();
+	if(!s)
+		return;
+
+	//set casting flag to true if creature can use it to not check it every time
+	const auto spellcaster = s->getBonusLocalFirst(Selector::type()(Bonus::SPELLCASTER));
+	const auto randomSpellcaster = s->getBonusLocalFirst(Selector::type()(Bonus::RANDOM_SPELLCASTER));
+	if(s->canCast() && (spellcaster || randomSpellcaster))
+	{
+		stackCanCastSpell = true;
+		if(randomSpellcaster)
+			creatureSpellToCast = -1; //spell will be set later on cast
+		else
+			creatureSpellToCast = owner.curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), s, CBattleInfoCallback::RANDOM_AIMED); //faerie dragon can cast only one spell until their next move
+		//TODO: what if creature can cast BOTH random genie spell and aimed spell?
+		//TODO: faerie dragon type spell should be selected by server
+	}
+	else
+	{
+		stackCanCastSpell = false;
+		creatureSpellToCast = -1;
+	}
+}
+
+void BattleStacksController::setSelectedStack(const CStack *stack)
+{
+	selectedStack = stack;
+}
+
+const CStack* BattleStacksController::getSelectedStack() const
+{
+	return selectedStack;
+}
+
+const CStack* BattleStacksController::getActiveStack() const
+{
+	return activeStack;
+}
+
+bool BattleStacksController::facingRight(const CStack * stack) const
+{
+	return stackFacingRight.at(stack->ID);
+}
+
+bool BattleStacksController::activeStackSpellcaster()
+{
+	return stackCanCastSpell;
+}
+
+SpellID BattleStacksController::activeStackSpellToCast()
+{
+	if (!stackCanCastSpell)
+		return SpellID::NONE;
+	return SpellID(creatureSpellToCast);
+}
+
+Point BattleStacksController::getStackPositionAtHex(BattleHex hexNum, const CStack * stack) const
+{
+	Point ret(-500, -500); //returned value
+	if(stack && stack->initialPosition < 0) //creatures in turrets
+		return owner.siegeController->getTurretCreaturePosition(stack->initialPosition);
+
+	static const Point basePos(-190, -139); // position of creature in topleft corner
+	static const int imageShiftX = 30; // X offset to base pos for facing right stacks, negative for facing left
+
+	ret.x = basePos.x + 22 * ( (hexNum.getY() + 1)%2 ) + 44 * hexNum.getX();
+	ret.y = basePos.y + 42 * hexNum.getY();
+
+	if (stack)
+	{
+		if(facingRight(stack))
+			ret.x += imageShiftX;
+		else
+			ret.x -= imageShiftX;
+
+		//shifting position for double - hex creatures
+		if(stack->doubleWide())
+		{
+			if(stack->side == BattleSide::ATTACKER)
+			{
+				if(facingRight(stack))
+					ret.x -= 44;
+			}
+			else
+			{
+				if(!facingRight(stack))
+					ret.x += 44;
+			}
+		}
+	}
+	//returning
+	return ret;
+}
+
+void BattleStacksController::setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell * source, bool persistent)
+{
+	for (auto & filter : stackFilterEffects)
+	{
+		if (filter.target == target && filter.source == source)
+		{
+			filter.effect     = effect;
+			filter.persistent = persistent;
+			return;
+		}
+	}
+	stackFilterEffects.push_back({ effect, target, source, persistent });
+}
+
+void BattleStacksController::removeExpiredColorFilters()
+{
+	vstd::erase_if(stackFilterEffects, [&](const BattleStackFilterEffect & filter)
+	{
+		if (!filter.persistent)
+		{
+			if (filter.source && !filter.target->hasBonus(Selector::source(Bonus::SPELL_EFFECT, filter.source->id), Selector::all))
+				return true;
+			if (filter.effect == ColorFilter::genEmptyShifter())
+				return true;
+		}
+		return false;
+	});
+}
+
+void BattleStacksController::updateHoveredStacks()
+{
+	auto newStacks = selectHoveredStacks();
+
+	for (auto const * stack : mouseHoveredStacks)
+	{
+		if (vstd::contains(newStacks, stack))
+			continue;
+
+		if (stack == activeStack)
+			stackAnimation[stack->ID]->setBorderColor(AnimationControls::getGoldBorder());
+		else
+			stackAnimation[stack->ID]->setBorderColor(AnimationControls::getNoBorder());
+	}
+
+	for (auto const * stack : newStacks)
+	{
+		if (vstd::contains(mouseHoveredStacks, stack))
+			continue;
+
+		stackAnimation[stack->ID]->setBorderColor(AnimationControls::getBlueBorder());
+		if (stackAnimation[stack->ID]->framesInGroup(ECreatureAnimType::MOUSEON) > 0 && stack->alive() && !stack->isFrozen())
+			stackAnimation[stack->ID]->playOnce(ECreatureAnimType::MOUSEON);
+
+	}
+
+	mouseHoveredStacks = newStacks;
+}
+
+std::vector<const CStack *> BattleStacksController::selectHoveredStacks()
+{
+	// only allow during our turn - do not try to highlight creatures while they are in the middle of actions
+	if (!activeStack)
+		return {};
+
+	if(owner.getAnimationCondition(EAnimationEvents::ACTION) == true)
+		return {};
+
+	auto hoveredHex = owner.fieldController->getHoveredHex();
+
+	if (!hoveredHex.isValid())
+		return {};
+
+	const spells::Caster *caster = nullptr;
+	const CSpell *spell = nullptr;
+
+	spells::Mode mode = spells::Mode::HERO;
+
+	if(owner.actionsController->spellcastingModeActive())//hero casts spell
+	{
+		spell = owner.actionsController->selectedSpell().toSpell();
+		caster = owner.getActiveHero();
+	}
+	else if(owner.stacksController->activeStackSpellToCast() != SpellID::NONE)//stack casts spell
+	{
+		spell = SpellID(owner.stacksController->activeStackSpellToCast()).toSpell();
+		caster = owner.stacksController->getActiveStack();
+		mode = spells::Mode::CREATURE_ACTIVE;
+	}
+
+	if(caster && spell) //when casting spell
+	{
+		spells::Target target;
+		target.emplace_back(hoveredHex);
+
+		spells::BattleCast event(owner.curInt->cb.get(), caster, mode, spell);
+		auto mechanics = spell->battleMechanics(&event);
+		return mechanics->getAffectedStacks(target);
+	}
+
+	if(hoveredHex.isValid())
+	{
+		const CStack * const stack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true);
+
+		if (stack)
+			return {stack};
+	}
+
+	return {};
+}

+ 154 - 0
client/battle/BattleStacksController.h

@@ -0,0 +1,154 @@
+/*
+ * BattleStacksController.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../gui/Geometries.h"
+#include "../gui/ColorFilter.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct BattleHex;
+class BattleAction;
+class CStack;
+class CSpell;
+class SpellID;
+
+VCMI_LIB_NAMESPACE_END
+
+struct StackAttackedInfo;
+struct StackAttackInfo;
+
+class ColorFilter;
+class Canvas;
+class BattleInterface;
+class BattleAnimation;
+class CreatureAnimation;
+class BattleAnimation;
+class BattleRenderer;
+class IImage;
+
+struct BattleStackFilterEffect
+{
+	ColorFilter effect;
+	const CStack * target;
+	const CSpell * source;
+	bool persistent;
+};
+
+/// Class responsible for handling stacks in battle
+/// Handles ordering of stacks animation
+/// As well as rendering of stacks, their amount boxes
+/// And any other effect applied to stacks
+class BattleStacksController
+{
+	BattleInterface & owner;
+
+	std::shared_ptr<IImage> amountNormal;
+	std::shared_ptr<IImage> amountNegative;
+	std::shared_ptr<IImage> amountPositive;
+	std::shared_ptr<IImage> amountEffNeutral;
+
+	/// currently displayed animations <anim, initialized>
+	std::vector<BattleAnimation *> currentAnimations;
+
+	/// currently active color effects on stacks, in order of their addition (which corresponds to their apply order)
+	std::vector<BattleStackFilterEffect> stackFilterEffects;
+
+	/// animations of creatures from fighting armies (order by BattleInfo's stacks' ID)
+	std::map<int32_t, std::shared_ptr<CreatureAnimation>> stackAnimation;
+
+	/// <creatureID, if false reverse creature's animation> //TODO: move it to battle callback
+	std::map<int, bool> stackFacingRight;
+
+	/// currently active stack; nullptr - no one
+	const CStack *activeStack;
+
+	/// stacks below mouse pointer (multiple stacks possible while spellcasting), used for border animation
+	std::vector<const CStack *> mouseHoveredStacks;
+
+	///when animation is playing, we should wait till the end to make the next stack active; nullptr of none
+	const CStack *stackToActivate;
+
+	/// stack that was selected for multi-target spells - Teleport / Sacrifice
+	const CStack *selectedStack;
+
+	/// if true, active stack could possibly cast some target spell
+	bool stackCanCastSpell;
+	si32 creatureSpellToCast;
+
+	/// for giving IDs for animations
+	ui32 animIDhelper;
+
+	bool stackNeedsAmountBox(const CStack * stack) const;
+	void showStackAmountBox(Canvas & canvas, const CStack * stack);
+	BattleHex getStackCurrentPosition(const CStack * stack) const;
+
+	std::shared_ptr<IImage> getStackAmountBox(const CStack * stack);
+
+	void executeAttackAnimations();
+	void removeExpiredColorFilters();
+
+	void initializeBattleAnimations();
+	void stepFrameBattleAnimations();
+
+	void updateBattleAnimations();
+	void updateHoveredStacks();
+
+	std::vector<const CStack *> selectHoveredStacks();
+
+	bool shouldAttackFacingRight(const CStack * attacker, const CStack * defender);
+
+public:
+	BattleStacksController(BattleInterface & owner);
+
+	bool shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const;
+	bool facingRight(const CStack * stack) const;
+
+	void stackReset(const CStack * stack);
+	void stackAdded(const CStack * stack, bool instant); //new stack appeared on battlefield
+	void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled
+	void stackActivated(const CStack *stack); //active stack has been changed
+	void stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance); //stack with id number moved to destHex
+	void stackTeleported(const CStack *stack, std::vector<BattleHex> destHex, int distance); //stack with id number moved to destHex
+	void stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos); //called when a certain amount of stacks has been attacked
+	void stackAttacking(const StackAttackInfo & info); //called when stack with id ID is attacking something on hex dest
+
+	void startAction(const BattleAction* action);
+	void endAction(const BattleAction* action);
+
+	bool activeStackSpellcaster();
+	SpellID activeStackSpellToCast();
+
+	void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all
+
+	void setActiveStack(const CStack *stack);
+	void setSelectedStack(const CStack *stack);
+
+	void showAliveStack(Canvas & canvas, const CStack * stack);
+	void showStack(Canvas & canvas, const CStack * stack);
+
+	void collectRenderableObjects(BattleRenderer & renderer);
+
+	/// Adds new color filter effect targeting stack
+	/// Effect will last as long as stack is affected by specified spell (unless effect is persistent)
+	/// If effect from same (target, source) already exists, it will be updated
+	void setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell *source, bool persistent);
+	void addNewAnim(BattleAnimation *anim); //adds new anim to pendingAnims
+
+	const CStack* getActiveStack() const;
+	const CStack* getSelectedStack() const;
+
+	void update();
+
+	/// returns position of animation needed to place stack in specific hex
+	Point getStackPositionAtHex(BattleHex hexNum, const CStack * creature) const;
+
+	friend class BattleAnimation; // for exposing pendingAnims/creAnims/creDir to animations
+};

+ 572 - 0
client/battle/BattleWindow.cpp

@@ -0,0 +1,572 @@
+/*
+ * BattleWindow.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 "BattleWindow.h"
+
+#include "BattleInterface.h"
+#include "BattleInterfaceClasses.h"
+#include "BattleFieldController.h"
+#include "BattleStacksController.h"
+#include "BattleActionsController.h"
+
+#include "../CGameInfo.h"
+#include "../CMessage.h"
+#include "../CPlayerInterface.h"
+#include "../CMusicHandler.h"
+#include "../gui/Canvas.h"
+#include "../gui/CCursorHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/CAnimation.h"
+#include "../windows/CSpellWindow.h"
+#include "../widgets/AdventureMapClasses.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/Images.h"
+
+#include "../../CCallback.h"
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/CStack.h"
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/filesystem/ResourceID.h"
+
+BattleWindow::BattleWindow(BattleInterface & owner):
+	owner(owner)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+	pos.w = 800;
+	pos.h = 600;
+	pos = center();
+
+	REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole);
+	
+	const JsonNode config(ResourceID("config/widgets/BattleWindow.json"));
+	
+	addCallback("options", std::bind(&BattleWindow::bOptionsf, this));
+	addCallback("surrender", std::bind(&BattleWindow::bSurrenderf, this));
+	addCallback("flee", std::bind(&BattleWindow::bFleef, this));
+	addCallback("autofight", std::bind(&BattleWindow::bAutofightf, this));
+	addCallback("spellbook", std::bind(&BattleWindow::bSpellf, this));
+	addCallback("wait", std::bind(&BattleWindow::bWaitf, this));
+	addCallback("defence", std::bind(&BattleWindow::bDefencef, this));
+	addCallback("consoleUp", std::bind(&BattleWindow::bConsoleUpf, this));
+	addCallback("consoleDown", std::bind(&BattleWindow::bConsoleDownf, this));
+	addCallback("tacticNext", std::bind(&BattleWindow::bTacticNextStack, this));
+	addCallback("tacticEnd", std::bind(&BattleWindow::bTacticPhaseEnd, this));
+	addCallback("alternativeAction", std::bind(&BattleWindow::bSwitchActionf, this));
+	
+	build(config);
+	
+	console = widget<BattleConsole>("console");
+
+	GH.statusbar = console;
+	owner.console = console;
+
+	owner.fieldController.reset( new BattleFieldController(owner));
+	owner.fieldController->createHeroes();
+
+	//create stack queue and adjust our own position
+	bool embedQueue;
+	std::string queueSize = settings["battle"]["queueSize"].String();
+
+	if(queueSize == "auto")
+		embedQueue = screen->h < 700;
+	else
+		embedQueue = screen->h < 700 || queueSize == "small";
+
+	queue = std::make_shared<StackQueue>(embedQueue, owner);
+	if(!embedQueue && settings["battle"]["showQueue"].Bool())
+	{
+		//re-center, taking into account stack queue position
+		pos.y -= queue->pos.h;
+		pos.h += queue->pos.h;
+		pos = center();
+	}
+
+	if ( owner.tacticsMode )
+		tacticPhaseStarted();
+	else
+		tacticPhaseEnded();
+
+	addUsedEvents(RCLICK | KEYBOARD);
+}
+
+BattleWindow::~BattleWindow()
+{
+	CPlayerInterface::battleInt = nullptr;
+}
+
+std::shared_ptr<BattleConsole> BattleWindow::buildBattleConsole(const JsonNode & config) const
+{
+	auto rect = readRect(config["rect"]);
+	auto offset = readPosition(config["imagePosition"]);
+	auto background = widget<CPicture>("menuBattle");
+	return std::make_shared<BattleConsole>(background, rect.topLeft(), offset, rect.dimensions() );
+}
+
+void BattleWindow::hideQueue()
+{
+	Settings showQueue = settings.write["battle"]["showQueue"];
+	showQueue->Bool() = false;
+
+	queue->disable();
+
+	if (!queue->embedded)
+	{
+		//re-center, taking into account stack queue position
+		pos.y += queue->pos.h;
+		pos.h -= queue->pos.h;
+		pos = center();
+		GH.totalRedraw();
+	}
+}
+
+void BattleWindow::showQueue()
+{
+	Settings showQueue = settings.write["battle"]["showQueue"];
+	showQueue->Bool() = true;
+
+	queue->enable();
+
+	if (!queue->embedded)
+	{
+		//re-center, taking into account stack queue position
+		pos.y -= queue->pos.h;
+		pos.h += queue->pos.h;
+		pos = center();
+		GH.totalRedraw();
+	}
+}
+
+void BattleWindow::updateQueue()
+{
+	queue->update();
+}
+
+void BattleWindow::activate()
+{
+	GH.statusbar = console;
+	CIntObject::activate();
+	LOCPLINT->cingconsole->activate();
+}
+
+void BattleWindow::deactivate()
+{
+	CIntObject::deactivate();
+	LOCPLINT->cingconsole->deactivate();
+}
+
+void BattleWindow::keyPressed(const SDL_KeyboardEvent & key)
+{
+	if(key.keysym.sym == SDLK_q && key.state == SDL_PRESSED)
+	{
+		if(settings["battle"]["showQueue"].Bool()) //hide queue
+			hideQueue();
+		else
+			showQueue();
+
+	}
+	else if(key.keysym.sym == SDLK_f && key.state == SDL_PRESSED)
+	{
+		owner.actionsController->enterCreatureCastingMode();
+	}
+	else if(key.keysym.sym == SDLK_ESCAPE)
+	{
+		if(owner.getAnimationCondition(EAnimationEvents::OPENING) == true)
+			CCS->soundh->stopSound(owner.battleIntroSoundChannel);
+		else
+			owner.actionsController->endCastingSpell();
+	}
+}
+
+void BattleWindow::clickRight(tribool down, bool previousState)
+{
+	if (!down)
+		owner.actionsController->endCastingSpell();
+}
+
+void BattleWindow::tacticPhaseStarted()
+{
+	auto menuBattle = widget<CIntObject>("menuBattle");
+	auto console = widget<CIntObject>("console");
+	auto menuTactics = widget<CIntObject>("menuTactics");
+	auto tacticNext = widget<CIntObject>("tacticNext");
+	auto tacticEnd = widget<CIntObject>("tacticEnd");
+
+	menuBattle->disable();
+	console->disable();
+
+	menuTactics->enable();
+	tacticNext->enable();
+	tacticEnd->enable();
+
+	redraw();
+}
+
+void BattleWindow::tacticPhaseEnded()
+{
+	auto menuBattle = widget<CIntObject>("menuBattle");
+	auto console = widget<CIntObject>("console");
+	auto menuTactics = widget<CIntObject>("menuTactics");
+	auto tacticNext = widget<CIntObject>("tacticNext");
+	auto tacticEnd = widget<CIntObject>("tacticEnd");
+
+	menuBattle->enable();
+	console->enable();
+
+	menuTactics->disable();
+	tacticNext->disable();
+	tacticEnd->disable();
+
+	redraw();
+}
+
+void BattleWindow::bOptionsf()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	CCS->curh->set(Cursor::Map::POINTER);
+
+	GH.pushIntT<BattleOptionsWindow>(owner);
+}
+
+void BattleWindow::bSurrenderf()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	int cost = owner.curInt->cb->battleGetSurrenderCost();
+	if(cost >= 0)
+	{
+		std::string enemyHeroName = owner.curInt->cb->battleGetEnemyHero().name;
+		if(enemyHeroName.empty())
+		{
+			logGlobal->warn("Surrender performed without enemy hero, should not happen!");
+			enemyHeroName = "#ENEMY#";
+		}
+
+		std::string surrenderMessage = boost::str(boost::format(CGI->generaltexth->allTexts[32]) % enemyHeroName % cost); //%s states: "I will accept your surrender and grant you and your troops safe passage for the price of %d gold."
+		owner.curInt->showYesNoDialog(surrenderMessage, [this](){ reallySurrender(); }, nullptr);
+	}
+}
+
+void BattleWindow::bFleef()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	if ( owner.curInt->cb->battleCanFlee() )
+	{
+		CFunctionList<void()> ony = std::bind(&BattleWindow::reallyFlee,this);
+		owner.curInt->showYesNoDialog(CGI->generaltexth->allTexts[28], ony, nullptr); //Are you sure you want to retreat?
+	}
+	else
+	{
+		std::vector<std::shared_ptr<CComponent>> comps;
+		std::string heroName;
+		//calculating fleeing hero's name
+		if (owner.attackingHeroInstance)
+			if (owner.attackingHeroInstance->tempOwner == owner.curInt->cb->getMyColor())
+				heroName = owner.attackingHeroInstance->name;
+		if (owner.defendingHeroInstance)
+			if (owner.defendingHeroInstance->tempOwner == owner.curInt->cb->getMyColor())
+				heroName = owner.defendingHeroInstance->name;
+		//calculating text
+		auto txt = boost::format(CGI->generaltexth->allTexts[340]) % heroName; //The Shackles of War are present.  %s can not retreat!
+
+		//printing message
+		owner.curInt->showInfoDialog(boost::to_string(txt), comps);
+	}
+}
+
+void BattleWindow::reallyFlee()
+{
+	owner.giveCommand(EActionType::RETREAT);
+	CCS->curh->set(Cursor::Map::POINTER);
+}
+
+void BattleWindow::reallySurrender()
+{
+	if (owner.curInt->cb->getResourceAmount(Res::GOLD) < owner.curInt->cb->battleGetSurrenderCost())
+	{
+		owner.curInt->showInfoDialog(CGI->generaltexth->allTexts[29]); //You don't have enough gold!
+	}
+	else
+	{
+		owner.giveCommand(EActionType::SURRENDER);
+		CCS->curh->set(Cursor::Map::POINTER);
+	}
+}
+
+void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action)
+{
+	auto w = widget<CButton>("alternativeAction");
+	if(!w)
+		return;
+	
+	std::string iconName = variables["actionIconDefault"].String();
+	switch(action)
+	{
+		case PossiblePlayerBattleAction::ATTACK:
+			iconName = variables["actionIconAttack"].String();
+			break;
+			
+		case PossiblePlayerBattleAction::SHOOT:
+			iconName = variables["actionIconShoot"].String();
+			break;
+			
+		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+			iconName = variables["actionIconSpell"].String();
+			break;
+			
+		//TODO: figure out purpose of this icon
+		//case PossiblePlayerBattleAction::???:
+			//iconName = variables["actionIconWalk"].String();
+			//break;
+			
+		case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
+			iconName = variables["actionIconReturn"].String();
+			break;
+			
+		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
+			iconName = variables["actionIconNoReturn"].String();
+			break;
+	}
+		
+	auto anim = std::make_shared<CAnimation>(iconName);
+	w->setImage(anim, false);
+}
+
+void BattleWindow::setAlternativeActions(const std::list<PossiblePlayerBattleAction> & actions)
+{
+	alternativeActions = actions;
+	defaultAction = PossiblePlayerBattleAction::INVALID;
+	if(alternativeActions.size() > 1)
+		defaultAction = alternativeActions.back();
+	if(!alternativeActions.empty())
+		showAlternativeActionIcon(alternativeActions.front());
+	else
+		showAlternativeActionIcon(defaultAction);
+}
+
+void BattleWindow::bAutofightf()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	//Stop auto-fight mode
+	if(owner.curInt->isAutoFightOn)
+	{
+		assert(owner.curInt->autofightingAI);
+		owner.curInt->isAutoFightOn = false;
+		logGlobal->trace("Stopping the autofight...");
+	}
+	else if(!owner.curInt->autofightingAI)
+	{
+		owner.curInt->isAutoFightOn = true;
+		blockUI(true);
+
+		auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
+		ai->initBattleInterface(owner.curInt->env, owner.curInt->cb);
+		ai->battleStart(owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.curInt->cb->battleGetMySide());
+		owner.curInt->autofightingAI = ai;
+		owner.curInt->cb->registerBattleInterface(ai);
+
+		owner.requestAutofightingAIToTakeAction();
+	}
+}
+
+void BattleWindow::bSpellf()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	if (!owner.myTurn)
+		return;
+
+	auto myHero = owner.currentHero();
+	if(!myHero)
+		return;
+
+	CCS->curh->set(Cursor::Map::POINTER);
+
+	ESpellCastProblem::ESpellCastProblem spellCastProblem = owner.curInt->cb->battleCanCastSpell(myHero, spells::Mode::HERO);
+
+	if(spellCastProblem == ESpellCastProblem::OK)
+	{
+		GH.pushIntT<CSpellWindow>(myHero, owner.curInt.get());
+	}
+	else if (spellCastProblem == ESpellCastProblem::MAGIC_IS_BLOCKED)
+	{
+		//TODO: move to spell mechanics, add more information to spell cast problem
+		//Handle Orb of Inhibition-like effects -> we want to display dialog with info, why casting is impossible
+		auto blockingBonus = owner.currentHero()->getBonusLocalFirst(Selector::type()(Bonus::BLOCK_ALL_MAGIC));
+		if (!blockingBonus)
+			return;
+
+		if (blockingBonus->source == Bonus::ARTIFACT)
+		{
+			const auto artID = ArtifactID(blockingBonus->sid);
+			//If we have artifact, put name of our hero. Otherwise assume it's the enemy.
+			//TODO check who *really* is source of bonus
+			std::string heroName = myHero->hasArt(artID) ? myHero->name : owner.enemyHero().name;
+
+			//%s wields the %s, an ancient artifact which creates a p dead to all magic.
+			LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[683])
+										% heroName % CGI->artifacts()->getByIndex(artID)->getName()));
+		}
+	}
+}
+
+void BattleWindow::bSwitchActionf()
+{
+	if(alternativeActions.empty())
+		return;
+	
+	if(alternativeActions.front() == defaultAction)
+	{
+		alternativeActions.push_back(alternativeActions.front());
+		alternativeActions.pop_front();
+	}
+	
+	auto actions = owner.actionsController->getPossibleActions();
+	if(!actions.empty() && actions.front() == alternativeActions.front())
+	{
+		owner.actionsController->removePossibleAction(alternativeActions.front());
+		showAlternativeActionIcon(defaultAction);
+	}
+	else
+	{
+		owner.actionsController->pushFrontPossibleAction(alternativeActions.front());
+		showAlternativeActionIcon(alternativeActions.front());
+	}
+	
+	alternativeActions.push_back(alternativeActions.front());
+	alternativeActions.pop_front();
+}
+
+void BattleWindow::bWaitf()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	if (owner.stacksController->getActiveStack() != nullptr)
+		owner.giveCommand(EActionType::WAIT);
+}
+
+void BattleWindow::bDefencef()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	if (owner.stacksController->getActiveStack() != nullptr)
+		owner.giveCommand(EActionType::DEFEND);
+}
+
+void BattleWindow::bConsoleUpf()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	console->scrollUp();
+}
+
+void BattleWindow::bConsoleDownf()
+{
+	if (owner.actionsController->spellcastingModeActive())
+		return;
+
+	console->scrollDown();
+}
+
+void BattleWindow::bTacticNextStack()
+{
+	owner.tacticNextStack(nullptr);
+}
+
+void BattleWindow::bTacticPhaseEnd()
+{
+	owner.tacticPhaseEnd();
+}
+
+void BattleWindow::blockUI(bool on)
+{
+	bool canCastSpells = false;
+	auto hero = owner.curInt->cb->battleGetMyHero();
+
+	if(hero)
+	{
+		ESpellCastProblem::ESpellCastProblem spellcastingProblem = owner.curInt->cb->battleCanCastSpell(hero, spells::Mode::HERO);
+
+		//if magic is blocked, we leave button active, so the message can be displayed after button click
+		canCastSpells = spellcastingProblem == ESpellCastProblem::OK || spellcastingProblem == ESpellCastProblem::MAGIC_IS_BLOCKED;
+	}
+
+	bool canWait = owner.stacksController->getActiveStack() ? !owner.stacksController->getActiveStack()->waitedThisTurn : false;
+
+	if(auto w = widget<CButton>("options"))
+		w->block(on);
+	if(auto w = widget<CButton>("flee"))
+		w->block(on || !owner.curInt->cb->battleCanFlee());
+	if(auto w = widget<CButton>("surrender"))
+		w->block(on || owner.curInt->cb->battleGetSurrenderCost() < 0);
+	if(auto w = widget<CButton>("cast"))
+		w->block(on || owner.tacticsMode || !canCastSpells);
+	if(auto w = widget<CButton>("wait"))
+		w->block(on || owner.tacticsMode || !canWait);
+	if(auto w = widget<CButton>("defence"))
+		w->block(on || owner.tacticsMode);
+	if(auto w = widget<CButton>("alternativeAction"))
+		w->block(on || owner.tacticsMode);
+
+	// block only if during enemy turn and auto-fight is off
+	// otherwise - crash on accessing non-exisiting active stack
+	if(auto w = widget<CButton>("options"))
+		w->block(!owner.curInt->isAutoFightOn && !owner.stacksController->getActiveStack());
+
+	auto btactEnd = widget<CButton>("tacticEnd");
+	auto btactNext = widget<CButton>("tacticNext");
+	if(owner.tacticsMode && btactEnd && btactNext)
+	{
+		btactNext->block(on);
+		btactEnd->block(on);
+	}
+	else
+	{
+		auto bConsoleUp = widget<CButton>("consoleUp");
+		auto bConsoleDown = widget<CButton>("consoleDown");
+		if(bConsoleUp && bConsoleDown)
+		{
+			bConsoleUp->block(on);
+			bConsoleDown->block(on);
+		}
+	}
+}
+
+void BattleWindow::showAll(SDL_Surface *to)
+{
+	CIntObject::showAll(to);
+
+	if (screen->w != 800 || screen->h !=600)
+		CMessage::drawBorder(owner.curInt->playerID, to, pos.w+28, pos.h+29, pos.x-14, pos.y-15);
+}
+
+void BattleWindow::show(SDL_Surface *to)
+{
+	CIntObject::show(to);
+	LOCPLINT->cingconsole->show(to);
+}
+
+void BattleWindow::close()
+{
+	if(GH.topInt().get() != this)
+		logGlobal->error("Only top interface must be closed");
+	GH.popInts(1);
+}

+ 94 - 0
client/battle/BattleWindow.h

@@ -0,0 +1,94 @@
+/*
+ * BattleWindow.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../gui/CIntObject.h"
+#include "../gui/InterfaceObjectConfigurable.h"
+#include "../../lib/battle/CBattleInfoCallback.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+class CStack;
+
+VCMI_LIB_NAMESPACE_END
+
+class CButton;
+class BattleInterface;
+class BattleConsole;
+class BattleRenderer;
+class StackQueue;
+
+/// GUI object that handles functionality of panel at the bottom of combat screen
+class BattleWindow : public InterfaceObjectConfigurable
+{
+	BattleInterface & owner;
+
+	std::shared_ptr<StackQueue> queue;
+	std::shared_ptr<BattleConsole> console;
+
+	/// button press handling functions
+	void bOptionsf();
+	void bSurrenderf();
+	void bFleef();
+	void bAutofightf();
+	void bSpellf();
+	void bWaitf();
+	void bSwitchActionf();
+	void bDefencef();
+	void bConsoleUpf();
+	void bConsoleDownf();
+	void bTacticNextStack();
+	void bTacticPhaseEnd();
+
+	/// functions for handling actions after they were confirmed by popup window
+	void reallyFlee();
+	void reallySurrender();
+	
+	/// management of alternative actions
+	std::list<PossiblePlayerBattleAction> alternativeActions;
+	PossiblePlayerBattleAction defaultAction;
+	void showAlternativeActionIcon(PossiblePlayerBattleAction);
+
+	/// Toggle StackQueue visibility
+	void hideQueue();
+	void showQueue();
+
+	std::shared_ptr<BattleConsole> buildBattleConsole(const JsonNode &) const;
+
+public:
+	BattleWindow(BattleInterface & owner );
+	~BattleWindow();
+
+	/// Closes window once battle finished
+	void close();
+
+	/// block all UI elements when player is not allowed to act, e.g. during enemy turn
+	void blockUI(bool on);
+
+	/// Refresh queue after turn order changes
+	void updateQueue();
+
+	void activate() override;
+	void deactivate() override;
+	void keyPressed(const SDL_KeyboardEvent & key) override;
+	void clickRight(tribool down, bool previousState) override;
+	void show(SDL_Surface *to) override;
+	void showAll(SDL_Surface *to) override;
+
+	/// Toggle UI to displaying tactics phase
+	void tacticPhaseStarted();
+
+	/// Toggle UI to displaying battle log in place of tactics UI
+	void tacticPhaseEnded();
+
+	/// Set possible alternative options. If more than 1 - the last will be considered as default option
+	void setAlternativeActions(const std::list<PossiblePlayerBattleAction> &);
+
+};
+

+ 0 - 1229
client/battle/CBattleAnimations.cpp

@@ -1,1229 +0,0 @@
-/*
- * CBattleAnimations.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 "CBattleAnimations.h"
-
-#include <boost/math/constants/constants.hpp>
-
-#include "CBattleInterfaceClasses.h"
-#include "CBattleInterface.h"
-#include "CCreatureAnimation.h"
-
-#include "../CGameInfo.h"
-#include "../CMusicHandler.h"
-#include "../CPlayerInterface.h"
-#include "../Graphics.h"
-#include "../gui/CAnimation.h"
-#include "../gui/CCursorHandler.h"
-#include "../gui/CGuiHandler.h"
-#include "../gui/SDL_Extensions.h"
-
-#include "../../CCallback.h"
-#include "../../lib/CStack.h"
-#include "../../lib/CTownHandler.h"
-#include "../../lib/mapObjects/CGTownInstance.h"
-
-CBattleAnimation::CBattleAnimation(CBattleInterface * _owner)
-	: owner(_owner), ID(_owner->animIDhelper++)
-{
-	logAnim->trace("Animation #%d created", ID);
-}
-
-CBattleAnimation::~CBattleAnimation()
-{
-	logAnim->trace("Animation #%d deleted", ID);
-}
-
-void CBattleAnimation::endAnim()
-{
-	logAnim->trace("Animation #%d ended, type is %s", ID, typeid(this).name());
-	for(auto & elem : owner->pendingAnims)
-	{
-		if(elem.first == this)
-		{
-			elem.first = nullptr;
-		}
-	}
-}
-
-bool CBattleAnimation::isEarliest(bool perStackConcurrency)
-{
-	int lowestMoveID = owner->animIDhelper + 5;
-	CBattleStackAnimation * thAnim = dynamic_cast<CBattleStackAnimation *>(this);
-	CEffectAnimation * thSen = dynamic_cast<CEffectAnimation *>(this);
-
-	for(auto & elem : owner->pendingAnims)
-	{
-
-		CBattleStackAnimation * stAnim = dynamic_cast<CBattleStackAnimation *>(elem.first);
-		CEffectAnimation * sen = dynamic_cast<CEffectAnimation *>(elem.first);
-		if(perStackConcurrency && stAnim && thAnim && stAnim->stack->ID != thAnim->stack->ID)
-			continue;
-
-		if(perStackConcurrency && sen && thSen && sen != thSen)
-			continue;
-
-		CReverseAnimation * revAnim = dynamic_cast<CReverseAnimation *>(stAnim);
-
-		if(revAnim && thAnim && stAnim && stAnim->stack->ID == thAnim->stack->ID && revAnim->priority)
-			return false;
-
-		if(elem.first)
-			vstd::amin(lowestMoveID, elem.first->ID);
-	}
-	return (ID == lowestMoveID) || (lowestMoveID == (owner->animIDhelper + 5));
-}
-
-CBattleStackAnimation::CBattleStackAnimation(CBattleInterface * owner, const CStack * stack)
-	: CBattleAnimation(owner),
-	  myAnim(owner->creAnims[stack->ID]),
-	  stack(stack)
-{
-	assert(myAnim);
-}
-
-void CBattleStackAnimation::shiftColor(const ColorShifter * shifter)
-{
-	assert(myAnim);
-	myAnim->shiftColor(shifter);
-}
-
-void CAttackAnimation::nextFrame()
-{
-	if(myAnim->getType() != group)
-	{
-		myAnim->setType(group);
-		myAnim->onAnimationReset += std::bind(&CAttackAnimation::endAnim, this);
-	}
-
-	if(!soundPlayed)
-	{
-		if(shooting)
-			CCS->soundh->playSound(battle_sound(attackingStack->getCreature(), shoot));
-		else
-			CCS->soundh->playSound(battle_sound(attackingStack->getCreature(), attack));
-		soundPlayed = true;
-	}
-	CBattleAnimation::nextFrame();
-}
-
-void CAttackAnimation::endAnim()
-{
-	myAnim->setType(CCreatureAnim::HOLDING);
-	CBattleStackAnimation::endAnim();
-}
-
-bool CAttackAnimation::checkInitialConditions()
-{
-	for(auto & elem : owner->pendingAnims)
-	{
-		CBattleStackAnimation * stAnim = dynamic_cast<CBattleStackAnimation *>(elem.first);
-		CReverseAnimation * revAnim = dynamic_cast<CReverseAnimation *>(stAnim);
-
-		if(revAnim && attackedStack) // enemy must be fully reversed
-		{
-			if (revAnim->stack->ID == attackedStack->ID)
-				return false;
-		}
-	}
-	return isEarliest(false);
-}
-
-CAttackAnimation::CAttackAnimation(CBattleInterface *_owner, const CStack *attacker, BattleHex _dest, const CStack *defender)
-	: CBattleStackAnimation(_owner, attacker),
-		shooting(false), group(CCreatureAnim::SHOOT_FRONT),
-		soundPlayed(false),
-		dest(_dest), attackedStack(defender), attackingStack(attacker)
-{
-	assert(attackingStack && "attackingStack is nullptr in CBattleAttack::CBattleAttack !\n");
-	attackingStackPosBeforeReturn = attackingStack->getPosition();
-}
-
-CDefenceAnimation::CDefenceAnimation(StackAttackedInfo _attackedInfo, CBattleInterface * _owner)
-: CBattleStackAnimation(_owner, _attackedInfo.defender),
-attacker(_attackedInfo.attacker), rangedAttack(_attackedInfo.indirectAttack),
-killed(_attackedInfo.killed), timeToWait(0)
-{
-	logAnim->debug("Created defence anim for %s", _attackedInfo.defender->getName());
-}
-
-bool CDefenceAnimation::init()
-{
-	if(attacker == nullptr && owner->battleEffects.size() > 0)
-		return false;
-
-	ui32 lowestMoveID = owner->animIDhelper + 5;
-	for(auto & elem : owner->pendingAnims)
-	{
-
-		CDefenceAnimation * defAnim = dynamic_cast<CDefenceAnimation *>(elem.first);
-		if(defAnim && defAnim->stack->ID != stack->ID)
-			continue;
-
-		CAttackAnimation * attAnim = dynamic_cast<CAttackAnimation *>(elem.first);
-		if(attAnim && attAnim->stack->ID != stack->ID)
-			continue;
-
-		CEffectAnimation * sen = dynamic_cast<CEffectAnimation *>(elem.first);
-		if (sen)
-			continue;
-
-		CReverseAnimation * animAsRev = dynamic_cast<CReverseAnimation *>(elem.first);
-
-		if(animAsRev)
-			return false;
-
-		if(elem.first)
-			vstd::amin(lowestMoveID, elem.first->ID);
-	}
-
-	if(ID > lowestMoveID)
-		return false;
-
-
-	//reverse unit if necessary
-	if(attacker && owner->getCurrentPlayerInterface()->cb->isToReverse(stack->getPosition(), attacker->getPosition(), owner->creDir[stack->ID], attacker->doubleWide(), owner->creDir[attacker->ID]))
-	{
-		owner->addNewAnim(new CReverseAnimation(owner, stack, stack->getPosition(), true));
-		return false;
-	}
-	//unit reversed
-
-	if(rangedAttack && attacker != nullptr) //delay hit animation
-	{
-		for(std::list<ProjectileInfo>::const_iterator it = owner->projectiles.begin(); it != owner->projectiles.end(); ++it)
-		{
-			if(it->creID == attacker->getCreature()->idNumber)
-			{
-				return false;
-			}
-		}
-	}
-
-	// synchronize animation with attacker, unless defending or attacked by shooter:
-	// wait for 1/2 of attack animation
-	if (!rangedAttack && getMyAnimType() != CCreatureAnim::DEFENCE)
-	{
-		float frameLength = AnimationControls::getCreatureAnimationSpeed(
-								  stack->getCreature(), owner->creAnims[stack->ID].get(), getMyAnimType());
-
-		timeToWait = myAnim->framesInGroup(getMyAnimType()) * frameLength / 2;
-
-		myAnim->setType(CCreatureAnim::HOLDING);
-	}
-	else
-	{
-		timeToWait = 0;
-		startAnimation();
-	}
-
-	return true; //initialized successfuly
-}
-
-std::string CDefenceAnimation::getMySound()
-{
-	if(killed)
-		return battle_sound(stack->getCreature(), killed);
-	else if(stack->defendingAnim)
-		return battle_sound(stack->getCreature(), defend);
-	else
-		return battle_sound(stack->getCreature(), wince);
-}
-
-CCreatureAnim::EAnimType CDefenceAnimation::getMyAnimType()
-{
-	if(killed)
-	{
-		if(rangedAttack && myAnim->framesInGroup(CCreatureAnim::DEATH_RANGED) > 0)
-			return CCreatureAnim::DEATH_RANGED;
-		else
-			return CCreatureAnim::DEATH;
-	}
-
-	if(stack->defendingAnim)
-		return CCreatureAnim::DEFENCE;
-	else
-		return CCreatureAnim::HITTED;
-}
-
-void CDefenceAnimation::startAnimation()
-{
-	CCS->soundh->playSound(getMySound());
-	myAnim->setType(getMyAnimType());
-	myAnim->onAnimationReset += std::bind(&CDefenceAnimation::endAnim, this);
-}
-
-void CDefenceAnimation::nextFrame()
-{
-	if (timeToWait > 0)
-	{
-		timeToWait -= float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000;
-		if (timeToWait <= 0)
-			startAnimation();
-	}
-
-	CBattleAnimation::nextFrame();
-}
-
-void CDefenceAnimation::endAnim()
-{
-	if(killed)
-	{
-		if(rangedAttack && myAnim->framesInGroup(CCreatureAnim::DEAD_RANGED) > 0)
-			myAnim->setType(CCreatureAnim::DEAD_RANGED);
-		else
-			myAnim->setType(CCreatureAnim::DEAD);
-	}
-	else
-	{
-		myAnim->setType(CCreatureAnim::HOLDING);
-	}
-
-
-	CBattleAnimation::endAnim();
-
-	delete this;
-}
-
-CDummyAnimation::CDummyAnimation(CBattleInterface * _owner, int howManyFrames)
-: CBattleAnimation(_owner), counter(0), howMany(howManyFrames)
-{
-	logAnim->debug("Created dummy animation for %d frames", howManyFrames);
-}
-
-bool CDummyAnimation::init()
-{
-	return true;
-}
-
-void CDummyAnimation::nextFrame()
-{
-	counter++;
-	if(counter > howMany)
-		endAnim();
-}
-
-void CDummyAnimation::endAnim()
-{
-	CBattleAnimation::endAnim();
-
-	delete this;
-}
-
-bool CMeleeAttackAnimation::init()
-{
-	if(!CAttackAnimation::checkInitialConditions())
-		return false;
-
-	if(!attackingStack || myAnim->isDead())
-	{
-		endAnim();
-		return false;
-	}
-
-	bool toReverse = owner->getCurrentPlayerInterface()->cb->isToReverse(attackingStackPosBeforeReturn, attackedStack->getPosition(), owner->creDir[stack->ID], attackedStack->doubleWide(), owner->creDir[attackedStack->ID]);
-
-	if(toReverse)
-	{
-		owner->addNewAnim(new CReverseAnimation(owner, stack, attackingStackPosBeforeReturn, true));
-		return false;
-	}
-
-	// opponent must face attacker ( = different directions) before he can be attacked
-	if(attackingStack && attackedStack &&
-		owner->creDir[attackingStack->ID] == owner->creDir[attackedStack->ID])
-		return false;
-
-	//reversed
-
-	shooting = false;
-
-	static const CCreatureAnim::EAnimType mutPosToGroup[] =
-	{
-		CCreatureAnim::ATTACK_UP,
-		CCreatureAnim::ATTACK_UP,
-		CCreatureAnim::ATTACK_FRONT,
-		CCreatureAnim::ATTACK_DOWN,
-		CCreatureAnim::ATTACK_DOWN,
-		CCreatureAnim::ATTACK_FRONT
-	};
-
-	static const CCreatureAnim::EAnimType mutPosToGroup2H[] =
-	{
-		CCreatureAnim::VCMI_2HEX_UP,
-		CCreatureAnim::VCMI_2HEX_UP,
-		CCreatureAnim::VCMI_2HEX_FRONT,
-		CCreatureAnim::VCMI_2HEX_DOWN,
-		CCreatureAnim::VCMI_2HEX_DOWN,
-		CCreatureAnim::VCMI_2HEX_FRONT
-	};
-
-	int revShiftattacker = (attackingStack->side == BattleSide::ATTACKER ? -1 : 1);
-
-	int mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, dest);
-	if(mutPos == -1 && attackingStack->doubleWide())
-	{
-		mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, attackedStack->getPosition());
-	}
-	if (mutPos == -1 && attackedStack->doubleWide())
-	{
-		mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, attackedStack->occupiedHex());
-	}
-	if (mutPos == -1 && attackedStack->doubleWide() && attackingStack->doubleWide())
-	{
-		mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, attackedStack->occupiedHex());
-	}
-
-
-	switch(mutPos) //attack direction
-	{
-	case 0:
-	case 1:
-	case 2:
-	case 3:
-	case 4:
-	case 5:
-		group = mutPosToGroup[mutPos];
-		if(attackingStack->hasBonusOfType(Bonus::TWO_HEX_ATTACK_BREATH))
-		{
-			CCreatureAnim::EAnimType group2H = mutPosToGroup2H[mutPos];
-			if(myAnim->framesInGroup(group2H)>0)
-				group = group2H;
-		}
-
-		break;
-	default:
-		logGlobal->error("Critical Error! Wrong dest in stackAttacking! dest: %d; attacking stack pos: %d; mutual pos: %d", dest.hex, attackingStackPosBeforeReturn, mutPos);
-		group = CCreatureAnim::ATTACK_FRONT;
-		break;
-	}
-
-	return true;
-}
-
-CMeleeAttackAnimation::CMeleeAttackAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked)
-: CAttackAnimation(_owner, attacker, _dest, _attacked)
-{
-	logAnim->debug("Created melee attack anim for %s", attacker->getName());
-}
-
-void CMeleeAttackAnimation::endAnim()
-{
-	CAttackAnimation::endAnim();
-	delete this;
-}
-
-bool CMovementAnimation::init()
-{
-	if( !isEarliest(false) )
-		return false;
-
-	if(!stack || myAnim->isDead())
-	{
-		endAnim();
-		return false;
-	}
-
-	if(owner->creAnims[stack->ID]->framesInGroup(CCreatureAnim::MOVING) == 0 ||
-	   stack->hasBonus(Selector::typeSubtype(Bonus::FLYING, 1)))
-	{
-		//no movement or teleport, end immediately
-		endAnim();
-		return false;
-	}
-
-	//reverse unit if necessary
-	if(owner->shouldRotate(stack, oldPos, nextHex))
-	{
-		// it seems that H3 does NOT plays full rotation animation here in most situations
-		// Logical since it takes quite a lot of time
-		if (curentMoveIndex == 0) // full rotation only for moving towards first tile.
-		{
-			owner->addNewAnim(new CReverseAnimation(owner, stack, oldPos, true));
-			return false;
-		}
-		else
-		{
-			CReverseAnimation::rotateStack(owner, stack, oldPos);
-		}
-	}
-
-	if(myAnim->getType() != CCreatureAnim::MOVING)
-	{
-		myAnim->setType(CCreatureAnim::MOVING);
-	}
-
-	if (owner->moveSoundHander == -1)
-	{
-		owner->moveSoundHander = CCS->soundh->playSound(battle_sound(stack->getCreature(), move), -1);
-	}
-
-	Point begPosition = CClickableHex::getXYUnitAnim(oldPos, stack, owner);
-	Point endPosition = CClickableHex::getXYUnitAnim(nextHex, stack, owner);
-
-	timeToMove = AnimationControls::getMovementDuration(stack->getCreature());
-
-	begX = begPosition.x;
-	begY = begPosition.y;
-	progress = 0;
-	distanceX = endPosition.x - begPosition.x;
-	distanceY = endPosition.y - begPosition.y;
-
-	if (stack->hasBonus(Selector::type()(Bonus::FLYING)))
-	{
-		float distance = static_cast<float>(sqrt(distanceX * distanceX + distanceY * distanceY));
-
-		timeToMove *= AnimationControls::getFlightDistance(stack->getCreature()) / distance;
-	}
-
-	return true;
-}
-
-void CMovementAnimation::nextFrame()
-{
-	progress += float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000 * timeToMove;
-
-	//moving instructions
-	myAnim->pos.x = static_cast<Sint16>(begX + distanceX * progress );
-	myAnim->pos.y = static_cast<Sint16>(begY + distanceY * progress );
-
-	CBattleAnimation::nextFrame();
-
-	if(progress >= 1.0)
-	{
-		// Sets the position of the creature animation sprites
-		Point coords = CClickableHex::getXYUnitAnim(nextHex, stack, owner);
-		myAnim->pos = coords;
-
-		// true if creature haven't reached the final destination hex
-		if ((curentMoveIndex + 1) < destTiles.size())
-		{
-			// update the next hex field which has to be reached by the stack
-			curentMoveIndex++;
-			oldPos = nextHex;
-			nextHex = destTiles[curentMoveIndex];
-
-			// re-init animation
-			for(auto & elem : owner->pendingAnims)
-			{
-				if (elem.first == this)
-				{
-					elem.second = false;
-					break;
-				}
-			}
-		}
-		else
-			endAnim();
-	}
-}
-
-void CMovementAnimation::endAnim()
-{
-	assert(stack);
-
-	myAnim->pos = CClickableHex::getXYUnitAnim(nextHex, stack, owner);
-	CBattleAnimation::endAnim();
-
-	owner->addNewAnim(new CMovementEndAnimation(owner, stack, nextHex));
-
-	if(owner->moveSoundHander != -1)
-	{
-		CCS->soundh->stopSound(owner->moveSoundHander);
-		owner->moveSoundHander = -1;
-	}
-	delete this;
-}
-
-CMovementAnimation::CMovementAnimation(CBattleInterface *_owner, const CStack *_stack, std::vector<BattleHex> _destTiles, int _distance)
-	: CBattleStackAnimation(_owner, _stack),
-	  destTiles(_destTiles),
-	  curentMoveIndex(0),
-	  oldPos(stack->getPosition()),
-	  begX(0), begY(0),
-	  distanceX(0), distanceY(0),
-	  timeToMove(0.0),
-	  progress(0.0),
-	  nextHex(destTiles.front())
-{
-	logAnim->debug("Created movement anim for %s", stack->getName());
-}
-
-CMovementEndAnimation::CMovementEndAnimation(CBattleInterface * _owner, const CStack * _stack, BattleHex destTile)
-: CBattleStackAnimation(_owner, _stack), destinationTile(destTile)
-{
-	logAnim->debug("Created movement end anim for %s", stack->getName());
-}
-
-bool CMovementEndAnimation::init()
-{
-	if( !isEarliest(true) )
-		return false;
-
-	if(!stack || myAnim->framesInGroup(CCreatureAnim::MOVE_END) == 0 ||
-		myAnim->isDead())
-	{
-		endAnim();
-
-		return false;
-	}
-
-	CCS->soundh->playSound(battle_sound(stack->getCreature(), endMoving));
-
-	myAnim->setType(CCreatureAnim::MOVE_END);
-
-	myAnim->onAnimationReset += std::bind(&CMovementEndAnimation::endAnim, this);
-
-	return true;
-}
-
-void CMovementEndAnimation::endAnim()
-{
-	CBattleAnimation::endAnim();
-
-	if(myAnim->getType() != CCreatureAnim::DEAD)
-		myAnim->setType(CCreatureAnim::HOLDING); //resetting to default
-
-	CCS->curh->show();
-	delete this;
-}
-
-CMovementStartAnimation::CMovementStartAnimation(CBattleInterface * _owner, const CStack * _stack)
-: CBattleStackAnimation(_owner, _stack)
-{
-	logAnim->debug("Created movement start anim for %s", stack->getName());
-}
-
-bool CMovementStartAnimation::init()
-{
-	if( !isEarliest(false) )
-		return false;
-
-
-	if(!stack || myAnim->isDead())
-	{
-		CMovementStartAnimation::endAnim();
-		return false;
-	}
-
-	CCS->soundh->playSound(battle_sound(stack->getCreature(), startMoving));
-	myAnim->setType(CCreatureAnim::MOVE_START);
-	myAnim->onAnimationReset += std::bind(&CMovementStartAnimation::endAnim, this);
-
-	return true;
-}
-
-void CMovementStartAnimation::endAnim()
-{
-	CBattleAnimation::endAnim();
-
-	delete this;
-}
-
-CReverseAnimation::CReverseAnimation(CBattleInterface * _owner, const CStack * stack, BattleHex dest, bool _priority)
-: CBattleStackAnimation(_owner, stack), hex(dest), priority(_priority)
-{
-	logAnim->debug("Created reverse anim for %s", stack->getName());
-}
-
-bool CReverseAnimation::init()
-{
-	if(myAnim == nullptr || myAnim->isDead())
-	{
-		endAnim();
-		return false; //there is no such creature
-	}
-
-	if(!priority && !isEarliest(false))
-		return false;
-
-	if(myAnim->framesInGroup(CCreatureAnim::TURN_L))
-	{
-		myAnim->setType(CCreatureAnim::TURN_L);
-		myAnim->onAnimationReset += std::bind(&CReverseAnimation::setupSecondPart, this);
-	}
-	else
-	{
-		setupSecondPart();
-	}
-	return true;
-}
-
-void CReverseAnimation::endAnim()
-{
-	CBattleAnimation::endAnim();
-	if( stack->alive() )//don't do that if stack is dead
-		myAnim->setType(CCreatureAnim::HOLDING);
-
-	delete this;
-}
-
-void CReverseAnimation::rotateStack(CBattleInterface * owner, const CStack * stack, BattleHex hex)
-{
-	owner->creDir[stack->ID] = !owner->creDir[stack->ID];
-
-	owner->creAnims[stack->ID]->pos = CClickableHex::getXYUnitAnim(hex, stack, owner);
-}
-
-void CReverseAnimation::setupSecondPart()
-{
-	if(!stack)
-	{
-		endAnim();
-		return;
-	}
-
-	rotateStack(owner, stack, hex);
-
-	if(myAnim->framesInGroup(CCreatureAnim::TURN_R))
-	{
-		myAnim->setType(CCreatureAnim::TURN_R);
-		myAnim->onAnimationReset += std::bind(&CReverseAnimation::endAnim, this);
-	}
-	else
-		endAnim();
-}
-
-CRangedAttackAnimation::CRangedAttackAnimation(CBattleInterface * owner_, const CStack * attacker, BattleHex dest_, const CStack * defender)
-	: CAttackAnimation(owner_, attacker, dest_, defender)
-{
-
-}
-
-
-CShootingAnimation::CShootingAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool _catapult, int _catapultDmg)
-	: CRangedAttackAnimation(_owner, attacker, _dest, _attacked),
-	catapultDamage(_catapultDmg)
-{
-	logAnim->debug("Created shooting anim for %s", stack->getName());
-}
-
-bool CShootingAnimation::init()
-{
-	if( !CAttackAnimation::checkInitialConditions() )
-		return false;
-
-	const CStack * shooter = attackingStack;
-
-	if(!shooter || myAnim->isDead())
-	{
-		endAnim();
-		return false;
-	}
-
-	//reverse unit if necessary
-	if (attackingStack && attackedStack && owner->getCurrentPlayerInterface()->cb->isToReverse(attackingStack->getPosition(), attackedStack->getPosition(), owner->creDir[attackingStack->ID], attackingStack->doubleWide(), owner->creDir[attackedStack->ID]))
-	{
-		owner->addNewAnim(new CReverseAnimation(owner, attackingStack, attackingStack->getPosition(), true));
-		return false;
-	}
-
-	// opponent must face attacker ( = different directions) before he can be attacked
-	//FIXME: this cause freeze
-
-//	if (attackingStack && attackedStack &&
-//	    owner->creDir[attackingStack->ID] == owner->creDir[attackedStack->ID])
-//		return false;
-
-	// Create the projectile animation
-
-	//maximal angle in radians between straight horizontal line and shooting line for which shot is considered to be straight (absoulte value)
-	static const double straightAngle = 0.2;
-
-	// Get further info about the shooter e.g. relative pos of projectile to unit.
-	// If the creature id is 149 then it's a arrow tower which has no additional info so get the
-	// actual arrow tower shooter instead.
-	const CCreature *shooterInfo = shooter->getCreature();
-
-	if(shooterInfo->idNumber == CreatureID::ARROW_TOWERS)
-	{
-		int creID = owner->siegeH->town->town->clientInfo.siegeShooter;
-		shooterInfo = CGI->creh->operator[](creID);
-	}
-	if(!shooterInfo->animation.missleFrameAngles.size())
-		logAnim->error("Mod error: Creature '%s' on the Archer's tower is not a shooter. Mod should be fixed. Trying to use archer's data instead..."
-			, shooterInfo->nameSing);
-
-	auto & angles = shooterInfo->animation.missleFrameAngles.size()
-		? shooterInfo->animation.missleFrameAngles
-		: CGI->creh->operator[](CreatureID::ARCHER)->animation.missleFrameAngles;
-
-	ProjectileInfo spi;
-	spi.shotDone = false;
-	spi.creID = shooter->getCreature()->idNumber;
-	spi.stackID = shooter->ID;
-	// reverse if creature is facing right OR this is non-existing stack that is not tower (war machines)
-	spi.reverse = attackingStack ? !owner->creDir[attackingStack->ID] : shooter->getCreature()->idNumber != CreatureID::ARROW_TOWERS ;
-
-	spi.step = 0;
-	spi.frameNum = 0;
-
-	Point fromPos;
-	Point destPos;
-
-	// NOTE: two lines below return different positions (very notable with 2-hex creatures). Obtaining via creanims seems to be more precise
-	fromPos = owner->creAnims[spi.stackID]->pos.topLeft();
-	//xycoord = CClickableHex::getXYUnitAnim(shooter->position, true, shooter, owner);
-
-	destPos = CClickableHex::getXYUnitAnim(dest, attackedStack, owner);
-
-	// to properly translate coordinates when shooter is rotated
-	int multiplier = spi.reverse ? -1 : 1;
-
-	double projectileAngle = atan2(fabs((double)destPos.y - fromPos.y), fabs((double)destPos.x - fromPos.x));
-	if(shooter->getPosition() < dest)
-		projectileAngle = -projectileAngle;
-
-	// Calculate projectile start position. Offsets are read out of the CRANIM.TXT.
-	if (projectileAngle > straightAngle)
-	{
-		//upper shot
-		spi.x0 = fromPos.x + 222 + ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier;
-		spi.y0 = fromPos.y + 265 + shooterInfo->animation.upperRightMissleOffsetY;
-	}
-	else if (projectileAngle < -straightAngle)
-	{
-		//lower shot
-		spi.x0 = fromPos.x + 222 + ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier;
-		spi.y0 = fromPos.y + 265 + shooterInfo->animation.lowerRightMissleOffsetY;
-	}
-	else
-	{
-		//straight shot
-		spi.x0 = fromPos.x + 222 + ( -25 + shooterInfo->animation.rightMissleOffsetX ) * multiplier;
-		spi.y0 = fromPos.y + 265 + shooterInfo->animation.rightMissleOffsetY;
-	}
-
-	spi.x = spi.x0;
-	spi.y = spi.y0;
-
-	destPos += Point(225, 225);
-
-	// recalculate angle taking in account offsets
-	//projectileAngle = atan2(fabs(destPos.y - spi.y), fabs(destPos.x - spi.x));
-	//if(shooter->position < dest)
-	//	projectileAngle = -projectileAngle;
-
-	if (attackedStack)
-	{
-		double animSpeed = AnimationControls::getProjectileSpeed(); // flight speed of projectile
-		double distanceSquared = (destPos.x - spi.x) * (destPos.x - spi.x) + (destPos.y - spi.y) * (destPos.y - spi.y);
-		double distance = sqrt(distanceSquared);
-		spi.lastStep = std::round(distance / animSpeed);
-		if(spi.lastStep == 0)
-			spi.lastStep = 1;
-		spi.dx = (destPos.x - spi.x) / spi.lastStep;
-		spi.dy = (destPos.y - spi.y) / spi.lastStep;
-	}
-	else
-	{
-		// Catapult attack
-		spi.catapultInfo.reset(new CatapultProjectileInfo(Point((int)spi.x, (int)spi.y), destPos));
-
-		double animSpeed = AnimationControls::getProjectileSpeed() / 10;
-		spi.lastStep = static_cast<int>(std::abs((destPos.x - spi.x) / animSpeed));
-		spi.dx = animSpeed;
-		spi.dy = 0;
-
-		auto img = owner->idToProjectile[spi.creID]->getImage(0);
-
-		// Add explosion anim
-		Point animPos(destPos.x - 126 + img->width() / 2,
-					  destPos.y - 105 + img->height() / 2);
-
-		owner->addNewAnim( new CEffectAnimation(owner, catapultDamage ? "SGEXPL.DEF" : "CSGRCK.DEF", animPos.x, animPos.y));
-	}
-	double pi = boost::math::constants::pi<double>();
-
-	//in some cases (known one: hero grants shooter bonus to unit) the shooter stack's projectile may not be properly initialized
-	if (!owner->idToProjectile.count(spi.creID) && !owner->idToRay.count(spi.creID))
-		owner->initStackProjectile(shooter);
-
-	if (owner->idToProjectile.count(spi.creID))
-	{
-		// only frames below maxFrame are usable: anything  higher is either no present or we don't know when it should be used
-		size_t maxFrame = std::min<size_t>(angles.size(), owner->idToProjectile.at(spi.creID)->size(0));
-
-		assert(maxFrame > 0);
-
-		// values in angles array indicate position from which this frame was rendered, in degrees.
-		// find frame that has closest angle to one that we need for this shot
-		size_t bestID = 0;
-		double bestDiff = fabs( angles[0] / 180 * pi - projectileAngle );
-
-		for (size_t i=1; i<maxFrame; i++)
-		{
-			double currentDiff = fabs( angles[i] / 180 * pi - projectileAngle );
-			if (currentDiff < bestDiff)
-			{
-				bestID = i;
-				bestDiff = currentDiff;
-			}
-		}
-
-		spi.frameNum = static_cast<int>(bestID);
-	}
-	else if (owner->idToRay.count(spi.creID))
-	{
-		// no-op
-	}
-	else
-	{
-		logGlobal->error("Unable to find valid projectile for shooter %d", spi.creID);
-	}
-
-	// Set projectile animation start delay which is specified in frames
-	spi.animStartDelay = shooterInfo->animation.attackClimaxFrame;
-	owner->projectiles.push_back(spi);
-
-	//attack animation
-
-	shooting = true;
-
-	if(projectileAngle > straightAngle)
-		group = CCreatureAnim::SHOOT_UP;
-	else if(projectileAngle < -straightAngle)
-		group = CCreatureAnim::SHOOT_DOWN;
-	else //straight shot
-		group = CCreatureAnim::SHOOT_FRONT;
-
-	return true;
-}
-
-void CShootingAnimation::nextFrame()
-{
-	for(auto & it : owner->pendingAnims)
-	{
-		CMovementStartAnimation * anim = dynamic_cast<CMovementStartAnimation *>(it.first);
-		CReverseAnimation * anim2 = dynamic_cast<CReverseAnimation *>(it.first);
-		if( (anim && anim->stack->ID == stack->ID) || (anim2 && anim2->stack->ID == stack->ID && anim2->priority ) )
-			return;
-	}
-
-	CAttackAnimation::nextFrame();
-}
-
-void CShootingAnimation::endAnim()
-{
-	// play wall hit/miss sound for catapult attack
-	if(!attackedStack)
-	{
-		if(catapultDamage > 0)
-		{
-			CCS->soundh->playSound("WALLHIT");
-		}
-		else
-		{
-			CCS->soundh->playSound("WALLMISS");
-		}
-	}
-
-	CAttackAnimation::endAnim();
-	delete this;
-}
-
-CCastAnimation::CCastAnimation(CBattleInterface * owner_, const CStack * attacker, BattleHex dest_, const CStack * defender)
-	: CRangedAttackAnimation(owner_, attacker, dest_, defender)
-{
-	if(!dest_.isValid() && defender)
-		dest = defender->getPosition();
-}
-
-bool CCastAnimation::init()
-{
-	if(!CAttackAnimation::checkInitialConditions())
-		return false;
-
-	if(!attackingStack || myAnim->isDead())
-	{
-		endAnim();
-		return false;
-	}
-
-	//reverse unit if necessary
-	if(attackedStack)
-	{
-		if(owner->getCurrentPlayerInterface()->cb->isToReverse(attackingStack->getPosition(), attackedStack->getPosition(), owner->creDir[attackingStack->ID], attackingStack->doubleWide(), owner->creDir[attackedStack->ID]))
-		{
-			owner->addNewAnim(new CReverseAnimation(owner, attackingStack, attackingStack->getPosition(), true));
-			return false;
-		}
-	}
-	else
-	{
-		if(dest.isValid() && owner->getCurrentPlayerInterface()->cb->isToReverse(attackingStack->getPosition(), dest, owner->creDir[attackingStack->ID], false, false))
-		{
-			owner->addNewAnim(new CReverseAnimation(owner, attackingStack, attackingStack->getPosition(), true));
-			return false;
-		}
-	}
-
-	//TODO: display spell projectile here
-
-	static const double straightAngle = 0.2;
-
-
-	Point fromPos;
-	Point destPos;
-
-	// NOTE: two lines below return different positions (very notable with 2-hex creatures). Obtaining via creanims seems to be more precise
-	fromPos = owner->creAnims[attackingStack->ID]->pos.topLeft();
-	//xycoord = CClickableHex::getXYUnitAnim(shooter->getPosition(), true, shooter, owner);
-
-	destPos = CClickableHex::getXYUnitAnim(dest, attackedStack, owner);
-
-
-	double projectileAngle = atan2(fabs((double)destPos.y - fromPos.y), fabs((double)destPos.x - fromPos.x));
-	if(attackingStack->getPosition() < dest)
-		projectileAngle = -projectileAngle;
-
-
-	if(projectileAngle > straightAngle)
-		group = CCreatureAnim::VCMI_CAST_UP;
-	else if(projectileAngle < -straightAngle)
-		group = CCreatureAnim::VCMI_CAST_DOWN;
-	else
-		group = CCreatureAnim::VCMI_CAST_FRONT;
-
-	//fall back to H3 cast/2hex
-	//even if creature have 2hex attack instead of cast it is ok since we fall back to attack anyway
-	if(myAnim->framesInGroup(group) == 0)
-	{
-		if(projectileAngle > straightAngle)
-			group = CCreatureAnim::CAST_UP;
-		else if(projectileAngle < -straightAngle)
-			group = CCreatureAnim::CAST_DOWN;
-		else
-			group = CCreatureAnim::CAST_FRONT;
-	}
-
-	//fall back to ranged attack
-	if(myAnim->framesInGroup(group) == 0)
-	{
-		if(projectileAngle > straightAngle)
-			group = CCreatureAnim::SHOOT_UP;
-		else if(projectileAngle < -straightAngle)
-			group = CCreatureAnim::SHOOT_DOWN;
-		else
-			group = CCreatureAnim::SHOOT_FRONT;
-	}
-
-	//fall back to normal attack
-	if(myAnim->framesInGroup(group) == 0)
-	{
-		if(projectileAngle > straightAngle)
-			group = CCreatureAnim::ATTACK_UP;
-		else if(projectileAngle < -straightAngle)
-			group = CCreatureAnim::ATTACK_DOWN;
-		else
-			group = CCreatureAnim::ATTACK_FRONT;
-	}
-
-	return true;
-}
-
-void CCastAnimation::nextFrame()
-{
-	for(auto & it : owner->pendingAnims)
-	{
-		CReverseAnimation * anim = dynamic_cast<CReverseAnimation *>(it.first);
-		if(anim && anim->stack->ID == stack->ID && anim->priority)
-			return;
-	}
-
-	if(myAnim->getType() != group)
-	{
-		myAnim->setType(group);
-		myAnim->onAnimationReset += std::bind(&CAttackAnimation::endAnim, this);
-	}
-
-	CBattleAnimation::nextFrame();
-}
-
-
-void CCastAnimation::endAnim()
-{
-	CAttackAnimation::endAnim();
-	delete this;
-}
-
-
-CEffectAnimation::CEffectAnimation(CBattleInterface * _owner, std::string _customAnim, int _x, int _y, int _dx, int _dy, bool _Vflip, bool _alignToBottom)
-	: CBattleAnimation(_owner),
-	destTile(BattleHex::INVALID),
-	x(_x),
-	y(_y),
-	dx(_dx),
-	dy(_dy),
-	Vflip(_Vflip),
-	alignToBottom(_alignToBottom)
-{
-	logAnim->debug("Created effect animation %s", _customAnim);
-
-	customAnim = std::make_shared<CAnimation>(_customAnim);
-}
-
-CEffectAnimation::CEffectAnimation(CBattleInterface * _owner, std::shared_ptr<CAnimation> _customAnim, int _x, int _y, int _dx, int _dy)
-	: CBattleAnimation(_owner),
-	destTile(BattleHex::INVALID),
-	customAnim(_customAnim),
-	x(_x),
-	y(_y),
-	dx(_dx),
-	dy(_dy),
-	Vflip(false),
-	alignToBottom(false)
-{
-	logAnim->debug("Created custom effect animation");
-}
-
-
-CEffectAnimation::CEffectAnimation(CBattleInterface * _owner, std::string _customAnim, BattleHex _destTile, bool _Vflip, bool _alignToBottom)
-	: CBattleAnimation(_owner),
-	destTile(_destTile),
-	x(-1),
-	y(-1),
-	dx(0),
-	dy(0),
-	Vflip(_Vflip),
-	alignToBottom(_alignToBottom)
-{
-	logAnim->debug("Created effect animation %s", _customAnim);
-	customAnim = std::make_shared<CAnimation>(_customAnim);
-}
-
-bool CEffectAnimation::init()
-{
-	if(!isEarliest(true))
-		return false;
-
-	const bool areaEffect = (!destTile.isValid() && x == -1 && y == -1);
-
-	std::shared_ptr<CAnimation> animation = customAnim;
-
-	animation->preload();
-	if(Vflip)
-		animation->verticalFlip();
-
-	auto first = animation->getImage(0, 0, true);
-	if(!first)
-	{
-		endAnim();
-		return false;
-	}
-
-	if(areaEffect) //f.e. armageddon
-	{
-		for(int i=0; i * first->width() < owner->pos.w ; ++i)
-		{
-			for(int j=0; j * first->height() < owner->pos.h ; ++j)
-			{
-				BattleEffect be;
-				be.effectID = ID;
-				be.animation = animation;
-				be.currentFrame = 0;
-
-				be.x = i * first->width() + owner->pos.x;
-				be.y = j * first->height() + owner->pos.y;
-				be.position = BattleHex::INVALID;
-
-				owner->battleEffects.push_back(be);
-			}
-		}
-	}
-	else // Effects targeted at a specific creature/hex.
-	{
-		const CStack * destStack = owner->getCurrentPlayerInterface()->cb->battleGetStackByPos(destTile, false);
-		BattleEffect be;
-		be.effectID = ID;
-		be.animation = animation;
-		be.currentFrame = 0;
-
-
-		//todo: lightning anim frame count override
-
-//			if(effect == 1)
-//				be.maxFrame = 3;
-
-		be.x = x;
-		be.y = y;
-		if(destTile.isValid())
-		{
-			Rect & tilePos = owner->bfield[destTile]->pos;
-			if(x == -1)
-				be.x = tilePos.x + tilePos.w/2 - first->width()/2;
-			if(y == -1)
-			{
-				if(alignToBottom)
-					be.y = tilePos.y + tilePos.h - first->height();
-				else
-					be.y = tilePos.y - first->height()/2;
-			}
-
-			// Correction for 2-hex creatures.
-			if(destStack != nullptr && destStack->doubleWide())
-				be.x += (destStack->side == BattleSide::ATTACKER ? -1 : 1)*tilePos.w/2;
-		}
-
-		assert(be.x != -1 && be.y != -1);
-
-		//Indicate if effect should be drawn on top of everything or just on top of the hex
-		be.position = destTile;
-
-		owner->battleEffects.push_back(be);
-	}
-	//battleEffects
-	return true;
-}
-
-void CEffectAnimation::nextFrame()
-{
-	//notice: there may be more than one effect in owner->battleEffects correcponding to this animation (ie. armageddon)
-	for(auto & elem : owner->battleEffects)
-	{
-		if(elem.effectID == ID)
-		{
-			elem.currentFrame += AnimationControls::getSpellEffectSpeed() * GH.mainFPSmng->getElapsedMilliseconds() / 1000;
-
-			if(elem.currentFrame >= elem.animation->size())
-			{
-				endAnim();
-				break;
-			}
-			else
-			{
-				elem.x += dx;
-				elem.y += dy;
-			}
-		}
-	}
-}
-
-void CEffectAnimation::endAnim()
-{
-	CBattleAnimation::endAnim();
-
-	std::vector<std::list<BattleEffect>::iterator> toDel;
-
-	for(auto it = owner->battleEffects.begin(); it != owner->battleEffects.end(); ++it)
-	{
-		if(it->effectID == ID)
-		{
-			toDel.push_back(it);
-		}
-	}
-
-	for(auto & elem : toDel)
-	{
-		owner->battleEffects.erase(elem);
-	}
-
-	delete this;
-}

+ 0 - 262
client/battle/CBattleAnimations.h

@@ -1,262 +0,0 @@
-/*
- * CBattleAnimations.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/BattleHex.h"
-#include "../widgets/Images.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class CStack;
-
-VCMI_LIB_NAMESPACE_END
-
-class CBattleInterface;
-class CCreatureAnimation;
-struct CatapultProjectileInfo;
-struct StackAttackedInfo;
-
-/// Base class of battle animations
-class CBattleAnimation
-{
-protected:
-	CBattleInterface * owner;
-public:
-	virtual bool init() = 0; //to be called - if returned false, call again until returns true
-	virtual void nextFrame() {} //call every new frame
-	virtual void endAnim(); //to be called mostly internally; in this class it removes animation from pendingAnims list
-	virtual ~CBattleAnimation();
-
-	bool isEarliest(bool perStackConcurrency); //determines if this animation is earliest of all
-
-	ui32 ID; //unique identifier
-	CBattleAnimation(CBattleInterface * _owner);
-};
-
-/// Sub-class which is responsible for managing the battle stack animation.
-class CBattleStackAnimation : public CBattleAnimation
-{
-public:
-	std::shared_ptr<CCreatureAnimation> myAnim; //animation for our stack, managed by CBattleInterface
-	const CStack * stack; //id of stack whose animation it is
-
-	CBattleStackAnimation(CBattleInterface * _owner, const CStack * _stack);
-
-	void shiftColor(const ColorShifter * shifter);
-};
-
-/// This class is responsible for managing the battle attack animation
-class CAttackAnimation : public CBattleStackAnimation
-{
-	bool soundPlayed;
-
-protected:
-	BattleHex dest; //attacked hex
-	bool shooting;
-	CCreatureAnim::EAnimType group; //if shooting is true, print this animation group
-	const CStack *attackedStack;
-	const CStack *attackingStack;
-	int attackingStackPosBeforeReturn; //for stacks with return_after_strike feature
-public:
-	void nextFrame() override;
-	void endAnim() override;
-	bool checkInitialConditions();
-
-	CAttackAnimation(CBattleInterface *_owner, const CStack *attacker, BattleHex _dest, const CStack *defender);
-};
-
-/// Animation of a defending unit
-class CDefenceAnimation : public CBattleStackAnimation
-{
-	CCreatureAnim::EAnimType getMyAnimType();
-	std::string getMySound();
-
-	void startAnimation();
-
-	const CStack * attacker; //attacking stack
-	bool rangedAttack; //if true, stack has been attacked by shooting
-	bool killed; //if true, stack has been killed
-
-	float timeToWait; // for how long this animation should be paused
-public:
-	bool init() override;
-	void nextFrame() override;
-	void endAnim() override;
-
-	CDefenceAnimation(StackAttackedInfo _attackedInfo, CBattleInterface * _owner);
-	virtual ~CDefenceAnimation(){};
-};
-
-class CDummyAnimation : public CBattleAnimation
-{
-private:
-	int counter;
-	int howMany;
-public:
-	bool init() override;
-	void nextFrame() override;
-	void endAnim() override;
-
-	CDummyAnimation(CBattleInterface * _owner, int howManyFrames);
-	virtual ~CDummyAnimation(){}
-};
-
-/// Hand-to-hand attack
-class CMeleeAttackAnimation : public CAttackAnimation
-{
-public:
-	bool init() override;
-	void endAnim() override;
-
-	CMeleeAttackAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked);
-	virtual ~CMeleeAttackAnimation(){};
-};
-
-/// Move animation of a creature
-class CMovementAnimation : public CBattleStackAnimation
-{
-private:
-	std::vector<BattleHex> destTiles; //full path, includes already passed hexes
-	ui32 curentMoveIndex; // index of nextHex in destTiles
-
-	BattleHex oldPos; //position of stack before move
-
-	double begX, begY; // starting position
-	double distanceX, distanceY; // full movement distance, may be negative if creture moves topleft
-
-	double timeToMove; // full length of movement animation
-	double progress; // range 0 -> 1, indicates move progrees. 0 = movement starts, 1 = move ends
-
-public:
-	BattleHex nextHex; // next hex, to which creature move right now
-
-	bool init() override;
-	void nextFrame() override;
-	void endAnim() override;
-
-	CMovementAnimation(CBattleInterface *_owner, const CStack *_stack, std::vector<BattleHex> _destTiles, int _distance);
-	virtual ~CMovementAnimation(){};
-};
-
-/// Move end animation of a creature
-class CMovementEndAnimation : public CBattleStackAnimation
-{
-private:
-	BattleHex destinationTile;
-public:
-	bool init() override;
-	void endAnim() override;
-
-	CMovementEndAnimation(CBattleInterface * _owner, const CStack * _stack, BattleHex destTile);
-	virtual ~CMovementEndAnimation(){};
-};
-
-/// Move start animation of a creature
-class CMovementStartAnimation : public CBattleStackAnimation
-{
-public:
-	bool init() override;
-	void endAnim() override;
-
-	CMovementStartAnimation(CBattleInterface * _owner, const CStack * _stack);
-	virtual ~CMovementStartAnimation(){};
-};
-
-/// Class responsible for animation of stack chaning direction (left <-> right)
-class CReverseAnimation : public CBattleStackAnimation
-{
-	BattleHex hex;
-public:
-	bool priority; //true - high, false - low
-	bool init() override;
-
-	static void rotateStack(CBattleInterface * owner, const CStack * stack, BattleHex hex);
-
-	void setupSecondPart();
-	void endAnim() override;
-
-	CReverseAnimation(CBattleInterface * _owner, const CStack * stack, BattleHex dest, bool _priority);
-	virtual ~CReverseAnimation(){};
-};
-
-/// Small struct which contains information about the position and the velocity of a projectile
-struct ProjectileInfo
-{
-	double x0, y0; //initial position on the screen
-	double x, y; //position on the screen
-	double dx, dy; //change in position in one step
-	int step, lastStep; //to know when finish showing this projectile
-	int creID; //ID of creature that shot this projectile
-	int stackID; //ID of stack
-	int frameNum; //frame to display form projectile animation
-	//bool spin; //if true, frameNum will be increased
-	int animStartDelay; //frame of shooter animation when projectile should appear
-	bool shotDone; // actual shot already done, projectile is flying
-	bool reverse; //if true, projectile will be flipped by vertical asix
-	std::shared_ptr<CatapultProjectileInfo> catapultInfo; // holds info about the parabolic trajectory of the cannon
-};
-
-class CRangedAttackAnimation : public CAttackAnimation
-{
-public:
-	CRangedAttackAnimation(CBattleInterface * owner_, const CStack * attacker, BattleHex dest_, const CStack * defender);
-protected:
-
-};
-
-/// Shooting attack
-class CShootingAnimation : public CRangedAttackAnimation
-{
-private:
-	int catapultDamage;
-public:
-	bool init() override;
-	void nextFrame() override;
-	void endAnim() override;
-
-	//last two params only for catapult attacks
-	CShootingAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex _dest,
-		const CStack * _attacked, bool _catapult = false, int _catapultDmg = 0);
-	virtual ~CShootingAnimation(){};
-};
-
-class CCastAnimation : public CRangedAttackAnimation
-{
-public:
-	CCastAnimation(CBattleInterface * owner_, const CStack * attacker, BattleHex dest_, const CStack * defender);
-
-	bool init() override;
-	void nextFrame() override;
-	void endAnim() override;
-
-};
-
-
-/// This class manages effect animation
-class CEffectAnimation : public CBattleAnimation
-{
-private:
-	BattleHex destTile;
-	std::shared_ptr<CAnimation>	customAnim;
-	int	x, y, dx, dy;
-	bool Vflip;
-	bool alignToBottom;
-public:
-	bool init() override;
-	void nextFrame() override;
-	void endAnim() override;
-
-	CEffectAnimation(CBattleInterface * _owner, std::string _customAnim, int _x, int _y, int _dx = 0, int _dy = 0, bool _Vflip = false, bool _alignToBottom = false);
-
-	CEffectAnimation(CBattleInterface * _owner, std::shared_ptr<CAnimation> _customAnim, int _x, int _y, int _dx = 0, int _dy = 0);
-
-	CEffectAnimation(CBattleInterface * _owner, std::string _customAnim, BattleHex _destTile, bool _Vflip = false, bool _alignToBottom = false);
-	virtual ~CEffectAnimation(){};
-};

+ 0 - 3807
client/battle/CBattleInterface.cpp

@@ -1,3807 +0,0 @@
-/*
- * CBattleInterface.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 "CBattleInterface.h"
-
-#include "CBattleAnimations.h"
-#include "CBattleInterfaceClasses.h"
-#include "CCreatureAnimation.h"
-
-#include "../CBitmapHandler.h"
-#include "../CGameInfo.h"
-#include "../CMessage.h"
-#include "../CMT.h"
-#include "../CMusicHandler.h"
-#include "../CPlayerInterface.h"
-#include "../CVideoHandler.h"
-#include "../Graphics.h"
-#include "../gui/CAnimation.h"
-#include "../gui/CCursorHandler.h"
-#include "../gui/CGuiHandler.h"
-#include "../gui/SDL_Extensions.h"
-#include "../windows/CAdvmapInterface.h"
-#include "../windows/CCreatureWindow.h"
-#include "../windows/CSpellWindow.h"
-
-#include "../../CCallback.h"
-#include "../../lib/CStack.h"
-#include "../../lib/CConfigHandler.h"
-#include "../../lib/CGeneralTextHandler.h"
-#include "../../lib/CHeroHandler.h"
-#include "../../lib/CondSh.h"
-#include "../../lib/CRandomGenerator.h"
-#include "../../lib/spells/CSpellHandler.h"
-#include "../../lib/spells/ISpellMechanics.h"
-#include "../../lib/spells/Problem.h"
-#include "../../lib/CTownHandler.h"
-#include "../../lib/BattleFieldHandler.h"
-#include "../../lib/ObstacleHandler.h"
-#include "../../lib/CGameState.h"
-#include "../../lib/mapping/CMap.h"
-#include "../../lib/NetPacks.h"
-#include "../../lib/UnlockGuard.h"
-
-CondSh<bool> CBattleInterface::animsAreDisplayed(false);
-CondSh<BattleAction *> CBattleInterface::givenCommand(nullptr);
-
-static void onAnimationFinished(const CStack *stack, std::weak_ptr<CCreatureAnimation> anim)
-{
-	std::shared_ptr<CCreatureAnimation> animation = anim.lock();
-	if(!animation)
-		return;
-
-	if (animation->isIdle())
-	{
-		const CCreature *creature = stack->getCreature();
-
-		if (animation->framesInGroup(CCreatureAnim::MOUSEON) > 0)
-		{
-			if (CRandomGenerator::getDefault().nextDouble(99.0) < creature->animation.timeBetweenFidgets *10)
-				animation->playOnce(CCreatureAnim::MOUSEON);
-			else
-				animation->setType(CCreatureAnim::HOLDING);
-		}
-		else
-		{
-			animation->setType(CCreatureAnim::HOLDING);
-		}
-	}
-	// always reset callback
-	animation->onAnimationReset += std::bind(&onAnimationFinished, stack, anim);
-}
-
-static void transformPalette(SDL_Surface *surf, double rCor, double gCor, double bCor)
-{
-	SDL_Color *colorsToChange = surf->format->palette->colors;
-	for (int g=0; g<surf->format->palette->ncolors; ++g)
-	{
-		SDL_Color *color = &colorsToChange[g];
-		if (color->b != 132 &&
-			color->g != 231 &&
-			color->r != 255) //it's not yellow border
-		{
-			color->r = static_cast<Uint8>(color->r * rCor);
-			color->g = static_cast<Uint8>(color->g * gCor);
-			color->b = static_cast<Uint8>(color->b * bCor);
-		}
-	}
-}
-
-void CBattleInterface::addNewAnim(CBattleAnimation *anim)
-{
-	pendingAnims.push_back( std::make_pair(anim, false) );
-	animsAreDisplayed.setn(true);
-}
-
-CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet *army2,
-		const CGHeroInstance *hero1, const CGHeroInstance *hero2,
-		const SDL_Rect & myRect,
-		std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen, std::shared_ptr<CPlayerInterface> spectatorInt)
-	: background(nullptr), attackingHeroInstance(hero1), defendingHeroInstance(hero2), animCount(0),
-	activeStack(nullptr), mouseHoveredStack(nullptr), stackToActivate(nullptr), selectedStack(nullptr), previouslyHoveredHex(-1),
-	currentlyHoveredHex(-1), attackingHex(-1), stackCanCastSpell(false), creatureCasting(false), spellDestSelectMode(false), spellToCast(nullptr), sp(nullptr),
-	creatureSpellToCast(-1),
-	siegeH(nullptr), attackerInt(att), defenderInt(defen), curInt(att), animIDhelper(0),
-	myTurn(false), moveStarted(false), moveSoundHander(-1), bresult(nullptr), battleActionsStarted(false)
-{
-	OBJ_CONSTRUCTION;
-
-	if(spectatorInt)
-	{
-		curInt = spectatorInt;
-	}
-	else if(!curInt)
-	{
-		//May happen when we are defending during network MP game -> attacker interface is just not present
-		curInt = defenderInt;
-	}
-
-	animsAreDisplayed.setn(false);
-	pos = myRect;
-	strongInterest = true;
-	givenCommand.setn(nullptr);
-
-	//hot-seat -> check tactics for both players (defender may be local human)
-	if(attackerInt && attackerInt->cb->battleGetTacticDist())
-		tacticianInterface = attackerInt;
-	else if(defenderInt && defenderInt->cb->battleGetTacticDist())
-		tacticianInterface = defenderInt;
-
-	//if we found interface of player with tactics, then enter tactics mode
-	tacticsMode = static_cast<bool>(tacticianInterface);
-
-	//create stack queue
-
-	bool embedQueue;
-
-	std::string queueSize = settings["battle"]["queueSize"].String();
-
-	if(queueSize == "auto")
-		embedQueue = screen->h < 700;
-	else
-		embedQueue = screen->h < 700 || queueSize == "small";
-
-	queue = std::make_shared<CStackQueue>(embedQueue, this);
-	if(!embedQueue)
-	{
-		if (settings["battle"]["showQueue"].Bool())
-			pos.y += queue->pos.h / 2; //center whole window
-
-		queue->moveTo(Point(pos.x, pos.y - queue->pos.h));
-	}
-
-	//preparing siege info
-	const CGTownInstance *town = curInt->cb->battleGetDefendedTown();
-	if(town && town->hasFort())
-	{
-		siegeH = new SiegeHelper(town, this);
-	}
-
-	CPlayerInterface::battleInt = this;
-
-	//initializing armies
-	this->army1 = army1;
-	this->army2 = army2;
-	std::vector<const CStack*> stacks = curInt->cb->battleGetAllStacks(true);
-	for(const CStack * s : stacks)
-	{
-		unitAdded(s);
-	}
-
-	//preparing menu background and terrain
-	if(siegeH)
-	{
-		background = BitmapHandler::loadBitmap( siegeH->getSiegeName(0), false );
-		ui8 siegeLevel = curInt->cb->battleGetSiegeLevel();
-		if (siegeLevel >= 2) //citadel or castle
-		{
-			//print moat/mlip
-			SDL_Surface *moat = BitmapHandler::loadBitmap( siegeH->getSiegeName(13) ),
-				* mlip = BitmapHandler::loadBitmap( siegeH->getSiegeName(14) );
-
-			auto & info = siegeH->town->town->clientInfo;
-			Point moatPos(info.siegePositions[13].x, info.siegePositions[13].y);
-			Point mlipPos(info.siegePositions[14].x, info.siegePositions[14].y);
-
-			if (moat) //eg. tower has no moat
-				blitAt(moat, moatPos.x,moatPos.y, background);
-			if (mlip) //eg. tower has no mlip
-				blitAt(mlip, mlipPos.x, mlipPos.y, background);
-
-			SDL_FreeSurface(moat);
-			SDL_FreeSurface(mlip);
-		}
-	}
-	else
-	{
-		auto bfieldType = curInt->cb->battleGetBattlefieldType();
-
-		if(bfieldType == BattleField::NONE)
-		{
-			logGlobal->error("Invalid battlefield returned for current battle");
-		}
-		else
-		{
-			background = BitmapHandler::loadBitmap(bfieldType.getInfo()->graphics, false);
-		}
-	}
-
-	//preparing graphics for displaying amounts of creatures
-	amountNormal = BitmapHandler::loadBitmap("CMNUMWIN.BMP");
-	CSDL_Ext::alphaTransform(amountNormal);
-	transformPalette(amountNormal, 0.59, 0.19, 0.93);
-
-	amountPositive = BitmapHandler::loadBitmap("CMNUMWIN.BMP");
-	CSDL_Ext::alphaTransform(amountPositive);
-	transformPalette(amountPositive, 0.18, 1.00, 0.18);
-
-	amountNegative = BitmapHandler::loadBitmap("CMNUMWIN.BMP");
-	CSDL_Ext::alphaTransform(amountNegative);
-	transformPalette(amountNegative, 1.00, 0.18, 0.18);
-
-	amountEffNeutral = BitmapHandler::loadBitmap("CMNUMWIN.BMP");
-	CSDL_Ext::alphaTransform(amountEffNeutral);
-	transformPalette(amountEffNeutral, 1.00, 1.00, 0.18);
-
-	//preparing buttons and console
-	bOptions = std::make_shared<CButton>(Point(  3, 561), "icm003.def", CGI->generaltexth->zelp[381], std::bind(&CBattleInterface::bOptionsf,this), SDLK_o);
-	bSurrender = std::make_shared<CButton>(Point( 54, 561), "icm001.def", CGI->generaltexth->zelp[379], std::bind(&CBattleInterface::bSurrenderf,this), SDLK_s);
-	bFlee = std::make_shared<CButton>(Point(105, 561), "icm002.def", CGI->generaltexth->zelp[380], std::bind(&CBattleInterface::bFleef,this), SDLK_r);
-	bAutofight = std::make_shared<CButton>(Point(157, 561), "icm004.def", CGI->generaltexth->zelp[382], std::bind(&CBattleInterface::bAutofightf,this), SDLK_a);
-	bSpell = std::make_shared<CButton>(Point(645, 561), "icm005.def", CGI->generaltexth->zelp[385], std::bind(&CBattleInterface::bSpellf,this), SDLK_c);
-	bWait = std::make_shared<CButton>(Point(696, 561), "icm006.def", CGI->generaltexth->zelp[386], std::bind(&CBattleInterface::bWaitf,this), SDLK_w);
-	bDefence = std::make_shared<CButton>(Point(747, 561), "icm007.def", CGI->generaltexth->zelp[387], std::bind(&CBattleInterface::bDefencef,this), SDLK_d);
-	bDefence->assignedKeys.insert(SDLK_SPACE);
-	bConsoleUp = std::make_shared<CButton>(Point(624, 561), "ComSlide.def", std::make_pair("", ""), std::bind(&CBattleInterface::bConsoleUpf,this), SDLK_UP);
-	bConsoleDown = std::make_shared<CButton>(Point(624, 580), "ComSlide.def", std::make_pair("", ""), std::bind(&CBattleInterface::bConsoleDownf,this), SDLK_DOWN);
-	bConsoleUp->setImageOrder(0, 1, 0, 0);
-	bConsoleDown->setImageOrder(2, 3, 2, 2);
-
-	console = std::make_shared<CBattleConsole>();
-	console->pos.x += 211;
-	console->pos.y += 560;
-	console->pos.w = 406;
-	console->pos.h = 38;
-	if(tacticsMode)
-	{
-		btactNext = std::make_shared<CButton>(Point(213, 560), "icm011.def", std::make_pair("", ""), [&](){ bTacticNextStack(nullptr);}, SDLK_SPACE);
-		btactEnd = std::make_shared<CButton>(Point(419, 560), "icm012.def", std::make_pair("", ""), [&](){ bEndTacticPhase();}, SDLK_RETURN);
-		menu = BitmapHandler::loadBitmap("COPLACBR.BMP");
-	}
-	else
-	{
-		menu = BitmapHandler::loadBitmap("CBAR.BMP");
-	}
-	graphics->blueToPlayersAdv(menu, curInt->playerID);
-
-	//loading hero animations
-	if(hero1) // attacking hero
-	{
-		std::string battleImage;
-		if(!hero1->type->battleImage.empty())
-		{
-			battleImage = hero1->type->battleImage;
-		}
-		else
-		{
-			if(hero1->sex)
-				battleImage = hero1->type->heroClass->imageBattleFemale;
-			else
-				battleImage = hero1->type->heroClass->imageBattleMale;
-		}
-
-		attackingHero = std::make_shared<CBattleHero>(battleImage, false, hero1->tempOwner, hero1->tempOwner == curInt->playerID ? hero1 : nullptr, this);
-
-		auto img = attackingHero->animation->getImage(0, 0, true);
-		if(img)
-			attackingHero->pos = genRect(img->height(), img->width(), pos.x - 43, pos.y - 19);
-	}
-
-
-	if(hero2) // defending hero
-	{
-		std::string battleImage;
-
-		if(!hero2->type->battleImage.empty())
-		{
-			battleImage = hero2->type->battleImage;
-		}
-		else
-		{
-			if(hero2->sex)
-				battleImage = hero2->type->heroClass->imageBattleFemale;
-			else
-				battleImage = hero2->type->heroClass->imageBattleMale;
-		}
-
-		defendingHero = std::make_shared<CBattleHero>(battleImage, true, hero2->tempOwner, hero2->tempOwner == curInt->playerID ? hero2 : nullptr, this);
-
-		auto img = defendingHero->animation->getImage(0, 0, true);
-		if(img)
-			defendingHero->pos = genRect(img->height(), img->width(), pos.x + 693, pos.y - 19);
-	}
-
-
-	//preparing cells and hexes
-	cellBorder = BitmapHandler::loadBitmap("CCELLGRD.BMP");
-	CSDL_Ext::alphaTransform(cellBorder);
-	cellShade = BitmapHandler::loadBitmap("CCELLSHD.BMP");
-	CSDL_Ext::alphaTransform(cellShade);
-	for (int h = 0; h < GameConstants::BFIELD_SIZE; ++h)
-	{
-		auto hex = std::make_shared<CClickableHex>();
-		hex->myNumber = h;
-		hex->pos = hexPosition(h);
-		hex->accessible = true;
-		hex->myInterface = this;
-		bfield.push_back(hex);
-	}
-	//locking occupied positions on batlefield
-	for(const CStack * s : stacks)  //stacks gained at top of this function
-		if(s->initialPosition >= 0) //turrets have position < 0
-			bfield[s->getPosition()]->accessible = false;
-
-	//preparing graphic with cell borders
-	cellBorders = CSDL_Ext::newSurface(background->w, background->h, cellBorder);
-	//copying palette
-	for (int g=0; g<cellBorder->format->palette->ncolors; ++g) //we assume that cellBorders->format->palette->ncolors == 256
-	{
-		cellBorders->format->palette->colors[g] = cellBorder->format->palette->colors[g];
-	}
-	//palette copied
-	for (int i=0; i<GameConstants::BFIELD_HEIGHT; ++i) //rows
-	{
-		for (int j=0; j<GameConstants::BFIELD_WIDTH-2; ++j) //columns
-		{
-			int x = 58 + (i%2==0 ? 22 : 0) + 44*j;
-			int y = 86 + 42 *i;
-			for (int cellX = 0; cellX < cellBorder->w; ++cellX)
-			{
-				for (int cellY = 0; cellY < cellBorder->h; ++cellY)
-				{
-					if (y+cellY < cellBorders->h && x+cellX < cellBorders->w)
-						* ((Uint8*)cellBorders->pixels + (y+cellY) *cellBorders->pitch + (x+cellX)) |= *((Uint8*)cellBorder->pixels + cellY *cellBorder->pitch + cellX);
-				}
-			}
-		}
-	}
-
-	backgroundWithHexes = CSDL_Ext::newSurface(background->w, background->h, screen);
-
-	//preparing obstacle defs
-	auto obst = curInt->cb->battleGetAllObstacles();
-	for(auto & elem : obst)
-	{
-		if(elem->obstacleType == CObstacleInstance::USUAL)
-		{
-			std::string animationName = elem->getInfo().animation;
-
-			auto cached = animationsCache.find(animationName);
-
-			if(cached == animationsCache.end())
-			{
-				auto animation = std::make_shared<CAnimation>(animationName);
-				animationsCache[animationName] = animation;
-				obstacleAnimations[elem->uniqueID] = animation;
-				animation->preload();
-			}
-			else
-			{
-				obstacleAnimations[elem->uniqueID] = cached->second;
-			}
-		}
-		else if (elem->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
-		{
-			std::string animationName = elem->getInfo().animation;
-
-			auto cached = animationsCache.find(animationName);
-
-			if(cached == animationsCache.end())
-			{
-				auto animation = std::make_shared<CAnimation>();
-				animation->setCustom(animationName, 0, 0);
-				animationsCache[animationName] = animation;
-				obstacleAnimations[elem->uniqueID] = animation;
-				animation->preload();
-			}
-			else
-			{
-				obstacleAnimations[elem->uniqueID] = cached->second;
-			}
-		}
-	}
-
-	for(auto hex : bfield)
-		addChild(hex.get());
-
-	if(tacticsMode)
-		bTacticNextStack();
-
-	CCS->musich->stopMusic();
-	battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds);
-	auto onIntroPlayed = [&]()
-	{
-		if(LOCPLINT->battleInt)
-		{
-			CCS->musich->playMusicFromSet("battle", true, true);
-			battleActionsStarted = true;
-			blockUI(settings["session"]["spectate"].Bool());
-			battleIntroSoundChannel = -1;
-		}
-	};
-
-	CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed);
-
-	currentAction = PossiblePlayerBattleAction::INVALID;
-	selectedAction = PossiblePlayerBattleAction::INVALID;
-	addUsedEvents(RCLICK | MOVE | KEYBOARD);
-	queue->update();
-	blockUI(true);
-}
-
-CBattleInterface::~CBattleInterface()
-{
-	CPlayerInterface::battleInt = nullptr;
-	givenCommand.cond.notify_all(); //that two lines should make any activeStack waiting thread to finish
-
-	if (active) //dirty fix for #485
-	{
-		deactivate();
-	}
-	SDL_FreeSurface(background);
-	SDL_FreeSurface(menu);
-	SDL_FreeSurface(amountNormal);
-	SDL_FreeSurface(amountNegative);
-	SDL_FreeSurface(amountPositive);
-	SDL_FreeSurface(amountEffNeutral);
-	SDL_FreeSurface(cellBorders);
-	SDL_FreeSurface(backgroundWithHexes);
-
-
-	SDL_FreeSurface(cellBorder);
-	SDL_FreeSurface(cellShade);
-
-	delete siegeH;
-
-	//TODO: play AI tracks if battle was during AI turn
-	//if (!curInt->makingTurn)
-	//CCS->musich->playMusicFromSet(CCS->musich->aiMusics, -1);
-
-	if (adventureInt && adventureInt->selection)
-	{
-		const auto & terrain = *(LOCPLINT->cb->getTile(adventureInt->selection->visitablePos())->terType);
-		CCS->musich->playMusicFromSet("terrain", terrain.name, true, false);
-	}
-	animsAreDisplayed.setn(false);
-}
-
-void CBattleInterface::setPrintCellBorders(bool set)
-{
-	Settings cellBorders = settings.write["battle"]["cellBorders"];
-	cellBorders->Bool() = set;
-
-	redrawBackgroundWithHexes(activeStack);
-	GH.totalRedraw();
-}
-
-void CBattleInterface::setPrintStackRange(bool set)
-{
-	Settings stackRange = settings.write["battle"]["stackRange"];
-	stackRange->Bool() = set;
-
-	redrawBackgroundWithHexes(activeStack);
-	GH.totalRedraw();
-}
-
-void CBattleInterface::setPrintMouseShadow(bool set)
-{
-	Settings shadow = settings.write["battle"]["mouseShadow"];
-	shadow->Bool() = set;
-}
-
-void CBattleInterface::activate()
-{
-	if (curInt->isAutoFightOn)
-	{
-		bAutofight->activate();
-		return;
-	}
-
-	CIntObject::activate();
-	bOptions->activate();
-	bSurrender->activate();
-	bFlee->activate();
-	bAutofight->activate();
-	bSpell->activate();
-	bWait->activate();
-	bDefence->activate();
-
-	if (attackingHero)
-		attackingHero->activate();
-	if (defendingHero)
-		defendingHero->activate();
-
-	for (auto hex : bfield)
-		hex->activate();
-
-	if (settings["battle"]["showQueue"].Bool())
-		queue->activate();
-
-	if (tacticsMode)
-	{
-		btactNext->activate();
-		btactEnd->activate();
-	}
-	else
-	{
-		bConsoleUp->activate();
-		bConsoleDown->activate();
-	}
-
-	LOCPLINT->cingconsole->activate();
-}
-
-void CBattleInterface::deactivate()
-{
-	CIntObject::deactivate();
-
-	bOptions->deactivate();
-	bSurrender->deactivate();
-	bFlee->deactivate();
-	bAutofight->deactivate();
-	bSpell->deactivate();
-	bWait->deactivate();
-	bDefence->deactivate();
-
-	for (auto hex : bfield)
-		hex->deactivate();
-
-	if (attackingHero)
-		attackingHero->deactivate();
-	if (defendingHero)
-		defendingHero->deactivate();
-	if (settings["battle"]["showQueue"].Bool())
-		queue->deactivate();
-
-	if (tacticsMode)
-	{
-		btactNext->deactivate();
-		btactEnd->deactivate();
-	}
-	else
-	{
-		bConsoleUp->deactivate();
-		bConsoleDown->deactivate();
-	}
-
-	LOCPLINT->cingconsole->deactivate();
-}
-
-void CBattleInterface::keyPressed(const SDL_KeyboardEvent & key)
-{
-	if(key.keysym.sym == SDLK_q && key.state == SDL_PRESSED)
-	{
-		if(settings["battle"]["showQueue"].Bool()) //hide queue
-			hideQueue();
-		else
-			showQueue();
-
-	}
-	else if(key.keysym.sym == SDLK_f && key.state == SDL_PRESSED)
-	{
-		enterCreatureCastingMode();
-	}
-	else if(key.keysym.sym == SDLK_ESCAPE)
-	{
-		if(!battleActionsStarted)
-			CCS->soundh->stopSound(battleIntroSoundChannel);
-		else
-			endCastingSpell();
-	}
-}
-void CBattleInterface::mouseMoved(const SDL_MouseMotionEvent &sEvent)
-{
-	auto hexItr = std::find_if(bfield.begin(), bfield.end(), [](std::shared_ptr<CClickableHex> hex)
-	{
-		return hex->hovered && hex->strictHovered;
-	});
-
-	handleHex(hexItr == bfield.end() ? -1 : (*hexItr)->myNumber, MOVE);
-}
-
-void CBattleInterface::setBattleCursor(const int myNumber)
-{
-	const CClickableHex & hoveredHex = *bfield[myNumber];
-	CCursorHandler *cursor = CCS->curh;
-
-	const double subdividingAngle = 2.0*M_PI/6.0; // Divide a hex into six sectors.
-	const double hexMidX = hoveredHex.pos.x + hoveredHex.pos.w/2.0;
-	const double hexMidY = hoveredHex.pos.y + hoveredHex.pos.h/2.0;
-	const double cursorHexAngle = M_PI - atan2(hexMidY - cursor->ypos, cursor->xpos - hexMidX) + subdividingAngle/2; //TODO: refactor this nightmare
-	const double sector = fmod(cursorHexAngle/subdividingAngle, 6.0);
-	const int zigzagCorrection = !((myNumber/GameConstants::BFIELD_WIDTH)%2); // Off-by-one correction needed to deal with the odd battlefield rows.
-
-	std::vector<int> sectorCursor; // From left to bottom left.
-	sectorCursor.push_back(8);
-	sectorCursor.push_back(9);
-	sectorCursor.push_back(10);
-	sectorCursor.push_back(11);
-	sectorCursor.push_back(12);
-	sectorCursor.push_back(7);
-
-	const bool doubleWide = activeStack->doubleWide();
-	bool aboveAttackable = true, belowAttackable = true;
-
-	// Exclude directions which cannot be attacked from.
-	// Check to the left.
-	if (myNumber%GameConstants::BFIELD_WIDTH <= 1 || !vstd::contains(occupyableHexes, myNumber - 1))
-	{
-		sectorCursor[0] = -1;
-	}
-	// Check top left, top right as well as above for 2-hex creatures.
-	if (myNumber/GameConstants::BFIELD_WIDTH == 0)
-	{
-			sectorCursor[1] = -1;
-			sectorCursor[2] = -1;
-			aboveAttackable = false;
-	}
-	else
-	{
-		if (doubleWide)
-		{
-			bool attackRow[4] = {true, true, true, true};
-
-			if (myNumber%GameConstants::BFIELD_WIDTH <= 1 || !vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH - 2 + zigzagCorrection))
-				attackRow[0] = false;
-			if (!vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection))
-				attackRow[1] = false;
-			if (!vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH + zigzagCorrection))
-				attackRow[2] = false;
-			if (myNumber%GameConstants::BFIELD_WIDTH >= GameConstants::BFIELD_WIDTH - 2 || !vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH + 1 + zigzagCorrection))
-				attackRow[3] = false;
-
-			if (!(attackRow[0] && attackRow[1]))
-				sectorCursor[1] = -1;
-			if (!(attackRow[1] && attackRow[2]))
-				aboveAttackable = false;
-			if (!(attackRow[2] && attackRow[3]))
-				sectorCursor[2] = -1;
-		}
-		else
-		{
-			if (!vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection))
-				sectorCursor[1] = -1;
-			if (!vstd::contains(occupyableHexes, myNumber - GameConstants::BFIELD_WIDTH + zigzagCorrection))
-				sectorCursor[2] = -1;
-		}
-	}
-	// Check to the right.
-	if (myNumber%GameConstants::BFIELD_WIDTH >= GameConstants::BFIELD_WIDTH - 2 || !vstd::contains(occupyableHexes, myNumber + 1))
-	{
-		sectorCursor[3] = -1;
-	}
-	// Check bottom right, bottom left as well as below for 2-hex creatures.
-	if (myNumber/GameConstants::BFIELD_WIDTH == GameConstants::BFIELD_HEIGHT - 1)
-	{
-		sectorCursor[4] = -1;
-		sectorCursor[5] = -1;
-		belowAttackable = false;
-	}
-	else
-	{
-		if (doubleWide)
-		{
-			bool attackRow[4] = {true, true, true, true};
-
-			if (myNumber%GameConstants::BFIELD_WIDTH <= 1 || !vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH - 2 + zigzagCorrection))
-				attackRow[0] = false;
-			if (!vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection))
-				attackRow[1] = false;
-			if (!vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH + zigzagCorrection))
-				attackRow[2] = false;
-			if (myNumber%GameConstants::BFIELD_WIDTH >= GameConstants::BFIELD_WIDTH - 2 || !vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH + 1 + zigzagCorrection))
-				attackRow[3] = false;
-
-			if (!(attackRow[0] && attackRow[1]))
-				sectorCursor[5] = -1;
-			if (!(attackRow[1] && attackRow[2]))
-				belowAttackable = false;
-			if (!(attackRow[2] && attackRow[3]))
-				sectorCursor[4] = -1;
-		}
-		else
-		{
-			if (!vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH + zigzagCorrection))
-				sectorCursor[4] = -1;
-			if (!vstd::contains(occupyableHexes, myNumber + GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection))
-				sectorCursor[5] = -1;
-		}
-	}
-
-	// Determine index from sector.
-	int cursorIndex;
-	if (doubleWide)
-	{
-		sectorCursor.insert(sectorCursor.begin() + 5, belowAttackable ? 13 : -1);
-		sectorCursor.insert(sectorCursor.begin() + 2, aboveAttackable ? 14 : -1);
-
-		if (sector < 1.5)
-			cursorIndex = static_cast<int>(sector);
-		else if (sector >= 1.5 && sector < 2.5)
-			cursorIndex = 2;
-		else if (sector >= 2.5 && sector < 4.5)
-			cursorIndex = (int) sector + 1;
-		else if (sector >= 4.5 && sector < 5.5)
-			cursorIndex = 6;
-		else
-			cursorIndex = (int) sector + 2;
-	}
-	else
-	{
-		cursorIndex = static_cast<int>(sector);
-	}
-
-	// Generally should NEVER happen, but to avoid the possibility of having endless loop below... [#1016]
-	if (!vstd::contains_if (sectorCursor, [](int sc) { return sc != -1; }))
-	{
-		logGlobal->error("Error: for hex %d cannot find a hex to attack from!", myNumber);
-		attackingHex = -1;
-		return;
-	}
-
-	// Find the closest direction attackable, starting with the right one.
-	// FIXME: Is this really how the original H3 client does it?
-	int i = 0;
-	while (sectorCursor[(cursorIndex + i)%sectorCursor.size()] == -1) //Why hast thou forsaken me?
-		i = i <= 0 ? 1 - i : -i; // 0, 1, -1, 2, -2, 3, -3 etc..
-	int index = (cursorIndex + i)%sectorCursor.size(); //hopefully we get elements from sectorCursor
-	cursor->changeGraphic(ECursor::COMBAT, sectorCursor[index]);
-	switch (index)
-	{
-		case 0:
-			attackingHex = myNumber - 1; //left
-			break;
-		case 1:
-			attackingHex = myNumber - GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection; //top left
-			break;
-		case 2:
-			attackingHex = myNumber - GameConstants::BFIELD_WIDTH + zigzagCorrection; //top right
-			break;
-		case 3:
-			attackingHex = myNumber + 1; //right
-			break;
-		case 4:
-			attackingHex = myNumber + GameConstants::BFIELD_WIDTH + zigzagCorrection; //bottom right
-			break;
-		case 5:
-			attackingHex = myNumber + GameConstants::BFIELD_WIDTH - 1 + zigzagCorrection; //bottom left
-			break;
-	}
-	BattleHex hex(attackingHex);
-	if (!hex.isValid())
-		attackingHex = -1;
-}
-
-void CBattleInterface::clickRight(tribool down, bool previousState)
-{
-	if (!down)
-	{
-		endCastingSpell();
-	}
-}
-
-void CBattleInterface::bOptionsf()
-{
-	if (spellDestSelectMode) //we are casting a spell
-		return;
-
-	CCS->curh->changeGraphic(ECursor::ADVENTURE,0);
-
-	Rect tempRect = genRect(431, 481, 160, 84);
-	tempRect += pos.topLeft();
-	GH.pushIntT<CBattleOptionsWindow>(tempRect, this);
-}
-
-void CBattleInterface::bSurrenderf()
-{
-	if(spellDestSelectMode) //we are casting a spell
-		return;
-
-	int cost = curInt->cb->battleGetSurrenderCost();
-	if(cost >= 0)
-	{
-		std::string enemyHeroName = curInt->cb->battleGetEnemyHero().name;
-		if(enemyHeroName.empty())
-		{
-			logGlobal->warn("Surrender performed without enemy hero, should not happen!");
-			enemyHeroName = "#ENEMY#";
-		}
-
-		std::string surrenderMessage = boost::str(boost::format(CGI->generaltexth->allTexts[32]) % enemyHeroName % cost); //%s states: "I will accept your surrender and grant you and your troops safe passage for the price of %d gold."
-		curInt->showYesNoDialog(surrenderMessage, [this](){ reallySurrender(); }, nullptr);
-	}
-}
-
-void CBattleInterface::bFleef()
-{
-	if (spellDestSelectMode) //we are casting a spell
-		return;
-
-	if ( curInt->cb->battleCanFlee() )
-	{
-		CFunctionList<void()> ony = std::bind(&CBattleInterface::reallyFlee,this);
-		curInt->showYesNoDialog(CGI->generaltexth->allTexts[28], ony, nullptr); //Are you sure you want to retreat?
-	}
-	else
-	{
-		std::vector<std::shared_ptr<CComponent>> comps;
-		std::string heroName;
-		//calculating fleeing hero's name
-		if (attackingHeroInstance)
-			if (attackingHeroInstance->tempOwner == curInt->cb->getMyColor())
-				heroName = attackingHeroInstance->name;
-		if (defendingHeroInstance)
-			if (defendingHeroInstance->tempOwner == curInt->cb->getMyColor())
-				heroName = defendingHeroInstance->name;
-		//calculating text
-		auto txt = boost::format(CGI->generaltexth->allTexts[340]) % heroName; //The Shackles of War are present.  %s can not retreat!
-
-		//printing message
-		curInt->showInfoDialog(boost::to_string(txt), comps);
-	}
-}
-
-void CBattleInterface::reallyFlee()
-{
-	giveCommand(EActionType::RETREAT);
-	CCS->curh->changeGraphic(ECursor::ADVENTURE, 0);
-}
-
-void CBattleInterface::reallySurrender()
-{
-	if (curInt->cb->getResourceAmount(Res::GOLD) < curInt->cb->battleGetSurrenderCost())
-	{
-		curInt->showInfoDialog(CGI->generaltexth->allTexts[29]); //You don't have enough gold!
-	}
-	else
-	{
-		giveCommand(EActionType::SURRENDER);
-		CCS->curh->changeGraphic(ECursor::ADVENTURE, 0);
-	}
-}
-
-void CBattleInterface::bAutofightf()
-{
-	if(spellDestSelectMode) //we are casting a spell
-		return;
-
-	//Stop auto-fight mode
-	if(curInt->isAutoFightOn)
-	{
-		assert(curInt->autofightingAI);
-		curInt->isAutoFightOn = false;
-		logGlobal->trace("Stopping the autofight...");
-	}
-	else if(!curInt->autofightingAI)
-	{
-		curInt->isAutoFightOn = true;
-		blockUI(true);
-
-		auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
-		ai->init(curInt->env, curInt->cb);
-		ai->battleStart(army1, army2, int3(0,0,0), attackingHeroInstance, defendingHeroInstance, curInt->cb->battleGetMySide());
-		curInt->autofightingAI = ai;
-		curInt->cb->registerBattleInterface(ai);
-
-		requestAutofightingAIToTakeAction();
-	}
-}
-
-void CBattleInterface::bSpellf()
-{
-	if (spellDestSelectMode) //we are casting a spell
-		return;
-
-	if (!myTurn)
-		return;
-
-	auto myHero = currentHero();
-	if(!myHero)
-		return;
-
-	CCS->curh->changeGraphic(ECursor::ADVENTURE,0);
-
-	ESpellCastProblem::ESpellCastProblem spellCastProblem = curInt->cb->battleCanCastSpell(myHero, spells::Mode::HERO);
-
-	if(spellCastProblem == ESpellCastProblem::OK)
-	{
-		GH.pushIntT<CSpellWindow>(myHero, curInt.get());
-	}
-	else if (spellCastProblem == ESpellCastProblem::MAGIC_IS_BLOCKED)
-	{
-		//TODO: move to spell mechanics, add more information to spell cast problem
-		//Handle Orb of Inhibition-like effects -> we want to display dialog with info, why casting is impossible
-		auto blockingBonus = currentHero()->getBonusLocalFirst(Selector::type()(Bonus::BLOCK_ALL_MAGIC));
-		if (!blockingBonus)
-			return;
-
-		if (blockingBonus->source == Bonus::ARTIFACT)
-		{
-			const auto artID = ArtifactID(blockingBonus->sid);
-			//If we have artifact, put name of our hero. Otherwise assume it's the enemy.
-			//TODO check who *really* is source of bonus
-			std::string heroName = myHero->hasArt(artID) ? myHero->name : enemyHero().name;
-
-			//%s wields the %s, an ancient artifact which creates a p dead to all magic.
-			LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[683])
-										% heroName % CGI->artifacts()->getByIndex(artID)->getName()));
-		}
-	}
-}
-
-void CBattleInterface::bWaitf()
-{
-	if (spellDestSelectMode) //we are casting a spell
-		return;
-
-	if (activeStack != nullptr)
-		giveCommand(EActionType::WAIT);
-}
-
-void CBattleInterface::bDefencef()
-{
-	if (spellDestSelectMode) //we are casting a spell
-		return;
-
-	if (activeStack != nullptr)
-		giveCommand(EActionType::DEFEND);
-}
-
-void CBattleInterface::bConsoleUpf()
-{
-	if (spellDestSelectMode) //we are casting a spell
-		return;
-
-	console->scrollUp();
-}
-
-void CBattleInterface::bConsoleDownf()
-{
-	if (spellDestSelectMode) //we are casting a spell
-		return;
-
-	console->scrollDown();
-}
-
-void CBattleInterface::unitAdded(const CStack * stack)
-{
-	creDir[stack->ID] = stack->side == BattleSide::ATTACKER; // must be set before getting stack position
-
-	Point coords = CClickableHex::getXYUnitAnim(stack->getPosition(), stack, this);
-
-	if(stack->initialPosition < 0) //turret
-	{
-		const CCreature *turretCreature = CGI->creh->objects[siegeH->town->town->clientInfo.siegeShooter];
-
-		creAnims[stack->ID] = AnimationControls::getAnimation(turretCreature);
-
-		// Turret positions are read out of the config/wall_pos.txt
-		int posID = 0;
-		switch (stack->initialPosition)
-		{
-		case -2: // keep creature
-			posID = 18;
-			break;
-		case -3: // bottom creature
-			posID = 19;
-			break;
-		case -4: // upper creature
-			posID = 20;
-			break;
-		}
-
-		if (posID != 0)
-		{
-			coords.x = siegeH->town->town->clientInfo.siegePositions[posID].x + this->pos.x;
-			coords.y = siegeH->town->town->clientInfo.siegePositions[posID].y + this->pos.y;
-		}
-		creAnims[stack->ID]->pos.h = 225;
-	}
-	else
-	{
-		creAnims[stack->ID] = AnimationControls::getAnimation(stack->getCreature());
-		creAnims[stack->ID]->onAnimationReset += std::bind(&onAnimationFinished, stack, creAnims[stack->ID]);
-		creAnims[stack->ID]->pos.h = creAnims[stack->ID]->getHeight();
-	}
-	creAnims[stack->ID]->pos.x = coords.x;
-	creAnims[stack->ID]->pos.y = coords.y;
-	creAnims[stack->ID]->pos.w = creAnims[stack->ID]->getWidth();
-	creAnims[stack->ID]->setType(CCreatureAnim::HOLDING);
-
-	//loading projectiles for units
-	if(stack->isShooter())
-	{
-		initStackProjectile(stack);
-	}
-}
-
-void CBattleInterface::initStackProjectile(const CStack * stack)
-{
-	const CCreature * creature;//creature whose shots should be loaded
-	if(stack->getCreature()->idNumber == CreatureID::ARROW_TOWERS)
-		creature = CGI->creh->objects[siegeH->town->town->clientInfo.siegeShooter];
-	else
-		creature = stack->getCreature();
-
-	if (creature->animation.projectileRay.empty())
-	{
-		std::shared_ptr<CAnimation> projectile = std::make_shared<CAnimation>(creature->animation.projectileImageName);
-		projectile->preload();
-
-		if(projectile->size(1) != 0)
-			logAnim->error("Expected empty group 1 in stack projectile");
-		else
-			projectile->createFlippedGroup(0, 1);
-
-		idToProjectile[stack->getCreature()->idNumber] = projectile;
-	}
-	else
-	{
-		idToRay[stack->getCreature()->idNumber] = creature->animation.projectileRay;
-	}
-}
-
-void CBattleInterface::stackRemoved(uint32_t stackID)
-{
-	if (activeStack != nullptr)
-	{
-		if (activeStack->ID == stackID)
-		{
-			BattleAction *action = new BattleAction();
-			action->side = defendingHeroInstance ? (curInt->playerID == defendingHeroInstance->tempOwner) : false;
-			action->actionType = EActionType::CANCEL;
-			action->stackNumber = activeStack->ID;
-			givenCommand.setn(action);
-			setActiveStack(nullptr);
-		}
-	}
-
-	//todo: ensure that ghost stack animation has fadeout effect
-
-	redrawBackgroundWithHexes(activeStack);
-	queue->update();
-}
-
-void CBattleInterface::stackActivated(const CStack *stack) //TODO: check it all before game state is changed due to abilities
-{
-	stackToActivate = stack;
-	waitForAnims();
-	if (stackToActivate) //during waiting stack may have gotten activated through show
-		activateStack();
-}
-
-void CBattleInterface::stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance)
-{
-	addNewAnim(new CMovementAnimation(this, stack, destHex, distance));
-	waitForAnims();
-}
-
-void CBattleInterface::stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos)
-{
-	for(auto & attackedInfo : attackedInfos)
-	{
-		//if (!attackedInfo.cloneKilled) //FIXME: play dead animation for cloned creature before it vanishes
-			addNewAnim(new CDefenceAnimation(attackedInfo, this));
-
-		if(attackedInfo.rebirth)
-		{
-			displayEffect(50, attackedInfo.defender->getPosition()); //TODO: play reverse death animation
-			CCS->soundh->playSound(soundBase::RESURECT);
-		}
-	}
-	waitForAnims();
-
-	std::array<int, 2> killedBySide = {0, 0};
-
-	int targets = 0;
-	for(const StackAttackedInfo & attackedInfo : attackedInfos)
-	{
-		++targets;
-
-		ui8 side = attackedInfo.defender->side;
-		killedBySide.at(side) += attackedInfo.amountKilled;
-	}
-
-	for(ui8 side = 0; side < 2; side++)
-	{
-		if(killedBySide.at(side) > killedBySide.at(1-side))
-			setHeroAnimation(side, 2);
-		else if(killedBySide.at(side) < killedBySide.at(1-side))
-			setHeroAnimation(side, 3);
-	}
-
-	for (auto & attackedInfo : attackedInfos)
-	{
-		if (attackedInfo.rebirth)
-			creAnims[attackedInfo.defender->ID]->setType(CCreatureAnim::HOLDING);
-		if (attackedInfo.cloneKilled)
-			stackRemoved(attackedInfo.defender->ID);
-	}
-}
-
-void CBattleInterface::stackAttacking( const CStack *attacker, BattleHex dest, const CStack *attacked, bool shooting )
-{
-	if (shooting)
-	{
-		addNewAnim(new CShootingAnimation(this, attacker, dest, attacked));
-	}
-	else
-	{
-		addNewAnim(new CMeleeAttackAnimation(this, attacker, dest, attacked));
-	}
-	//waitForAnims();
-}
-
-void CBattleInterface::newRoundFirst( int round )
-{
-	waitForAnims();
-}
-
-void CBattleInterface::newRound(int number)
-{
-	console->addText(CGI->generaltexth->allTexts[412]);
-}
-
-void CBattleInterface::giveCommand(EActionType action, BattleHex tile, si32 additional)
-{
-	const CStack * actor = nullptr;
-	if(action != EActionType::HERO_SPELL && action != EActionType::RETREAT && action != EActionType::SURRENDER)
-	{
-		actor = activeStack;
-	}
-
-	auto side = curInt->cb->playerToSide(curInt->playerID);
-	if(!side)
-	{
-		logGlobal->error("Player %s is not in battle", curInt->playerID.getStr());
-		return;
-	}
-
-	auto ba = new BattleAction(); //is deleted in CPlayerInterface::activeStack()
-	ba->side = side.get();
-	ba->actionType = action;
-	ba->aimToHex(tile);
-	ba->actionSubtype = additional;
-
-	sendCommand(ba, actor);
-}
-
-void CBattleInterface::sendCommand(BattleAction *& command, const CStack * actor)
-{
-	command->stackNumber = actor ? actor->unitId() : ((command->side == BattleSide::ATTACKER) ? -1 : -2);
-
-	if(!tacticsMode)
-	{
-		logGlobal->trace("Setting command for %s", (actor ? actor->nodeName() : "hero"));
-		myTurn = false;
-		setActiveStack(nullptr);
-		givenCommand.setn(command);
-	}
-	else
-	{
-		curInt->cb->battleMakeTacticAction(command);
-		vstd::clear_pointer(command);
-		setActiveStack(nullptr);
-		//next stack will be activated when action ends
-	}
-}
-
-bool CBattleInterface::isTileAttackable(const BattleHex & number) const
-{
-	for (auto & elem : occupyableHexes)
-	{
-		if (BattleHex::mutualPosition(elem, number) != -1 || elem == number)
-			return true;
-	}
-	return false;
-}
-
-bool CBattleInterface::isCatapultAttackable(BattleHex hex) const
-{
-	if (!siegeH || tacticsMode) return false;
-
-	auto wallPart = curInt->cb->battleHexToWallPart(hex);
-	if (!curInt->cb->isWallPartPotentiallyAttackable(wallPart)) return false;
-
-	auto state = curInt->cb->battleGetWallState(static_cast<int>(wallPart));
-	return state != EWallState::DESTROYED && state != EWallState::NONE;
-}
-
-const CGHeroInstance * CBattleInterface::getActiveHero()
-{
-	const CStack *attacker = activeStack;
-	if(!attacker)
-	{
-		return nullptr;
-	}
-
-	if(attacker->side == BattleSide::ATTACKER)
-	{
-		return attackingHeroInstance;
-	}
-
-	return defendingHeroInstance;
-}
-
-void CBattleInterface::hexLclicked(int whichOne)
-{
-	handleHex(whichOne, LCLICK);
-}
-
-void CBattleInterface::stackIsCatapulting(const CatapultAttack & ca)
-{
-	if (ca.attacker != -1)
-	{
-		const CStack *stack = curInt->cb->battleGetStackByID(ca.attacker);
-		for (auto attackInfo : ca.attackedParts)
-		{
-			addNewAnim(new CShootingAnimation(this, stack, attackInfo.destinationTile, nullptr, true, attackInfo.damageDealt));
-		}
-	}
-	else
-	{
-		//no attacker stack, assume spell-related (earthquake) - only hit animation
-		for (auto attackInfo : ca.attackedParts)
-		{
-			Point destPos = CClickableHex::getXYUnitAnim(attackInfo.destinationTile, nullptr, this) + Point(99, 120);
-
-			addNewAnim(new CEffectAnimation(this, "SGEXPL.DEF", destPos.x, destPos.y));
-		}
-	}
-
-	waitForAnims();
-
-	for (auto attackInfo : ca.attackedParts)
-	{
-		int wallId = attackInfo.attackedPart + 2;
-		//gate state changing handled separately
-		if (wallId == SiegeHelper::GATE)
-			continue;
-
-		SDL_FreeSurface(siegeH->walls[wallId]);
-		siegeH->walls[wallId] = BitmapHandler::loadBitmap(
-			siegeH->getSiegeName(wallId, curInt->cb->battleGetWallState(attackInfo.attackedPart)));
-	}
-}
-
-void CBattleInterface::battleFinished(const BattleResult& br)
-{
-	bresult = &br;
-	{
-		auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim);
-		animsAreDisplayed.waitUntil(false);
-	}
-	setActiveStack(nullptr);
-	displayBattleFinished();
-}
-
-void CBattleInterface::displayBattleFinished()
-{
-	CCS->curh->changeGraphic(ECursor::ADVENTURE,0);
-	curInt->waitWhileDialog();
-	if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-skip-battle-result"].Bool())
-	{
-		close();
-		return;
-	}
-
-	GH.pushInt(std::make_shared<CBattleResultWindow>(*bresult, *(this->curInt)));
-	curInt->waitWhileDialog(); // Avoid freeze when AI end turn after battle. Check bug #1897
-	CPlayerInterface::battleInt = nullptr;
-}
-
-void CBattleInterface::spellCast(const BattleSpellCast * sc)
-{
-	const SpellID spellID = sc->spellID;
-	const CSpell * spell = spellID.toSpell();
-
-	if(!spell)
-		return;
-
-	const std::string & castSoundPath = spell->getCastSound();
-
-	if (!castSoundPath.empty())
-		CCS->soundh->playSound(castSoundPath);
-
-	const auto casterStackID = sc->casterStack;
-	const CStack * casterStack = nullptr;
-	if(casterStackID >= 0)
-	{
-		casterStack = curInt->cb->battleGetStackByID(casterStackID);
-	}
-
-	Point srccoord = (sc->side ? Point(770, 60) : Point(30, 60)) + pos;	//hero position by default
-	{
-		if(casterStack != nullptr)
-		{
-			srccoord = CClickableHex::getXYUnitAnim(casterStack->getPosition(), casterStack, this);
-			srccoord.x += 250;
-			srccoord.y += 240;
-		}
-	}
-
-	if(casterStack != nullptr && sc->activeCast)
-	{
-		//todo: custom cast animation for hero
-		displaySpellCast(spellID, casterStack->getPosition());
-
-		addNewAnim(new CCastAnimation(this, casterStack, sc->tile, curInt->cb->battleGetStackByPos(sc->tile)));
-	}
-
-	waitForAnims(); //wait for cast animation
-
-	//playing projectile animation
-	if (sc->tile.isValid())
-	{
-		Point destcoord = CClickableHex::getXYUnitAnim(sc->tile, curInt->cb->battleGetStackByPos(sc->tile), this); //position attacked by projectile
-		destcoord.x += 250; destcoord.y += 240;
-
-		//animation angle
-		double angle = atan2(static_cast<double>(destcoord.x - srccoord.x), static_cast<double>(destcoord.y - srccoord.y));
-		bool Vflip = (angle < 0);
-		if (Vflip)
-			angle = -angle;
-
-		std::string animToDisplay = spell->animationInfo.selectProjectile(angle);
-
-		if(!animToDisplay.empty())
-		{
-			//TODO: calculate inside CEffectAnimation
-			std::shared_ptr<CAnimation> tmp = std::make_shared<CAnimation>(animToDisplay);
-			tmp->load(0, 0);
-			auto first = tmp->getImage(0, 0);
-
-			//displaying animation
-			double diffX = (destcoord.x - srccoord.x)*(destcoord.x - srccoord.x);
-			double diffY = (destcoord.y - srccoord.y)*(destcoord.y - srccoord.y);
-			double distance = sqrt(diffX + diffY);
-
-			int steps = static_cast<int>(distance / AnimationControls::getSpellEffectSpeed() + 1);
-			int dx = (destcoord.x - srccoord.x - first->width())/steps;
-			int dy = (destcoord.y - srccoord.y - first->height())/steps;
-
-			addNewAnim(new CEffectAnimation(this, animToDisplay, srccoord.x, srccoord.y, dx, dy, Vflip));
-		}
-	}
-
-	waitForAnims(); //wait for projectile animation
-
-	displaySpellHit(spellID, sc->tile);
-
-	//queuing affect animation
-	for(auto & elem : sc->affectedCres)
-	{
-		auto stack = curInt->cb->battleGetStackByID(elem, false);
-		if(stack)
-			displaySpellEffect(spellID, stack->getPosition());
-	}
-
-	//queuing additional animation
-	for(auto & elem : sc->customEffects)
-	{
-		auto stack = curInt->cb->battleGetStackByID(elem.stack, false);
-		if(stack)
-			displayEffect(elem.effect, stack->getPosition());
-	}
-
-	waitForAnims();
-	//mana absorption
-	if (sc->manaGained > 0)
-	{
-		Point leftHero = Point(15, 30) + pos;
-		Point rightHero = Point(755, 30) + pos;
-		addNewAnim(new CEffectAnimation(this, sc->side ? "SP07_A.DEF" : "SP07_B.DEF", leftHero.x, leftHero.y, 0, 0, false));
-		addNewAnim(new CEffectAnimation(this, sc->side ? "SP07_B.DEF" : "SP07_A.DEF", rightHero.x, rightHero.y, 0, 0, false));
-	}
-}
-
-void CBattleInterface::battleStacksEffectsSet(const SetStackEffect & sse)
-{
-	if(activeStack != nullptr)
-		redrawBackgroundWithHexes(activeStack);
-}
-
-void CBattleInterface::setHeroAnimation(ui8 side, int phase)
-{
-	if(side == BattleSide::ATTACKER)
-	{
-		if(attackingHero)
-			attackingHero->setPhase(phase);
-	}
-	else
-	{
-		if(defendingHero)
-			defendingHero->setPhase(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)
-	{
-		std::string formatted = line.toString();
-		boost::algorithm::trim(formatted);
-		if(!console->addText(formatted))
-			logGlobal->warn("Too long battle log line");
-	}
-}
-
-void CBattleInterface::displayCustomEffects(const std::vector<CustomEffectInfo> & customEffects)
-{
-	for(const CustomEffectInfo & one : customEffects)
-	{
-		if(one.sound != 0)
-			CCS->soundh->playSound(soundBase::soundID(one.sound));
-		const CStack * s = curInt->cb->battleGetStackByID(one.stack, false);
-		if(s && one.effect != 0)
-			displayEffect(one.effect, s->getPosition());
-	}
-}
-
-void CBattleInterface::displayEffect(ui32 effect, BattleHex destTile)
-{
-	std::string customAnim = graphics->battleACToDef[effect][0];
-
-	addNewAnim(new CEffectAnimation(this, customAnim, destTile));
-}
-
-void CBattleInterface::displaySpellAnimationQueue(const CSpell::TAnimationQueue & q, BattleHex destinationTile)
-{
-	for(const CSpell::TAnimation & animation : q)
-	{
-		if(animation.pause > 0)
-			addNewAnim(new CDummyAnimation(this, animation.pause));
-		else
-			addNewAnim(new CEffectAnimation(this, animation.resourceName, destinationTile, false, animation.verticalPosition == VerticalPosition::BOTTOM));
-	}
-}
-
-void CBattleInterface::displaySpellCast(SpellID spellID, BattleHex destinationTile)
-{
-	const CSpell * spell = spellID.toSpell();
-
-	if(spell)
-		displaySpellAnimationQueue(spell->animationInfo.cast, destinationTile);
-}
-
-void CBattleInterface::displaySpellEffect(SpellID spellID, BattleHex destinationTile)
-{
-	const CSpell *spell = spellID.toSpell();
-
-	if(spell)
-		displaySpellAnimationQueue(spell->animationInfo.affect, destinationTile);
-}
-
-void CBattleInterface::displaySpellHit(SpellID spellID, BattleHex destinationTile)
-{
-	const CSpell * spell = spellID.toSpell();
-
-	if(spell)
-		displaySpellAnimationQueue(spell->animationInfo.hit, destinationTile);
-}
-
-void CBattleInterface::battleTriggerEffect(const BattleTriggerEffect & bte)
-{
-	const CStack * stack = curInt->cb->battleGetStackByID(bte.stackID);
-	if(!stack)
-	{
-		logGlobal->error("Invalid stack ID %d", bte.stackID);
-		return;
-	}
-	//don't show animation when no HP is regenerated
-	switch(bte.effect)
-	{
-		//TODO: move to bonus type handler
-		case Bonus::HP_REGENERATION:
-			displayEffect(74, stack->getPosition());
-			CCS->soundh->playSound(soundBase::REGENER);
-			break;
-		case Bonus::MANA_DRAIN:
-			displayEffect(77, stack->getPosition());
-			CCS->soundh->playSound(soundBase::MANADRAI);
-			break;
-		case Bonus::POISON:
-			displayEffect(67, stack->getPosition());
-			CCS->soundh->playSound(soundBase::POISON);
-			break;
-		case Bonus::FEAR:
-			displayEffect(15, stack->getPosition());
-			CCS->soundh->playSound(soundBase::FEAR);
-			break;
-		case Bonus::MORALE:
-		{
-			std::string hlp = CGI->generaltexth->allTexts[33];
-			boost::algorithm::replace_first(hlp,"%s",(stack->getName()));
-			displayEffect(20,stack->getPosition());
-			CCS->soundh->playSound(soundBase::GOODMRLE);
-			console->addText(hlp);
-			break;
-		}
-		default:
-			return;
-	}
-	//waitForAnims(); //fixme: freezes game :?
-}
-
-void CBattleInterface::setAnimSpeed(int set)
-{
-	Settings speed = settings.write["battle"]["animationSpeed"];
-	speed->Float() = float(set) / 100;
-}
-
-int CBattleInterface::getAnimSpeed() const
-{
-	if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-battle-speed"].isNull())
-		return static_cast<int>(vstd::round(settings["session"]["spectate-battle-speed"].Float() *100));
-
-	return static_cast<int>(vstd::round(settings["battle"]["animationSpeed"].Float() *100));
-}
-
-CPlayerInterface *CBattleInterface::getCurrentPlayerInterface() const
-{
-	return curInt.get();
-}
-
-bool CBattleInterface::shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex)
-{
-	Point begPosition = CClickableHex::getXYUnitAnim(oldPos,stack, this);
-	Point endPosition = CClickableHex::getXYUnitAnim(nextHex, stack, this);
-
-	if((begPosition.x > endPosition.x) && creDir[stack->ID])
-		return true;
-	else if((begPosition.x < endPosition.x) && !creDir[stack->ID])
-		return true;
-
-	return false;
-}
-
-void CBattleInterface::setActiveStack(const CStack *stack)
-{
-	if (activeStack) // update UI
-		creAnims[activeStack->ID]->setBorderColor(AnimationControls::getNoBorder());
-
-	activeStack = stack;
-
-	if (activeStack) // update UI
-		creAnims[activeStack->ID]->setBorderColor(AnimationControls::getGoldBorder());
-
-	blockUI(activeStack == nullptr);
-}
-
-void CBattleInterface::setHoveredStack(const CStack *stack)
-{
-	if (mouseHoveredStack)
-		creAnims[mouseHoveredStack->ID]->setBorderColor(AnimationControls::getNoBorder());
-
-	// stack must be alive and not active (which uses gold border instead)
-	if (stack && stack->alive() && stack != activeStack)
-	{
-		mouseHoveredStack = stack;
-
-		if (mouseHoveredStack)
-		{
-			creAnims[mouseHoveredStack->ID]->setBorderColor(AnimationControls::getBlueBorder());
-			if (creAnims[mouseHoveredStack->ID]->framesInGroup(CCreatureAnim::MOUSEON) > 0)
-				creAnims[mouseHoveredStack->ID]->playOnce(CCreatureAnim::MOUSEON);
-		}
-	}
-	else
-		mouseHoveredStack = nullptr;
-}
-
-void CBattleInterface::activateStack()
-{
-	if(!battleActionsStarted)
-		return; //"show" function should re-call this function
-
-	myTurn = true;
-	if (!!attackerInt && defenderInt) //hotseat -> need to pick which interface "takes over" as active
-		curInt = attackerInt->playerID == stackToActivate->owner ? attackerInt : defenderInt;
-
-	setActiveStack(stackToActivate);
-	stackToActivate = nullptr;
-	const CStack * s = activeStack;
-	if(!s)
-		return;
-
-	queue->update();
-	redrawBackgroundWithHexes(activeStack);
-
-	//set casting flag to true if creature can use it to not check it every time
-	const auto spellcaster = s->getBonusLocalFirst(Selector::type()(Bonus::SPELLCASTER)),
-		randomSpellcaster = s->getBonusLocalFirst(Selector::type()(Bonus::RANDOM_SPELLCASTER));
-	if(s->canCast() && (spellcaster || randomSpellcaster))
-	{
-		stackCanCastSpell = true;
-		if(randomSpellcaster)
-			creatureSpellToCast = -1; //spell will be set later on cast
-		else
-			creatureSpellToCast = curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), s, CBattleInfoCallback::RANDOM_AIMED); //faerie dragon can cast only one spell until their next move
-		//TODO: what if creature can cast BOTH random genie spell and aimed spell?
-		//TODO: faerie dragon type spell should be selected by server
-	}
-	else
-	{
-		stackCanCastSpell = false;
-		creatureSpellToCast = -1;
-	}
-
-	possibleActions = getPossibleActionsForStack(s);
-
-	GH.fakeMouseMove();
-}
-
-void CBattleInterface::endCastingSpell()
-{
-	if(spellDestSelectMode)
-	{
-		spellToCast.reset();
-
-		sp = nullptr;
-		spellDestSelectMode = false;
-		CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER);
-
-		if(activeStack)
-		{
-			possibleActions = getPossibleActionsForStack(activeStack); //restore actions after they were cleared
-			myTurn = true;
-		}
-	}
-	else
-	{
-		if(activeStack)
-		{
-			possibleActions = getPossibleActionsForStack(activeStack);
-			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 (!activeStack)
-		return;
-
-	if (!stackCanCastSpell)
-		return;
-
-	//random spellcaster
-	if (creatureSpellToCast == -1)
-		return;
-
-	if (vstd::contains(possibleActions, PossiblePlayerBattleAction::NO_LOCATION))
-	{
-		const spells::Caster * caster = activeStack;
-		const CSpell * spell = SpellID(creatureSpellToCast).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, creatureSpellToCast);
-			selectedStack = nullptr;
-
-			CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER);
-		}
-	}
-	else
-	{
-		possibleActions = getPossibleActionsForStack(activeStack);
-
-		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 = creatureSpellToCast;
-	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);
-
-	if(action->actionType == EActionType::HERO_SPELL)
-		setHeroAnimation(action->side, 0);
-
-	//check if we should reverse stacks
-	//for some strange reason, it's not enough
-	TStacks stacks = curInt->cb->battleGetStacks(CBattleCallback::MINE_AND_ENEMY);
-
-	for (const CStack *s : stacks)
-	{
-		if (s && creDir[s->ID] != (s->side == BattleSide::ATTACKER) && s->alive()
-		   && creAnims[s->ID]->isIdle())
-		{
-			addNewAnim(new CReverseAnimation(this, s, s->getPosition(), false));
-		}
-	}
-
-	queue->update();
-
-	if (tacticsMode) //stack ended movement in tactics phase -> select the next one
-		bTacticNextStack(stack);
-
-	if(action->actionType == EActionType::HERO_SPELL) //we have activated next stack after sending request that has been just realized -> blockmap due to movement has changed
-		redrawBackgroundWithHexes(activeStack);
-
-	if (activeStack && !animsAreDisplayed.get() && pendingAnims.empty() && !active)
-	{
-		logGlobal->warn("Something wrong... interface was deactivated but there is no animation. Reactivating...");
-		blockUI(false);
-	}
-	else
-	{
-		// block UI if no active stack (e.g. enemy turn);
-		blockUI(activeStack == nullptr);
-	}
-}
-
-void CBattleInterface::hideQueue()
-{
-	Settings showQueue = settings.write["battle"]["showQueue"];
-	showQueue->Bool() = false;
-
-	queue->deactivate();
-
-	if (!queue->embedded)
-	{
-		moveBy(Point(0, -queue->pos.h / 2));
-		GH.totalRedraw();
-	}
-}
-
-void CBattleInterface::showQueue()
-{
-	Settings showQueue = settings.write["battle"]["showQueue"];
-	showQueue->Bool() = true;
-
-	queue->activate();
-
-	if (!queue->embedded)
-	{
-		moveBy(Point(0, +queue->pos.h / 2));
-		GH.totalRedraw();
-	}
-}
-
-void CBattleInterface::blockUI(bool on)
-{
-	bool canCastSpells = false;
-	auto hero = curInt->cb->battleGetMyHero();
-
-	if(hero)
-	{
-		ESpellCastProblem::ESpellCastProblem spellcastingProblem = curInt->cb->battleCanCastSpell(hero, spells::Mode::HERO);
-
-		//if magic is blocked, we leave button active, so the message can be displayed after button click
-		canCastSpells = spellcastingProblem == ESpellCastProblem::OK || spellcastingProblem == ESpellCastProblem::MAGIC_IS_BLOCKED;
-	}
-
-	bool canWait = activeStack ? !activeStack->waitedThisTurn : false;
-
-	bOptions->block(on);
-	bFlee->block(on || !curInt->cb->battleCanFlee());
-	bSurrender->block(on || curInt->cb->battleGetSurrenderCost() < 0);
-
-	// block only if during enemy turn and auto-fight is off
-	// otherwise - crash on accessing non-exisiting active stack
-	bAutofight->block(!curInt->isAutoFightOn && !activeStack);
-
-	if (tacticsMode && btactEnd && btactNext)
-	{
-		btactNext->block(on);
-		btactEnd->block(on);
-	}
-	else
-	{
-		bConsoleUp->block(on);
-		bConsoleDown->block(on);
-	}
-
-
-	bSpell->block(on || tacticsMode || !canCastSpells);
-	bWait->block(on || tacticsMode || !canWait);
-	bDefence->block(on || tacticsMode);
-}
-
-void CBattleInterface::startAction(const BattleAction* action)
-{
-	//setActiveStack(nullptr);
-	setHoveredStack(nullptr);
-	blockUI(true);
-
-	if(action->actionType == EActionType::END_TACTIC_PHASE)
-	{
-		SDL_FreeSurface(menu);
-		menu = BitmapHandler::loadBitmap("CBAR.bmp");
-
-		graphics->blueToPlayersAdv(menu, curInt->playerID);
-		return;
-	}
-
-	const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber);
-
-	if (stack)
-	{
-		queue->update();
-	}
-	else
-	{
-		assert(action->actionType == EActionType::HERO_SPELL); //only cast spell is valid action without acting stack number
-	}
-
-	auto actionTarget = action->getTarget(curInt->cb.get());
-
-	if(action->actionType == EActionType::WALK
-		|| (action->actionType == EActionType::WALK_AND_ATTACK && actionTarget.at(0).hexValue != stack->getPosition()))
-	{
-		assert(stack);
-		moveStarted = true;
-		if (creAnims[action->stackNumber]->framesInGroup(CCreatureAnim::MOVE_START))
-		{
-			pendingAnims.push_back(std::make_pair(new CMovementStartAnimation(this, stack), false));
-		}
-
-		if(shouldRotate(stack, stack->getPosition(), actionTarget.at(0).hexValue))
-			pendingAnims.push_back(std::make_pair(new CReverseAnimation(this, stack, stack->getPosition(), true), false));
-	}
-
-	redraw(); // redraw after deactivation, including proper handling of hovered hexes
-
-	if(action->actionType == EActionType::HERO_SPELL) //when hero casts spell
-	{
-		setHeroAnimation(action->side, 4);
-		return;
-	}
-
-	if (!stack)
-	{
-		logGlobal->error("Something wrong with stackNumber in actionStarted. Stack number: %d", action->stackNumber);
-		return;
-	}
-
-	int txtid = 0;
-	switch(action->actionType)
-	{
-	case EActionType::WAIT:
-		txtid = 136;
-		break;
-	case EActionType::BAD_MORALE:
-		txtid = -34; //negative -> no separate singular/plural form
-		displayEffect(30, stack->getPosition());
-		CCS->soundh->playSound(soundBase::BADMRLE);
-		break;
-	}
-
-	if(txtid != 0)
-		console->addText(stack->formatGeneralMessage(txtid));
-
-	//displaying special abilities
-	switch(action->actionType)
-	{
-		case EActionType::STACK_HEAL:
-			displayEffect(74, actionTarget.at(0).hexValue);
-			CCS->soundh->playSound(soundBase::REGENER);
-			break;
-	}
-}
-
-void CBattleInterface::waitForAnims()
-{
-	auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim);
-	animsAreDisplayed.waitWhileTrue();
-}
-
-void CBattleInterface::bEndTacticPhase()
-{
-	setActiveStack(nullptr);
-	blockUI(true);
-	tacticsMode = false;
-}
-
-static bool immobile(const CStack *s)
-{
-	return !s->Speed(0, true); //should bound stacks be immobile?
-}
-
-void CBattleInterface::bTacticNextStack(const CStack * current)
-{
-	if (!current)
-		current = activeStack;
-
-	//no switching stacks when the current one is moving
-	waitForAnims();
-
-	TStacks stacksOfMine = tacticianInterface->cb->battleGetStacks(CBattleCallback::ONLY_MINE);
-	vstd::erase_if (stacksOfMine, &immobile);
-	if (stacksOfMine.empty())
-	{
-		bEndTacticPhase();
-		return;
-	}
-
-	auto it = vstd::find(stacksOfMine, current);
-	if (it != stacksOfMine.end() && ++it != stacksOfMine.end())
-		stackActivated(*it);
-	else
-		stackActivated(stacksOfMine.front());
-
-}
-
-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 * activeStack, BattleHex myNumber)
-{
-	std::vector<BattleHex> acc = curInt->cb->battleGetAvailableHexes(activeStack);
-	BattleHex shiftedDest = myNumber.cloneInDirection(activeStack->destShiftDir(), false);
-
-	if (vstd::contains(acc, myNumber))
-		return true;
-	else if (activeStack->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(!activeStack)
-		return;
-
-	bool ourStack = false;
-	if (shere)
-		ourStack = shere->owner == curInt->playerID;
-
-	//stack changed, update selection border
-	if (shere != mouseHoveredStack)
-	{
-		setHoveredStack(shere);
-	}
-
-	localActions.clear();
-	illegalActions.clear();
-
-	reorderPossibleActionsPriority(activeStack, 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(activeStack, myNumber))
-						legalAction = true;
-				}
-				break;
-			}
-			case PossiblePlayerBattleAction::ATTACK:
-			case PossiblePlayerBattleAction::WALK_AND_ATTACK:
-			case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
-			{
-				if(curInt->cb->battleCanAttack(activeStack, shere, myNumber))
-				{
-					if (isTileAttackable(myNumber)) // move isTileAttackable to be part of battleCanAttack?
-					{
-						setBattleCursor(myNumber); // temporary - needed for following function :(
-						BattleHex attackFromHex = 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(activeStack, myNumber))
-					legalAction = true;
-				break;
-			case PossiblePlayerBattleAction::ANY_LOCATION:
-				if (myNumber > -1) //TODO: this should be checked for all actions
-				{
-					if(isCastingPossibleHere(activeStack, shere, myNumber))
-						legalAction = true;
-				}
-				break;
-			case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-				if(shere && isCastingPossibleHere(activeStack, shere, myNumber))
-					legalAction = true;
-				break;
-			case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
-			{
-				if(shere && ourStack && shere != activeStack && 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(activeStack, shere, myNumber))
-					legalAction = true;
-				break;
-			case PossiblePlayerBattleAction::TELEPORT:
-			{
-				//todo: move to mechanics
-				ui8 skill = 0;
-				if (creatureCasting)
-					skill = activeStack->getEffectLevel(SpellID(SpellID::TELEPORT).toSpell());
-				else
-					skill = getActiveHero()->getEffectLevel(SpellID(SpellID::TELEPORT).toSpell());
-				//TODO: explicitely save power, skill
-				if (curInt->cb->battleCanTeleportTo(selectedStack, myNumber, skill))
-					legalAction = true;
-				else
-					notLegal = true;
-			}
-				break;
-			case PossiblePlayerBattleAction::SACRIFICE: //choose our living stack to sacrifice
-				if (shere && shere != selectedStack && ourStack && shere->alive())
-					legalAction = true;
-				else
-					notLegal = true;
-				break;
-			case PossiblePlayerBattleAction::FREE_LOCATION:
-				legalAction = true;
-				if(!isCastingPossibleHere(activeStack, shere, myNumber))
-				{
-					legalAction = false;
-					notLegal = true;
-				}
-				break;
-			case PossiblePlayerBattleAction::CATAPULT:
-				if (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 (activeStack->hasBonusOfType(Bonus::FLYING))
-				{
-					cursorFrame = ECursor::COMBAT_FLY;
-					consoleMsg = (boost::format(CGI->generaltexth->allTexts[295]) % activeStack->getName()).str(); //Fly %s here
-				}
-				else
-				{
-					cursorFrame = ECursor::COMBAT_MOVE;
-					consoleMsg = (boost::format(CGI->generaltexth->allTexts[294]) % activeStack->getName()).str(); //Move %s here
-				}
-
-				realizeAction = [=]()
-				{
-					if(activeStack->doubleWide())
-					{
-						std::vector<BattleHex> acc = curInt->cb->battleGetAvailableHexes(activeStack);
-						BattleHex shiftedDest = myNumber.cloneInDirection(activeStack->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
-				{
-					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 = 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(activeStack, myNumber, attackFromHex, returnAfterAttack));
-							sendCommand(command, activeStack);
-						}
-					};
-
-					TDmgRange damage = curInt->cb->battleEstimateDamage(activeStack, 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(activeStack, myNumber))
-					cursorFrame = ECursor::COMBAT_SHOOT_PENALTY;
-				else
-					cursorFrame = ECursor::COMBAT_SHOOT;
-
-				realizeAction = [=](){giveCommand(EActionType::SHOOT, myNumber);};
-				TDmgRange damage = curInt->cb->battleEstimateDamage(activeStack, 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() % activeStack->shots.available() % estDmgText).str();
-			}
-				break;
-			case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-				sp = CGI->spellh->objects[creatureCasting ? creatureSpellToCast : 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:
-						selectedStack = shere; //remember first target
-						secondaryTarget = true;
-						break;
-				}
-				isCastingPossible = true;
-				break;
-			case PossiblePlayerBattleAction::ANY_LOCATION:
-				sp = CGI->spellh->objects[creatureCasting ? creatureSpellToCast : 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, creatureSpellToCast);
-					}
-					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();
-				}
-				selectedStack = nullptr;
-			}
-		};
-	}
-
-	{
-		if (eventType == MOVE)
-		{
-			if (setCursor)
-				CCS->curh->changeGraphic(cursorType, cursorFrame);
-			this->console->alterText(consoleMsg);
-			this->console->whoSetAlter = 0;
-		}
-		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);
-			this->console->alterText("");
-		}
-	}
-}
-
-bool CBattleInterface::isCastingPossibleHere(const CStack *sactive, const CStack *shere, BattleHex myNumber)
-{
-	creatureCasting = stackCanCastSpell && !spellDestSelectMode; //TODO: allow creatures to cast aimed spells
-
-	bool isCastingPossible = true;
-
-	int spellID = -1;
-	if (creatureCasting)
-	{
-		if (creatureSpellToCast > -1 && (shere != sactive)) //can't cast on itself
-			spellID = creatureSpellToCast; //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;
-}
-
-BattleHex CBattleInterface::fromWhichHexAttack(BattleHex myNumber)
-{
-	//TODO far too much repeating code
-	BattleHex destHex;
-	switch(CCS->curh->frame)
-	{
-	case 12: //from bottom right
-		{
-			bool doubleWide = activeStack->doubleWide();
-			destHex = myNumber + ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH : GameConstants::BFIELD_WIDTH+1 ) +
-				(activeStack->side == BattleSide::ATTACKER && doubleWide ? 1 : 0);
-			if(vstd::contains(occupyableHexes, destHex))
-				return destHex;
-			else if(activeStack->side == BattleSide::ATTACKER)
-			{
-				if (vstd::contains(occupyableHexes, destHex+1))
-					return destHex+1;
-			}
-			else //if we are defender
-			{
-				if(vstd::contains(occupyableHexes, destHex-1))
-					return destHex-1;
-			}
-			break;
-		}
-	case 7: //from bottom left
-		{
-			destHex = myNumber + ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH-1 : GameConstants::BFIELD_WIDTH );
-			if (vstd::contains(occupyableHexes, destHex))
-				return destHex;
-			else if(activeStack->side == BattleSide::ATTACKER)
-			{
-				if(vstd::contains(occupyableHexes, destHex+1))
-					return destHex+1;
-			}
-			else //we are defender
-			{
-				if(vstd::contains(occupyableHexes, destHex-1))
-					return destHex-1;
-			}
-			break;
-		}
-	case 8: //from left
-		{
-			if(activeStack->doubleWide() && activeStack->side == BattleSide::DEFENDER)
-			{
-				std::vector<BattleHex> acc = curInt->cb->battleGetAvailableHexes(activeStack);
-				if (vstd::contains(acc, myNumber))
-					return myNumber - 1;
-				else
-					return myNumber - 2;
-			}
-			else
-			{
-				return myNumber - 1;
-			}
-			break;
-		}
-	case 9: //from top left
-		{
-			destHex = myNumber - ((myNumber/GameConstants::BFIELD_WIDTH) % 2 ? GameConstants::BFIELD_WIDTH + 1 : GameConstants::BFIELD_WIDTH);
-			if(vstd::contains(occupyableHexes, destHex))
-				return destHex;
-			else if(activeStack->side == BattleSide::ATTACKER)
-			{
-				if(vstd::contains(occupyableHexes, destHex+1))
-					return destHex+1;
-			}
-			else //if we are defender
-			{
-				if(vstd::contains(occupyableHexes, destHex-1))
-					return destHex-1;
-			}
-			break;
-		}
-	case 10: //from top right
-		{
-			bool doubleWide = activeStack->doubleWide();
-			destHex = myNumber - ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH : GameConstants::BFIELD_WIDTH-1 ) +
-				(activeStack->side == BattleSide::ATTACKER && doubleWide ? 1 : 0);
-			if(vstd::contains(occupyableHexes, destHex))
-				return destHex;
-			else if(activeStack->side == BattleSide::ATTACKER)
-			{
-				if(vstd::contains(occupyableHexes, destHex+1))
-					return destHex+1;
-			}
-			else //if we are defender
-			{
-				if(vstd::contains(occupyableHexes, destHex-1))
-					return destHex-1;
-			}
-			break;
-		}
-	case 11: //from right
-		{
-			if(activeStack->doubleWide() && activeStack->side == BattleSide::ATTACKER)
-			{
-				std::vector<BattleHex> acc = curInt->cb->battleGetAvailableHexes(activeStack);
-				if(vstd::contains(acc, myNumber))
-					return myNumber + 1;
-				else
-					return myNumber + 2;
-			}
-			else
-			{
-				return myNumber + 1;
-			}
-			break;
-		}
-	case 13: //from bottom
-		{
-			destHex = myNumber + ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH : GameConstants::BFIELD_WIDTH+1 );
-			if(vstd::contains(occupyableHexes, destHex))
-				return destHex;
-			else if(activeStack->side == BattleSide::ATTACKER)
-			{
-				if(vstd::contains(occupyableHexes, destHex+1))
-					return destHex+1;
-			}
-			else //if we are defender
-			{
-				if(vstd::contains(occupyableHexes, destHex-1))
-					return destHex-1;
-			}
-			break;
-		}
-	case 14: //from top
-		{
-			destHex = myNumber - ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH : GameConstants::BFIELD_WIDTH-1 );
-			if (vstd::contains(occupyableHexes, destHex))
-				return destHex;
-			else if(activeStack->side == BattleSide::ATTACKER)
-			{
-				if(vstd::contains(occupyableHexes, destHex+1))
-					return destHex+1;
-			}
-			else //if we are defender
-			{
-				if(vstd::contains(occupyableHexes, destHex-1))
-					return destHex-1;
-			}
-			break;
-		}
-	}
-	return -1;
-}
-
-Rect CBattleInterface::hexPosition(BattleHex hex) const
-{
-	int x = 14 + ((hex.getY())%2==0 ? 22 : 0) + 44*hex.getX() + pos.x;
-	int y = 86 + 42 *hex.getY() + pos.y;
-	int w = cellShade->w;
-	int h = cellShade->h;
-	return Rect(x, y, w, h);
-}
-
-void CBattleInterface::obstaclePlaced(const CObstacleInstance & oi)
-{
-	//so when multiple obstacles are added, they show up one after another
-	waitForAnims();
-
-	//soundBase::soundID sound; // FIXME(v.markovtsev): soundh->playSound() is commented in the end => warning
-
-	std::string defname;
-
-	switch(oi.obstacleType)
-	{
-	case CObstacleInstance::SPELL_CREATED:
-		{
-			auto &spellObstacle = dynamic_cast<const SpellCreatedObstacle&>(oi);
-			defname = spellObstacle.appearAnimation;
-			//TODO: sound
-			//soundBase::QUIKSAND
-			//soundBase::LANDMINE
-			//soundBase::FORCEFLD
-			//soundBase::fireWall
-		}
-		break;
-	default:
-		logGlobal->error("I don't know how to animate appearing obstacle of type %d", (int)oi.obstacleType);
-		return;
-	}
-
-	auto animation = std::make_shared<CAnimation>(defname);
-	animation->preload();
-
-	auto first = animation->getImage(0, 0);
-	if(!first)
-		return;
-
-	//we assume here that effect graphics have the same size as the usual obstacle image
-	// -> if we know how to blit obstacle, let's blit the effect in the same place
-	Point whereTo = getObstaclePosition(first, oi);
-	addNewAnim(new CEffectAnimation(this, animation, whereTo.x, whereTo.y));
-
-	//TODO we need to wait after playing sound till it's finished, otherwise it overlaps and sounds really bad
-	//CCS->soundh->playSound(sound);
-}
-
-void CBattleInterface::gateStateChanged(const EGateState state)
-{
-	auto oldState = curInt->cb->battleGetGateState();
-	bool playSound = false;
-	int stateId = EWallState::NONE;
-	switch(state)
-	{
-	case EGateState::CLOSED:
-		if (oldState != EGateState::BLOCKED)
-			playSound = true;
-		break;
-	case EGateState::BLOCKED:
-		if (oldState != EGateState::CLOSED)
-			playSound = true;
-		break;
-	case EGateState::OPENED:
-		playSound = true;
-		stateId = EWallState::DAMAGED;
-		break;
-	case EGateState::DESTROYED:
-		stateId = EWallState::DESTROYED;
-		break;
-	}
-
-	if (oldState != EGateState::NONE && oldState != EGateState::CLOSED && oldState != EGateState::BLOCKED)
-		SDL_FreeSurface(siegeH->walls[SiegeHelper::GATE]);
-
-	if (stateId != EWallState::NONE)
-		siegeH->walls[SiegeHelper::GATE] = BitmapHandler::loadBitmap(siegeH->getSiegeName(SiegeHelper::GATE, stateId));
-	if (playSound)
-		CCS->soundh->playSound(soundBase::DRAWBRG);
-}
-
-const CGHeroInstance *CBattleInterface::currentHero() const
-{
-	if (attackingHeroInstance->tempOwner == curInt->playerID)
-		return attackingHeroInstance;
-	else
-		return defendingHeroInstance;
-}
-
-InfoAboutHero CBattleInterface::enemyHero() const
-{
-	InfoAboutHero ret;
-	if (attackingHeroInstance->tempOwner == curInt->playerID)
-		curInt->cb->getHeroInfo(defendingHeroInstance, ret);
-	else
-		curInt->cb->getHeroInfo(attackingHeroInstance, ret);
-
-	return ret;
-}
-
-void CBattleInterface::requestAutofightingAIToTakeAction()
-{
-	assert(curInt->isAutoFightOn);
-
-	boost::thread aiThread([&]()
-	{
-		auto ba = make_unique<BattleAction>(curInt->autofightingAI->activeStack(activeStack));
-
-		if(curInt->cb->battleIsFinished())
-		{
-			return; // battle finished with spellcast
-		}
-
-		if (curInt->isAutoFightOn)
-		{
-			if (tacticsMode)
-			{
-				// Always end tactics mode. Player interface is blocked currently, so it's not possible that
-				// the AI can take any action except end tactics phase (AI actions won't be triggered)
-				//TODO implement the possibility that the AI will be triggered for further actions
-				//TODO any solution to merge tactics phase & normal phase in the way it is handled by the player and battle interface?
-				setActiveStack(nullptr);
-				blockUI(true);
-				tacticsMode = false;
-			}
-			else
-			{
-				givenCommand.setn(ba.release());
-			}
-		}
-		else
-		{
-			boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
-			activateStack();
-		}
-	});
-
-	aiThread.detach();
-}
-
-CBattleInterface::SiegeHelper::SiegeHelper(const CGTownInstance *siegeTown, const CBattleInterface *_owner)
-	: owner(_owner), town(siegeTown)
-{
-	for (int g = 0; g < ARRAY_COUNT(walls); ++g)
-	{
-		if (g != SiegeHelper::GATE)
-			walls[g] = BitmapHandler::loadBitmap(getSiegeName(g));
-	}
-}
-
-CBattleInterface::SiegeHelper::~SiegeHelper()
-{
-	auto gateState = owner->curInt->cb->battleGetGateState();
-	for (int g = 0; g < ARRAY_COUNT(walls); ++g)
-	{
-		if (g != SiegeHelper::GATE || (gateState != EGateState::NONE && gateState != EGateState::CLOSED && gateState != EGateState::BLOCKED))
-			SDL_FreeSurface(walls[g]);
-	}
-}
-
-std::string CBattleInterface::SiegeHelper::getSiegeName(ui16 what) const
-{
-	return getSiegeName(what, EWallState::INTACT);
-}
-
-std::string CBattleInterface::SiegeHelper::getSiegeName(ui16 what, int state) const
-{
-	auto getImageIndex = [&]() -> int
-	{
-		switch (state)
-		{
-		case EWallState::INTACT : return 1;
-		case EWallState::DAMAGED :
-			if(what == 2 || what == 3 || what == 8) // towers don't have separate image here - INTACT and DAMAGED is 1, DESTROYED is 2
-				return 1;
-			else
-				return 2;
-		case EWallState::DESTROYED :
-			if (what == 2 || what == 3 || what == 8)
-				return 2;
-			else
-				return 3;
-		}
-		return 1;
-	};
-
-	const std::string & prefix = town->town->clientInfo.siegePrefix;
-	std::string addit = boost::lexical_cast<std::string>(getImageIndex());
-
-	switch(what)
-	{
-	case SiegeHelper::BACKGROUND:
-		return prefix + "BACK.BMP";
-	case SiegeHelper::BACKGROUND_WALL:
-		{
-			switch(town->town->faction->index)
-			{
-			case ETownType::RAMPART:
-			case ETownType::NECROPOLIS:
-			case ETownType::DUNGEON:
-			case ETownType::STRONGHOLD:
-				return prefix + "TPW1.BMP";
-			default:
-				return prefix + "TPWL.BMP";
-			}
-		}
-	case SiegeHelper::KEEP:
-		return prefix + "MAN" + addit + ".BMP";
-	case SiegeHelper::BOTTOM_TOWER:
-		return prefix + "TW1" + addit + ".BMP";
-	case SiegeHelper::BOTTOM_WALL:
-		return prefix + "WA1" + addit + ".BMP";
-	case SiegeHelper::WALL_BELLOW_GATE:
-		return prefix + "WA3" + addit + ".BMP";
-	case SiegeHelper::WALL_OVER_GATE:
-		return prefix + "WA4" + addit + ".BMP";
-	case SiegeHelper::UPPER_WALL:
-		return prefix + "WA6" + addit + ".BMP";
-	case SiegeHelper::UPPER_TOWER:
-		return prefix + "TW2" + addit + ".BMP";
-	case SiegeHelper::GATE:
-		return prefix + "DRW" + addit + ".BMP";
-	case SiegeHelper::GATE_ARCH:
-		return prefix + "ARCH.BMP";
-	case SiegeHelper::BOTTOM_STATIC_WALL:
-		return prefix + "WA2.BMP";
-	case SiegeHelper::UPPER_STATIC_WALL:
-		return prefix + "WA5.BMP";
-	case SiegeHelper::MOAT:
-		return prefix + "MOAT.BMP";
-	case SiegeHelper::BACKGROUND_MOAT:
-		return prefix + "MLIP.BMP";
-	case SiegeHelper::KEEP_BATTLEMENT:
-		return prefix + "MANC.BMP";
-	case SiegeHelper::BOTTOM_BATTLEMENT:
-		return prefix + "TW1C.BMP";
-	case SiegeHelper::UPPER_BATTLEMENT:
-		return prefix + "TW2C.BMP";
-	default:
-		return "";
-	}
-}
-
-void CBattleInterface::SiegeHelper::printPartOfWall(SDL_Surface *to, int what)
-{
-	Point pos = Point(-1, -1);
-	auto & ci = town->town->clientInfo;
-
-	if (vstd::iswithin(what, 1, 17))
-	{
-		pos.x = ci.siegePositions[what].x + owner->pos.x;
-		pos.y = ci.siegePositions[what].y + owner->pos.y;
-	}
-
-	if (town->town->faction->index == ETownType::TOWER
-		&& (what == SiegeHelper::MOAT || what == SiegeHelper::BACKGROUND_MOAT))
-		return; // no moat in Tower. TODO: remove hardcode somehow?
-
-	if (pos.x != -1)
-	{
-		//gate have no displayed bitmap when drawbridge is raised
-		if (what == SiegeHelper::GATE)
-		{
-			auto gateState = owner->curInt->cb->battleGetGateState();
-			if (gateState != EGateState::OPENED && gateState != EGateState::DESTROYED)
-				return;
-		}
-
-		blitAt(walls[what], pos.x, pos.y, to);
-	}
-}
-
-CatapultProjectileInfo::CatapultProjectileInfo(Point from, Point dest)
-{
-	facA = 0.005; // seems to be constant
-
-	// system of 2 linear equations, solutions of which are missing coefficients
-	// for quadratic equation a*x*x + b*x + c
-	double eq[2][3] = {
-		{ static_cast<double>(from.x), 1.0, from.y - facA*from.x*from.x },
-		{ static_cast<double>(dest.x), 1.0, dest.y - facA*dest.x*dest.x }
-	};
-
-	// solve system via determinants
-	double det  = eq[0][0] *eq[1][1] - eq[1][0] *eq[0][1];
-	double detB = eq[0][2] *eq[1][1] - eq[1][2] *eq[0][1];
-	double detC = eq[0][0] *eq[1][2] - eq[1][0] *eq[0][2];
-
-	facB = detB / det;
-	facC = detC / det;
-
-	// make sure that parabola is correct e.g. passes through from and dest
-	assert(fabs(calculateY(from.x) - from.y) < 1.0);
-	assert(fabs(calculateY(dest.x) - dest.y) < 1.0);
-}
-
-double CatapultProjectileInfo::calculateY(double x)
-{
-	return facA *pow(x, 2.0) + facB *x + facC;
-}
-
-void CBattleInterface::showAll(SDL_Surface *to)
-{
-	show(to);
-}
-
-void CBattleInterface::show(SDL_Surface *to)
-{
-	assert(to);
-
-	SDL_Rect buf;
-	SDL_GetClipRect(to, &buf);
-	SDL_SetClipRect(to, &pos);
-
-	++animCount;
-
-	showBackground(to);
-	showBattlefieldObjects(to);
-	showProjectiles(to);
-
-	if(battleActionsStarted)
-		updateBattleAnimations();
-
-	SDL_SetClipRect(to, &buf); //restoring previous clip_rect
-
-	showInterface(to);
-
-	//activation of next stack
-	if (pendingAnims.empty() && stackToActivate != nullptr && battleActionsStarted) //FIXME: watch for recursive infinite loop here when working with this file, this needs rework anyway...
-	{
-		activateStack();
-
-		//we may have changed active interface (another side in hot-seat),
-		// so we can't continue drawing with old setting.
-		show(to);
-	}
-}
-
-void CBattleInterface::showBackground(SDL_Surface *to)
-{
-	if (activeStack != nullptr && creAnims[activeStack->ID]->isIdle()) //show everything with range
-	{
-		// FIXME: any *real* reason to keep this separate? Speed difference can't be that big
-		blitAt(backgroundWithHexes, pos.x, pos.y, to);
-	}
-	else
-	{
-		showBackgroundImage(to);
-		showAbsoluteObstacles(to);
-	}
-	showHighlightedHexes(to);
-}
-
-void CBattleInterface::showBackgroundImage(SDL_Surface *to)
-{
-	blitAt(background, pos.x, pos.y, to);
-	if (settings["battle"]["cellBorders"].Bool())
-	{
-		CSDL_Ext::blit8bppAlphaTo24bpp(cellBorders, nullptr, to, &pos);
-	}
-}
-
-void CBattleInterface::showAbsoluteObstacles(SDL_Surface * to)
-{
-	//Blit absolute obstacles
-	for(auto & oi : curInt->cb->battleGetAllObstacles())
-	{
-		if(oi->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
-		{
-			auto img = getObstacleImage(*oi);
-			if(img)
-				img->draw(to, pos.x + oi->getInfo().width, pos.y + oi->getInfo().height);
-		}
-	}
-
-
-	if (siegeH && siegeH->town->hasBuilt(BuildingID::CITADEL))
-		siegeH->printPartOfWall(to, SiegeHelper::BACKGROUND_MOAT);
-}
-
-void CBattleInterface::showHighlightedHexes(SDL_Surface *to)
-{
-	bool delayedBlit = false; //workaround for blitting enemy stack hex without mouse shadow with stack range on
-	if(activeStack && settings["battle"]["stackRange"].Bool())
-	{
-		std::set<BattleHex> set = curInt->cb->battleGetAttackedHexes(activeStack, currentlyHoveredHex, attackingHex);
-		for(BattleHex hex : set)
-			if(hex != currentlyHoveredHex)
-				showHighlightedHex(to, hex);
-
-		// display the movement shadow of the stack at b (i.e. stack under mouse)
-		const CStack * const shere = curInt->cb->battleGetStackByPos(currentlyHoveredHex, false);
-		if(shere && shere != activeStack && shere->alive())
-		{
-			std::vector<BattleHex> v = curInt->cb->battleGetAvailableHexes(shere, true, nullptr);
-			for(BattleHex hex : v)
-			{
-				if(hex != currentlyHoveredHex)
-					showHighlightedHex(to, hex);
-				else if(!settings["battle"]["mouseShadow"].Bool())
-					delayedBlit = true; //blit at the end of method to avoid graphic artifacts
-				else
-					showHighlightedHex(to, hex, true); //blit now and blit 2nd time later for darker shadow - avoids graphic artifacts
-			}
-		}
-	}
-
-	for(int b=0; b<GameConstants::BFIELD_SIZE; ++b)
-	{
-		if(bfield[b]->strictHovered && bfield[b]->hovered)
-		{
-			if(previouslyHoveredHex == -1)
-				previouslyHoveredHex = b; //something to start with
-			if(currentlyHoveredHex == -1)
-				currentlyHoveredHex = b; //something to start with
-
-			if(currentlyHoveredHex != b) //repair hover info
-			{
-				previouslyHoveredHex = currentlyHoveredHex;
-				currentlyHoveredHex = b;
-			}
-			if(settings["battle"]["mouseShadow"].Bool() || delayedBlit)
-			{
-				const spells::Caster *caster = nullptr;
-				const CSpell *spell = nullptr;
-
-				spells::Mode mode = spells::Mode::HERO;
-
-				if(spellToCast)//hero casts spell
-				{
-					spell = SpellID(spellToCast->actionSubtype).toSpell();
-					caster = getActiveHero();
-				}
-				else if(creatureSpellToCast >= 0 && stackCanCastSpell && creatureCasting)//stack casts spell
-				{
-					spell = SpellID(creatureSpellToCast).toSpell();
-					caster = activeStack;
-					mode = spells::Mode::CREATURE_ACTIVE;
-				}
-
-				if(caster && spell) //when casting spell
-				{
-					// printing shaded hex(es)
-					spells::BattleCast event(curInt->cb.get(), caster, mode, spell);
-					auto shaded = spell->battleMechanics(&event)->rangeInHexes(currentlyHoveredHex);
-
-					for(BattleHex shadedHex : shaded)
-					{
-						if((shadedHex.getX() != 0) && (shadedHex.getX() != GameConstants::BFIELD_WIDTH - 1))
-							showHighlightedHex(to, shadedHex, true);
-					}
-				}
-				else if(active || delayedBlit) //always highlight pointed hex, keep this condition last in this method for correct behavior
-				{
-					if(currentlyHoveredHex.getX() != 0
-					 && currentlyHoveredHex.getX() != GameConstants::BFIELD_WIDTH - 1)
-						showHighlightedHex(to, currentlyHoveredHex, true); //keep true for OH3 behavior: hovered hex frame "thinner"
-				}
-			}
-		}
-	}
-}
-
-void CBattleInterface::showHighlightedHex(SDL_Surface *to, BattleHex hex, bool darkBorder)
-{
-	int x = 14 + (hex.getY() % 2 == 0 ? 22 : 0) + 44 *(hex.getX()) + pos.x;
-	int y = 86 + 42 *hex.getY() + pos.y;
-	SDL_Rect temp_rect = genRect (cellShade->h, cellShade->w, x, y);
-	CSDL_Ext::blit8bppAlphaTo24bpp (cellShade, nullptr, to, &temp_rect);
-	if(!darkBorder && settings["battle"]["cellBorders"].Bool())
-		CSDL_Ext::blit8bppAlphaTo24bpp(cellBorder, nullptr, to, &temp_rect); //redraw border to make it light green instead of shaded
-}
-
-void CBattleInterface::showProjectiles(SDL_Surface *to)
-{
-	assert(to);
-
-	std::list< std::list<ProjectileInfo>::iterator > toBeDeleted;
-	for (auto it = projectiles.begin(); it!=projectiles.end(); ++it)
-	{
-		// Check if projectile is already visible (shooter animation did the shot)
-		if (!it->shotDone)
-		{
-			// frame we're waiting for is reached OR animation has already finished
-			if (creAnims[it->stackID]->getCurrentFrame() >= it->animStartDelay ||
-				creAnims[it->stackID]->isShooting() == false)
-			{
-				//at this point projectile should become visible
-				creAnims[it->stackID]->pause(); // pause animation
-				it->shotDone = true;
-			}
-			else
-				continue; // wait...
-		}
-
-		if (idToProjectile.count(it->creID))
-		{
-			size_t group = it->reverse ? 1 : 0;
-			auto image = idToProjectile[it->creID]->getImage(it->frameNum, group, true);
-
-			if(image)
-			{
-				SDL_Rect dst;
-				dst.h = image->height();
-				dst.w = image->width();
-				dst.x = static_cast<int>(it->x - dst.w / 2);
-				dst.y = static_cast<int>(it->y - dst.h / 2);
-
-				image->draw(to, &dst, nullptr);
-			}
-		}
-		if (idToRay.count(it->creID))
-		{
-			auto const & ray = idToRay[it->creID];
-
-			if (std::abs(it->dx) > std::abs(it->dy)) // draw in horizontal axis
-			{
-				int y1 =  it->y0 - ray.size() / 2;
-				int y2 =  it->y - ray.size() / 2;
-
-				int x1 = it->x0;
-				int x2 = it->x;
-
-				for (size_t i = 0; i < ray.size(); ++i)
-				{
-					SDL_Color beginColor{ ray[i].r1, ray[i].g1, ray[i].b1, ray[i].a1};
-					SDL_Color endColor  { ray[i].r2, ray[i].g2, ray[i].b2, ray[i].a2};
-
-					CSDL_Ext::drawLine(to, x1, y1 + i, x2, y2 + i, beginColor, endColor);
-				}
-			}
-			else // draw in vertical axis
-			{
-				int x1 = it->x0 - ray.size() / 2;
-				int x2 = it->x - ray.size() / 2;
-
-				int y1 =  it->y0;
-				int y2 =  it->y;
-
-				for (size_t i = 0; i < ray.size(); ++i)
-				{
-					SDL_Color beginColor{ ray[i].r1, ray[i].g1, ray[i].b1, ray[i].a1};
-					SDL_Color endColor  { ray[i].r2, ray[i].g2, ray[i].b2, ray[i].a2};
-
-					CSDL_Ext::drawLine(to, x1 + i, y1, x2 + i, y2, beginColor, endColor);
-				}
-			}
-		}
-
-		// Update projectile
-		++it->step;
-		if (it->step > it->lastStep)
-		{
-			toBeDeleted.insert(toBeDeleted.end(), it);
-		}
-		else
-		{
-			if (it->catapultInfo)
-			{
-				// Parabolic shot of the trajectory, as follows: f(x) = ax^2 + bx + c
-				it->x += it->dx;
-				it->y = it->catapultInfo->calculateY(it->x);
-
-				++(it->frameNum);
-				it->frameNum %= idToProjectile[it->creID]->size(0);
-			}
-			else
-			{
-				// Normal projectile, just add the calculated "deltas" to the x and y positions.
-				it->x += it->dx;
-				it->y += it->dy;
-			}
-		}
-	}
-
-	for (auto & elem : toBeDeleted)
-	{
-		// resume animation
-		creAnims[elem->stackID]->play();
-		projectiles.erase(elem);
-	}
-}
-
-void CBattleInterface::showBattlefieldObjects(SDL_Surface *to)
-{
-	auto showHexEntry = [&](BattleObjectsByHex::HexData & hex)
-	{
-		showPiecesOfWall(to, hex.walls);
-		showObstacles(to, hex.obstacles);
-		showAliveStacks(to, hex.alive);
-		showBattleEffects(to, hex.effects);
-	};
-
-	BattleObjectsByHex objects = sortObjectsByHex();
-
-	// dead stacks should be blit first
-	showStacks(to, objects.beforeAll.dead);
-	for (auto & data : objects.hex)
-		showStacks(to, data.dead);
-	showStacks(to, objects.afterAll.dead);
-
-	// display objects that must be blit before anything else (e.g. topmost walls)
-	showHexEntry(objects.beforeAll);
-
-	// show heroes after "beforeAll" - e.g. topmost wall in siege
-	if (attackingHero)
-		attackingHero->show(to);
-	if (defendingHero)
-		defendingHero->show(to);
-
-	// actual blit of most of objects, hex by hex
-	// NOTE: row-by-row blitting may be a better approach
-	for (auto &data : objects.hex)
-		showHexEntry(data);
-
-	// objects that must be blit *after* everything else - e.g. bottom tower or some spell effects
-	showHexEntry(objects.afterAll);
-}
-
-void CBattleInterface::showAliveStacks(SDL_Surface *to, std::vector<const CStack *> stacks)
-{
-	BattleHex currentActionTarget;
-	if(curInt->curAction)
-	{
-		auto target = curInt->curAction->getTarget(curInt->cb.get());
-		if(!target.empty())
-			currentActionTarget = target.at(0).hexValue;
-	}
-
-	auto isAmountBoxVisible = [&](const CStack *stack) -> bool
-	{
-		if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON) && stack->getCount() == 1) //do not show box for singular war machines, stacked war machines with box shown are supported as extension feature
-			return false;
-
-		if(stack->getCount() == 0) //hide box when target is going to die anyway - do not display "0 creatures"
-			return false;
-
-		for(auto anim : pendingAnims) //no matter what other conditions below are, hide box when creature is playing hit animation
-		{
-			auto hitAnimation = dynamic_cast<CDefenceAnimation*>(anim.first);
-			if(hitAnimation && (hitAnimation->stack->ID == stack->ID)) //we process only "current creature" as other creatures will be processed reliably on their own iteration
-				return false;
-		}
-
-		if(curInt->curAction)
-		{
-			if(curInt->curAction->stackNumber == stack->ID) //stack is currently taking action (is not a target of another creature's action etc)
-			{
-				if(curInt->curAction->actionType == EActionType::WALK || curInt->curAction->actionType == EActionType::SHOOT) //hide when stack walks or shoots
-					return false;
-
-				else if(curInt->curAction->actionType == EActionType::WALK_AND_ATTACK && currentActionTarget != stack->getPosition()) //when attacking, hide until walk phase finished
-					return false;
-			}
-
-			if(curInt->curAction->actionType == EActionType::SHOOT && currentActionTarget == stack->getPosition()) //hide if we are ranged attack target
-				return false;
-		}
-
-		return true;
-	};
-
-	auto getEffectsPositivness = [&](const std::vector<si32> & activeSpells) -> int
-	{
-		int pos = 0;
-		for (const auto & spellId : activeSpells)
-		{
-			pos += CGI->spellh->objects.at(spellId)->positiveness;
-		}
-		return pos;
-	};
-
-	auto getAmountBoxBackground = [&](int positivness) -> SDL_Surface *
-	{
-		if (positivness > 0)
-			return amountPositive;
-		if (positivness < 0)
-			return amountNegative;
-		return amountEffNeutral;
-	};
-
-	showStacks(to, stacks); // Actual display of all stacks
-
-	for (auto & stack : stacks)
-	{
-		assert(stack);
-		//printing amount
-		if (isAmountBoxVisible(stack))
-		{
-			const int sideShift = stack->side == BattleSide::ATTACKER ? 1 : -1;
-			const int reverseSideShift = stack->side == BattleSide::ATTACKER ? -1 : 1;
-			const BattleHex nextPos = stack->getPosition() + sideShift;
-			const bool edge = stack->getPosition() % GameConstants::BFIELD_WIDTH == (stack->side == BattleSide::ATTACKER ? GameConstants::BFIELD_WIDTH - 2 : 1);
-			const bool moveInside = !edge && !stackCountOutsideHexes[nextPos];
-			int xAdd = (stack->side == BattleSide::ATTACKER ? 220 : 202) +
-					   (stack->doubleWide() ? 44 : 0) * sideShift +
-					   (moveInside ? amountNormal->w + 10 : 0) * reverseSideShift;
-			int yAdd = 260 + ((stack->side == BattleSide::ATTACKER || moveInside) ? 0 : -15);
-
-			//blitting amount background box
-			SDL_Surface *amountBG = amountNormal;
-			std::vector<si32> activeSpells = stack->activeSpells();
-			if (!activeSpells.empty())
-				amountBG = getAmountBoxBackground(getEffectsPositivness(activeSpells));
-
-			SDL_Rect temp_rect = genRect(amountBG->h, amountBG->w, creAnims[stack->ID]->pos.x + xAdd, creAnims[stack->ID]->pos.y + yAdd);
-			SDL_BlitSurface(amountBG, nullptr, to, &temp_rect);
-
-			//blitting amount
-			Point textPos(creAnims[stack->ID]->pos.x + xAdd + amountNormal->w/2,
-						  creAnims[stack->ID]->pos.y + yAdd + amountNormal->h/2);
-			graphics->fonts[FONT_TINY]->renderTextCenter(to, makeNumberShort(stack->getCount()), Colors::WHITE, textPos);
-		}
-	}
-}
-
-void CBattleInterface::showStacks(SDL_Surface *to, std::vector<const CStack *> stacks)
-{
-	for (const CStack *stack : stacks)
-	{
-		creAnims[stack->ID]->nextFrame(to, creDir[stack->ID]); // do actual blit
-		creAnims[stack->ID]->incrementFrame(float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000);
-	}
-}
-
-void CBattleInterface::showObstacles(SDL_Surface * to, std::vector<std::shared_ptr<const CObstacleInstance>> & obstacles)
-{
-	for(auto & obstacle : obstacles)
-	{
-		auto img = getObstacleImage(*obstacle);
-		if(img)
-		{
-			Point p = getObstaclePosition(img, *obstacle);
-			img->draw(to, p.x, p.y);
-		}
-	}
-}
-
-void CBattleInterface::showBattleEffects(SDL_Surface *to, const std::vector<const BattleEffect *> &battleEffects)
-{
-	for (auto & elem : battleEffects)
-	{
-		int currentFrame = static_cast<int>(floor(elem->currentFrame));
-		currentFrame %= elem->animation->size();
-
-		auto img = elem->animation->getImage(currentFrame);
-
-		SDL_Rect temp_rect = genRect(img->height(), img->width(), elem->x, elem->y);
-
-		img->draw(to, &temp_rect, nullptr);
-	}
-}
-
-void CBattleInterface::showInterface(SDL_Surface *to)
-{
-	blitAt(menu, pos.x, 556 + pos.y, to);
-
-	if (tacticsMode)
-	{
-		btactNext->showAll(to);
-		btactEnd->showAll(to);
-	}
-	else
-	{
-		console->showAll(to);
-		bConsoleUp->showAll(to);
-		bConsoleDown->showAll(to);
-	}
-
-	//showing buttons
-	bOptions->showAll(to);
-	bSurrender->showAll(to);
-	bFlee->showAll(to);
-	bAutofight->showAll(to);
-	bSpell->showAll(to);
-	bWait->showAll(to);
-	bDefence->showAll(to);
-
-	//showing in-game console
-	LOCPLINT->cingconsole->show(to);
-
-	Rect posWithQueue = Rect(pos.x, pos.y, 800, 600);
-
-	if (settings["battle"]["showQueue"].Bool())
-	{
-		if (!queue->embedded)
-		{
-			posWithQueue.y -= queue->pos.h;
-			posWithQueue.h += queue->pos.h;
-		}
-
-		queue->showAll(to);
-	}
-
-	//printing border around interface
-	if (screen->w != 800 || screen->h !=600)
-	{
-		CMessage::drawBorder(curInt->playerID,to,posWithQueue.w + 28, posWithQueue.h + 28, posWithQueue.x-14, posWithQueue.y-15);
-	}
-}
-
-BattleObjectsByHex CBattleInterface::sortObjectsByHex()
-{
-	auto getCurrentPosition = [&](const CStack *stack) -> BattleHex
-	{
-		for (auto & anim : pendingAnims)
-		{
-			// certainly ugly workaround but fixes quite annoying bug
-			// stack position will be updated only *after* movement is finished
-			// before this - stack is always at its initial position. Thus we need to find
-			// its current position. Which can be found only in this class
-			if (CMovementAnimation *move = dynamic_cast<CMovementAnimation*>(anim.first))
-			{
-				if (move->stack == stack)
-					return move->nextHex;
-			}
-		}
-		return stack->getPosition();
-	};
-
-	BattleObjectsByHex sorted;
-
-	auto stacks = curInt->cb->battleGetStacksIf([](const CStack *s)
-	{
-		return !s->isTurret();
-	});
-
-	// Sort creatures
-	for (auto & stack : stacks)
-	{
-		if (creAnims.find(stack->ID) == creAnims.end()) //e.g. for summoned but not yet handled stacks
-			continue;
-
-		if (stack->initialPosition < 0) // turret shooters are handled separately
-			continue;
-
-		//FIXME: hack to ignore ghost stacks
-		if ((creAnims[stack->ID]->getType() == CCreatureAnim::DEAD || creAnims[stack->ID]->getType() == CCreatureAnim::HOLDING) && stack->isGhost())
-			;//ignore
-		else if (!creAnims[stack->ID]->isDead())
-		{
-			if (!creAnims[stack->ID]->isMoving())
-				sorted.hex[stack->getPosition()].alive.push_back(stack);
-			else
-			{
-				// flying creature - just blit them over everyone else
-				if (stack->hasBonusOfType(Bonus::FLYING))
-					sorted.afterAll.alive.push_back(stack);
-				else//try to find current location
-					sorted.hex[getCurrentPosition(stack)].alive.push_back(stack);
-			}
-		}
-		else
-			sorted.hex[stack->getPosition()].dead.push_back(stack);
-	}
-
-	// Sort battle effects (spells)
-	for (auto & battleEffect : battleEffects)
-	{
-		if (battleEffect.position.isValid())
-			sorted.hex[battleEffect.position].effects.push_back(&battleEffect);
-		else
-			sorted.afterAll.effects.push_back(&battleEffect);
-	}
-
-	// Sort obstacles
-	{
-		std::map<BattleHex, std::shared_ptr<const CObstacleInstance>> backgroundObstacles;
-		for (auto &obstacle : curInt->cb->battleGetAllObstacles()) {
-			if (obstacle->obstacleType != CObstacleInstance::ABSOLUTE_OBSTACLE
-				&& obstacle->obstacleType != CObstacleInstance::MOAT) {
-				backgroundObstacles[obstacle->pos] = obstacle;
-			}
-		}
-		for (auto &op : backgroundObstacles)
-		{
-			sorted.beforeAll.obstacles.push_back(op.second);
-		}
-	}
-	// Sort wall parts
-	if (siegeH)
-	{
-		sorted.beforeAll.walls.push_back(SiegeHelper::BACKGROUND_WALL);
-		sorted.hex[135].walls.push_back(SiegeHelper::KEEP);
-		sorted.afterAll.walls.push_back(SiegeHelper::BOTTOM_TOWER);
-		sorted.hex[182].walls.push_back(SiegeHelper::BOTTOM_WALL);
-		sorted.hex[130].walls.push_back(SiegeHelper::WALL_BELLOW_GATE);
-		sorted.hex[78].walls.push_back(SiegeHelper::WALL_OVER_GATE);
-		sorted.hex[12].walls.push_back(SiegeHelper::UPPER_WALL);
-		sorted.beforeAll.walls.push_back(SiegeHelper::UPPER_TOWER);
-		sorted.hex[94].walls.push_back(SiegeHelper::GATE);
-		sorted.hex[112].walls.push_back(SiegeHelper::GATE_ARCH);
-		sorted.hex[165].walls.push_back(SiegeHelper::BOTTOM_STATIC_WALL);
-		sorted.hex[45].walls.push_back(SiegeHelper::UPPER_STATIC_WALL);
-
-		if (siegeH && siegeH->town->hasBuilt(BuildingID::CITADEL))
-		{
-			sorted.beforeAll.walls.push_back(SiegeHelper::MOAT);
-			//sorted.beforeAll.walls.push_back(SiegeHelper::BACKGROUND_MOAT); // blit as absolute obstacle
-			sorted.hex[135].walls.push_back(SiegeHelper::KEEP_BATTLEMENT);
-		}
-		if (siegeH && siegeH->town->hasBuilt(BuildingID::CASTLE))
-		{
-			sorted.afterAll.walls.push_back(SiegeHelper::BOTTOM_BATTLEMENT);
-			sorted.beforeAll.walls.push_back(SiegeHelper::UPPER_BATTLEMENT);
-		}
-	}
-	return sorted;
-}
-
-void CBattleInterface::updateBattleAnimations()
-{
-	//handle animations
-	for (auto & elem : pendingAnims)
-	{
-		if (!elem.first) //this animation should be deleted
-			continue;
-
-		if (!elem.second)
-		{
-			elem.second = elem.first->init();
-		}
-		if (elem.second && elem.first)
-			elem.first->nextFrame();
-	}
-
-	//delete anims
-	int preSize = static_cast<int>(pendingAnims.size());
-	for (auto it = pendingAnims.begin(); it != pendingAnims.end(); ++it)
-	{
-		if (it->first == nullptr)
-		{
-			pendingAnims.erase(it);
-			it = pendingAnims.begin();
-			break;
-		}
-	}
-
-	if (preSize > 0 && pendingAnims.empty())
-	{
-		//anims ended
-		blockUI(activeStack == nullptr);
-
-		animsAreDisplayed.setn(false);
-	}
-}
-
-std::shared_ptr<IImage> CBattleInterface::getObstacleImage(const CObstacleInstance & oi)
-{
-	int frameIndex = (animCount+1) *25 / getAnimSpeed();
-	std::shared_ptr<CAnimation> animation;
-
-	if(oi.obstacleType == CObstacleInstance::USUAL || oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
-	{
-		animation = obstacleAnimations[oi.uniqueID];
-	}
-	else if(oi.obstacleType == CObstacleInstance::SPELL_CREATED)
-	{
-		const SpellCreatedObstacle * spellObstacle = dynamic_cast<const SpellCreatedObstacle *>(&oi);
-		if(!spellObstacle)
-			return std::shared_ptr<IImage>();
-
-		std::string animationName = spellObstacle->animation;
-
-		auto cacheIter = animationsCache.find(animationName);
-
-		if(cacheIter == animationsCache.end())
-		{
-			logAnim->trace("Creating obstacle animation %s", animationName);
-
-			animation = std::make_shared<CAnimation>(animationName);
-			animation->preload();
-			animationsCache[animationName] = animation;
-		}
-		else
-		{
-			animation = cacheIter->second;
-		}
-	}
-
-	if(animation)
-	{
-		frameIndex %= animation->size(0);
-		return animation->getImage(frameIndex, 0);
-	}
-
-	return nullptr;
-}
-
-Point CBattleInterface::getObstaclePosition(std::shared_ptr<IImage> image, const CObstacleInstance & obstacle)
-{
-	int offset = obstacle.getAnimationYOffset(image->height());
-
-	Rect r = hexPosition(obstacle.pos);
-	r.y += 42 - image->height() + offset;
-
-	return r.topLeft();
-}
-
-void CBattleInterface::redrawBackgroundWithHexes(const CStack *activeStack)
-{
-	attackableHexes.clear();
-	if (activeStack)
-		occupyableHexes = curInt->cb->battleGetAvailableHexes(activeStack, true, &attackableHexes);
-
-	auto fillStackCountOutsideHexes = [&]()
-	{
-		auto accessibility = curInt->cb->getAccesibility();
-
-		for(int i = 0; i < accessibility.size(); i++)
-			stackCountOutsideHexes.at(i) = (accessibility[i] == EAccessibility::ACCESSIBLE);
-	};
-
-	fillStackCountOutsideHexes();
-
-	//prepare background graphic with hexes and shaded hexes
-	blitAt(background, 0, 0, backgroundWithHexes);
-
-	//draw absolute obstacles (cliffs and so on)
-	for(auto & oi : curInt->cb->battleGetAllObstacles())
-	{
-		if(oi->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
-		{
-			auto img = getObstacleImage(*oi);
-			if(img)
-				img->draw(backgroundWithHexes, oi->getInfo().width, oi->getInfo().height);
-		}
-	}
-
-	if (settings["battle"]["stackRange"].Bool())
-	{
-		std::vector<BattleHex> hexesToShade = occupyableHexes;
-		hexesToShade.insert(hexesToShade.end(), attackableHexes.begin(), attackableHexes.end());
-		for (BattleHex hex : hexesToShade)
-		{
-			int i = hex.getY(); //row
-			int j = hex.getX()-1; //column
-			int x = 58 + (i%2==0 ? 22 : 0) + 44*j;
-			int y = 86 + 42 *i;
-			SDL_Rect temp_rect = genRect(cellShade->h, cellShade->w, x, y);
-			CSDL_Ext::blit8bppAlphaTo24bpp(cellShade, nullptr, backgroundWithHexes, &temp_rect);
-		}
-	}
-
-	if(settings["battle"]["cellBorders"].Bool())
-		CSDL_Ext::blit8bppAlphaTo24bpp(cellBorders, nullptr, backgroundWithHexes, nullptr);
-}
-
-void CBattleInterface::showPiecesOfWall(SDL_Surface *to, std::vector<int> pieces)
-{
-	if (!siegeH)
-		return;
-	for (auto piece : pieces)
-	{
-		if (piece < 15) // not a tower - just print
-			siegeH->printPartOfWall(to, piece);
-		else // tower. find if tower is built and not destroyed - stack is present
-		{
-			// PieceID    StackID
-			// 15 = keep,  -2
-			// 16 = lower, -3
-			// 17 = upper, -4
-
-			// tower. check if tower is alive - stack is found
-			int stackPos = 13 - piece;
-
-			const CStack *turret = nullptr;
-
-			for (auto & stack : curInt->cb->battleGetAllStacks(true))
-			{
-				if(stack->initialPosition == stackPos)
-				{
-					turret = stack;
-					break;
-				}
-			}
-
-			if (turret)
-			{
-				std::vector<const CStack *> stackList(1, turret);
-				showStacks(to, stackList);
-				siegeH->printPartOfWall(to, piece);
-			}
-		}
-	}
-}

+ 0 - 414
client/battle/CBattleInterface.h

@@ -1,414 +0,0 @@
-/*
- * CBattleInterface.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 <vcmi/spells/Magic.h>
-
-#include "../../lib/ConstTransitivePtr.h" //may be redundant
-#include "../../lib/GameConstants.h"
-
-#include "CBattleAnimations.h"
-
-#include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation
-#include "../../lib/CCreatureHandler.h"
-#include "../../lib/battle/CBattleInfoCallback.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class CCreatureSet;
-class CGHeroInstance;
-class CStack;
-struct BattleResult;
-struct BattleSpellCast;
-struct CObstacleInstance;
-template <typename T> struct CondSh;
-struct SetStackEffect;
-class BattleAction;
-class CGTownInstance;
-struct CatapultAttack;
-struct BattleTriggerEffect;
-struct BattleHex;
-struct InfoAboutHero;
-class CBattleGameInterface;
-struct CustomEffectInfo;
-class CSpell;
-
-VCMI_LIB_NAMESPACE_END
-
-class CLabel;
-class CCallback;
-class CButton;
-class CToggleButton;
-class CToggleGroup;
-struct CatapultProjectileInfo;
-class CBattleAnimation;
-class CBattleHero;
-class CBattleConsole;
-class CBattleResultWindow;
-class CStackQueue;
-class CPlayerInterface;
-class CCreatureAnimation;
-struct ProjectileInfo;
-class CClickableHex;
-class CAnimation;
-class IImage;
-class CStackQueue;
-
-/// Small struct which contains information about the id of the attacked stack, the damage dealt,...
-struct StackAttackedInfo
-{
-	const CStack *defender; //attacked stack
-	int64_t dmg; //damage dealt
-	unsigned int amountKilled; //how many creatures in stack has been killed
-	const CStack *attacker; //attacking stack
-	bool indirectAttack; //if true, stack was attacked indirectly - spell or ranged attack
-	bool killed; //if true, stack has been killed
-	bool rebirth; //if true, play rebirth animation after all
-	bool cloneKilled;
-};
-
-/// Struct for battle effect animation e.g. morale, prayer, armageddon, bless,...
-struct BattleEffect
-{
-	int x, y; //position on the screen
-	float currentFrame;
-	std::shared_ptr<CAnimation> animation;
-	int effectID; //uniqueID equal ot ID of appropriate CSpellEffectAnim
-	BattleHex position; //Indicates if effect which hex the effect is drawn on
-};
-
-struct BattleObjectsByHex
-{
-	typedef std::vector<int> TWallList;
-	typedef std::vector<const CStack *> TStackList;
-	typedef std::vector<const BattleEffect *> TEffectList;
-	typedef std::vector<std::shared_ptr<const CObstacleInstance>> TObstacleList;
-
-	struct HexData
-	{
-		TWallList walls;
-		TStackList dead;
-		TStackList alive;
-		TEffectList effects;
-		TObstacleList obstacles;
-	};
-
-	HexData beforeAll;
-	HexData afterAll;
-	std::array<HexData, GameConstants::BFIELD_SIZE> hex;
-};
-
-/// Small struct which is needed for drawing the parabolic trajectory of the catapult cannon
-struct CatapultProjectileInfo
-{
-	CatapultProjectileInfo(Point from, Point dest);
-
-	double facA, facB, facC;
-
-	double calculateY(double x);
-};
-
-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
-{
-private:
-	SDL_Surface *background, *menu, *amountNormal, *amountNegative, *amountPositive, *amountEffNeutral, *cellBorders, *backgroundWithHexes;
-
-	std::shared_ptr<CButton> bOptions;
-	std::shared_ptr<CButton> bSurrender;
-	std::shared_ptr<CButton> bFlee;
-	std::shared_ptr<CButton> bAutofight;
-	std::shared_ptr<CButton> bSpell;
-	std::shared_ptr<CButton> bWait;
-	std::shared_ptr<CButton> bDefence;
-	std::shared_ptr<CButton> bConsoleUp;
-	std::shared_ptr<CButton> bConsoleDown;
-	std::shared_ptr<CButton> btactNext;
-	std::shared_ptr<CButton> btactEnd;
-
-	std::shared_ptr<CBattleConsole> console;
-	std::shared_ptr<CBattleHero> attackingHero;
-	std::shared_ptr<CBattleHero> defendingHero;
-	std::shared_ptr<CStackQueue> queue;
-
-	const CCreatureSet *army1, *army2; //copy of initial armies (for result window)
-	const CGHeroInstance *attackingHeroInstance, *defendingHeroInstance;
-	std::map<int32_t, std::shared_ptr<CCreatureAnimation>> creAnims; //animations of creatures from fighting armies (order by BattleInfo's stacks' ID)
-
-	std::map<int, std::shared_ptr<CAnimation>> idToProjectile;
-	std::map<int, std::vector<CCreature::CreatureAnimation::RayColor>> idToRay;
-
-	std::map<std::string, std::shared_ptr<CAnimation>> animationsCache;
-	std::map<si32, std::shared_ptr<CAnimation>> obstacleAnimations;
-
-	std::map<int, bool> creDir; // <creatureID, if false reverse creature's animation> //TODO: move it to battle callback
-	ui8 animCount;
-	const CStack *activeStack; //number of active stack; nullptr - no one
-	const CStack *mouseHoveredStack; // stack below mouse pointer, used for border animation
-	const CStack *stackToActivate; //when animation is playing, we should wait till the end to make the next stack active; nullptr of none
-	const CStack *selectedStack; //for Teleport / Sacrifice
-	void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all
-	std::vector<BattleHex> occupyableHexes, //hexes available for active stack
-		attackableHexes; //hexes attackable by active stack
-	std::array<bool, GameConstants::BFIELD_SIZE> stackCountOutsideHexes; // hexes that when in front of a unit cause it's amount box to move back
-	BattleHex previouslyHoveredHex; //number of hex that was hovered by the cursor a while ago
-	BattleHex currentlyHoveredHex; //number of hex that is supposed to be hovered (for a while it may be inappropriately set, but will be renewed soon)
-	int attackingHex; //hex from which the stack would perform attack with current cursor
-
-	std::shared_ptr<CPlayerInterface> tacticianInterface; //used during tactics mode, points to the interface of player with higher tactics (can be either attacker or defender in hot-seat), valid onloy for human players
-	bool tacticsMode;
-	bool stackCanCastSpell; //if true, active stack could possibly cast some target spell
-	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
-	si32 creatureSpellToCast;
-	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
-
-	void setActiveStack(const CStack *stack);
-	void setHoveredStack(const CStack *stack);
-
-	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();
-
-	std::list<ProjectileInfo> projectiles; //projectiles flying on battlefield
-	void giveCommand(EActionType action, BattleHex tile = BattleHex(), si32 additional = -1);
-	void sendCommand(BattleAction *& command, const CStack * actor = nullptr);
-
-	bool isTileAttackable(const BattleHex & number) const; //returns true if tile 'number' is neighboring any tile from active stack's range or is one of these tiles
-	bool isCatapultAttackable(BattleHex hex) const; //returns true if given tile can be attacked by catapult
-
-	std::list<BattleEffect> battleEffects; //different animations to display on the screen like spell effects
-
-	/// Class which is responsible for drawing the wall of a siege during battle
-	class SiegeHelper
-	{
-	private:
-		SDL_Surface* walls[18];
-		const CBattleInterface *owner;
-	public:
-		const CGTownInstance *town; //besieged town
-
-		SiegeHelper(const CGTownInstance *siegeTown, const CBattleInterface *_owner);
-		~SiegeHelper();
-
-		std::string getSiegeName(ui16 what) const;
-		std::string getSiegeName(ui16 what, int state) const; // state uses EWallState enum
-
-		void printPartOfWall(SDL_Surface *to, int what);
-
-		enum EWallVisual
-		{
-			BACKGROUND = 0,
-			BACKGROUND_WALL = 1,
-			KEEP,
-			BOTTOM_TOWER,
-			BOTTOM_WALL,
-			WALL_BELLOW_GATE,
-			WALL_OVER_GATE,
-			UPPER_WALL,
-			UPPER_TOWER,
-			GATE,
-			GATE_ARCH,
-			BOTTOM_STATIC_WALL,
-			UPPER_STATIC_WALL,
-			MOAT,
-			BACKGROUND_MOAT,
-			KEEP_BATTLEMENT,
-			BOTTOM_BATTLEMENT,
-			UPPER_BATTLEMENT
-		};
-
-		friend class CBattleInterface;
-	} *siegeH;
-
-	std::shared_ptr<CPlayerInterface> attackerInt, defenderInt; //because LOCPLINT is not enough in hotSeat
-	std::shared_ptr<CPlayerInterface> curInt; //current player interface
-	const CGHeroInstance *getActiveHero(); //returns hero that can currently cast a spell
-
-	/** Methods for displaying battle screen */
-	void showBackground(SDL_Surface *to);
-
-	void showBackgroundImage(SDL_Surface *to);
-	void showAbsoluteObstacles(SDL_Surface *to);
-	void showHighlightedHexes(SDL_Surface *to);
-	void showHighlightedHex(SDL_Surface *to, BattleHex hex, bool darkBorder = false);
-	void showInterface(SDL_Surface *to);
-
-	void showBattlefieldObjects(SDL_Surface *to);
-
-	void showAliveStacks(SDL_Surface *to, std::vector<const CStack *> stacks);
-	void showStacks(SDL_Surface *to, std::vector<const CStack *> stacks);
-	void showObstacles(SDL_Surface *to, std::vector<std::shared_ptr<const CObstacleInstance>> &obstacles);
-	void showPiecesOfWall(SDL_Surface *to, std::vector<int> pieces);
-
-	void showBattleEffects(SDL_Surface *to, const std::vector<const BattleEffect *> &battleEffects);
-	void showProjectiles(SDL_Surface *to);
-
-	BattleObjectsByHex sortObjectsByHex();
-	void updateBattleAnimations();
-
-	std::shared_ptr<IImage> getObstacleImage(const CObstacleInstance & oi);
-
-	Point getObstaclePosition(std::shared_ptr<IImage> image, const CObstacleInstance & obstacle);
-
-	void redrawBackgroundWithHexes(const CStack *activeStack);
-	/** End of battle screen blitting methods */
-
-	void setHeroAnimation(ui8 side, int phase);
-public:
-	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
-
-	std::list<std::pair<CBattleAnimation *, bool>> pendingAnims; //currently displayed animations <anim, initialized>
-	void addNewAnim(CBattleAnimation *anim); //adds new anim to pendingAnims
-	ui32 animIDhelper; //for giving IDs for animations
-
-
-	CBattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, const SDL_Rect & myRect, std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen, std::shared_ptr<CPlayerInterface> spectatorInt = nullptr);
-	virtual ~CBattleInterface();
-
-	//std::vector<TimeInterested*> timeinterested; //animation handling
-	void setPrintCellBorders(bool set); //if true, cell borders will be printed
-	void setPrintStackRange(bool set); //if true,range of active stack will be printed
-	void setPrintMouseShadow(bool set); //if true, hex under mouse will be shaded
-	void setAnimSpeed(int set); //speed of animation; range 1..100
-	int getAnimSpeed() const; //speed of animation; range 1..100
-	CPlayerInterface *getCurrentPlayerInterface() const;
-	bool shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex);
-
-	std::vector<std::shared_ptr<CClickableHex>> bfield; //11 lines, 17 hexes on each
-	SDL_Surface *cellBorder, *cellShade;
-
-	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
-
-	const BattleResult *bresult; //result of a battle; if non-zero then display when all animations end
-
-	// block all UI elements, e.g. during enemy turn
-	// unlike activate/deactivate this method will correctly grey-out all elements
-	void blockUI(bool on);
-
-	//button handle funcs:
-	void bOptionsf();
-	void bSurrenderf();
-	void bFleef();
-	void reallyFlee(); //performs fleeing without asking player
-	void reallySurrender(); //performs surrendering without asking player
-	void bAutofightf();
-	void bSpellf();
-	void bWaitf();
-	void bDefencef();
-	void bConsoleUpf();
-	void bConsoleDownf();
-	void bTacticNextStack(const CStack *current = nullptr);
-	void bEndTacticPhase();
-	//end of button handle funcs
-	//napisz tu klase odpowiadajaca za wyswietlanie bitwy i obsluge uzytkownika, polecenia ma przekazywac callbackiem
-	void activate() override;
-	void deactivate() override;
-	void keyPressed(const SDL_KeyboardEvent & key) override;
-	void mouseMoved(const SDL_MouseMotionEvent &sEvent) override;
-	void clickRight(tribool down, bool previousState) override;
-
-	void show(SDL_Surface *to) override;
-	void showAll(SDL_Surface *to) override;
-
-	//call-ins
-	void startAction(const BattleAction* action);
-	void unitAdded(const CStack * stack); //new stack appeared on battlefield
-	void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled
-	void stackActivated(const CStack *stack); //active stack has been changed
-	void stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance); //stack with id number moved to destHex
-	void waitForAnims();
-	void stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos); //called when a certain amount of stacks has been attacked
-	void stackAttacking(const CStack *attacker, BattleHex dest, const CStack *attacked, bool shooting); //called when stack with id ID is attacking something on hex dest
-	void newRoundFirst( int round );
-	void newRound(int number); //caled when round is ended; number is the number of round
-	void hexLclicked(int whichOne); //hex only call-in
-	void stackIsCatapulting(const CatapultAttack & ca); //called when a stack is attacking walls
-	void battleFinished(const BattleResult& br); //called when battle is finished - battleresult window should be printed
-	void displayBattleFinished(); //displays battle result
-	void spellCast(const BattleSpellCast *sc); //called when a hero casts a spell
-	void battleStacksEffectsSet(const SetStackEffect & sse); //called when a specific effect is set to stacks
-	void castThisSpell(SpellID spellID); //called when player has chosen a spell from spellbook
-
-	void displayBattleLog(const std::vector<MetaString> & battleLog);
-	void displayCustomEffects(const std::vector<CustomEffectInfo> & customEffects);
-
-	void displayEffect(ui32 effect, BattleHex destTile); //displays custom effect on the battlefield
-
-	void displaySpellAnimationQueue(const CSpell::TAnimationQueue & q, BattleHex destinationTile);
-	void displaySpellCast(SpellID spellID, BattleHex destinationTile); //displays spell`s cast animation
-	void displaySpellEffect(SpellID spellID, BattleHex destinationTile); //displays spell`s affected animation
-	void displaySpellHit(SpellID spellID, BattleHex destinationTile); //displays spell`s affected animation
-
-	void battleTriggerEffect(const BattleTriggerEffect & bte);
-	void setBattleCursor(const int myNumber); //really complex and messy, sets attackingHex
-	void endAction(const BattleAction* action);
-	void hideQueue();
-	void showQueue();
-
-	Rect hexPosition(BattleHex hex) const;
-
-	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
-
-	BattleHex fromWhichHexAttack(BattleHex myNumber);
-	void obstaclePlaced(const CObstacleInstance & oi);
-
-	void gateStateChanged(const EGateState state);
-
-	void initStackProjectile(const CStack * stack);
-
-	const CGHeroInstance *currentHero() const;
-	InfoAboutHero enemyHero() const;
-
-	friend class CPlayerInterface;
-	friend class CButton;
-	friend class CInGameConsole;
-	friend class CStackQueue;
-	friend class CBattleResultWindow;
-	friend class CBattleHero;
-	friend class CEffectAnimation;
-	friend class CBattleStackAnimation;
-	friend class CReverseAnimation;
-	friend class CDefenceAnimation;
-	friend class CMovementAnimation;
-	friend class CMovementStartAnimation;
-	friend class CAttackAnimation;
-	friend class CMeleeAttackAnimation;
-	friend class CShootingAnimation;
-	friend class CCastAnimation;
-	friend class CClickableHex;
-};

+ 0 - 365
client/battle/CCreatureAnimation.cpp

@@ -1,365 +0,0 @@
-/*
- * CCreatureAnimation.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 "CCreatureAnimation.h"
-
-#include "../../lib/CConfigHandler.h"
-#include "../../lib/CCreatureHandler.h"
-
-#include "../gui/SDL_Extensions.h"
-
-static const SDL_Color creatureBlueBorder = { 0, 255, 255, 255 };
-static const SDL_Color creatureGoldBorder = { 255, 255, 0, 255 };
-static const SDL_Color creatureNoBorder  =  { 0, 0, 0, 0 };
-
-SDL_Color AnimationControls::getBlueBorder()
-{
-	return creatureBlueBorder;
-}
-
-SDL_Color AnimationControls::getGoldBorder()
-{
-	return creatureGoldBorder;
-}
-
-SDL_Color AnimationControls::getNoBorder()
-{
-	return creatureNoBorder;
-}
-
-std::shared_ptr<CCreatureAnimation> AnimationControls::getAnimation(const CCreature * creature)
-{
-	auto func = std::bind(&AnimationControls::getCreatureAnimationSpeed, creature, _1, _2);
-	return std::make_shared<CCreatureAnimation>(creature->animDefName, func);
-}
-
-float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, const CCreatureAnimation * anim, size_t group)
-{
-	CCreatureAnim::EAnimType type = CCreatureAnim::EAnimType(group);
-
-	assert(creature->animation.walkAnimationTime != 0);
-	assert(creature->animation.attackAnimationTime != 0);
-	assert(anim->framesInGroup(type) != 0);
-
-	// possible new fields for creature format:
-	//split "Attack time" into "Shoot Time" and "Cast Time"
-
-	// a lot of arbitrary multipliers, mostly to make animation speed closer to H3
-	const float baseSpeed = 0.1f;
-	const float speedMult = static_cast<float>(settings["battle"]["animationSpeed"].Float());
-	const float speed = baseSpeed / speedMult;
-
-	switch (type)
-	{
-	case CCreatureAnim::MOVING:
-		return static_cast<float>(speed * 2 * creature->animation.walkAnimationTime / anim->framesInGroup(type));
-
-	case CCreatureAnim::MOUSEON:
-		return baseSpeed;
-	case CCreatureAnim::HOLDING:
-		return static_cast<float>(baseSpeed * creature->animation.idleAnimationTime / anim->framesInGroup(type));
-
-	case CCreatureAnim::SHOOT_UP:
-	case CCreatureAnim::SHOOT_FRONT:
-	case CCreatureAnim::SHOOT_DOWN:
-	case CCreatureAnim::CAST_UP:
-	case CCreatureAnim::CAST_FRONT:
-	case CCreatureAnim::CAST_DOWN:
-	case CCreatureAnim::VCMI_CAST_DOWN:
-	case CCreatureAnim::VCMI_CAST_FRONT:
-	case CCreatureAnim::VCMI_CAST_UP:
-		return static_cast<float>(speed * 4 * creature->animation.attackAnimationTime / anim->framesInGroup(type));
-
-	// as strange as it looks like "attackAnimationTime" does not affects melee attacks
-	// necessary because length of these animations must be same for all creatures for synchronization
-	case CCreatureAnim::ATTACK_UP:
-	case CCreatureAnim::ATTACK_FRONT:
-	case CCreatureAnim::ATTACK_DOWN:
-	case CCreatureAnim::HITTED:
-	case CCreatureAnim::DEFENCE:
-	case CCreatureAnim::DEATH:
-	case CCreatureAnim::DEATH_RANGED:
-	case CCreatureAnim::VCMI_2HEX_DOWN:
-	case CCreatureAnim::VCMI_2HEX_FRONT:
-	case CCreatureAnim::VCMI_2HEX_UP:
-		return speed * 3 / anim->framesInGroup(type);
-
-	case CCreatureAnim::TURN_L:
-	case CCreatureAnim::TURN_R:
-		return speed / 3;
-
-	case CCreatureAnim::MOVE_START:
-	case CCreatureAnim::MOVE_END:
-		return speed / 3;
-
-	case CCreatureAnim::DEAD:
-	case CCreatureAnim::DEAD_RANGED:
-		return speed;
-
-	default:
-		return speed;
-	}
-}
-
-float AnimationControls::getProjectileSpeed()
-{
-	return static_cast<float>(settings["battle"]["animationSpeed"].Float() * 100);
-}
-
-float AnimationControls::getSpellEffectSpeed()
-{
-	return static_cast<float>(settings["battle"]["animationSpeed"].Float() * 30);
-}
-
-float AnimationControls::getMovementDuration(const CCreature * creature)
-{
-	return static_cast<float>(settings["battle"]["animationSpeed"].Float() * 4 / creature->animation.walkAnimationTime);
-}
-
-float AnimationControls::getFlightDistance(const CCreature * creature)
-{
-	return static_cast<float>(creature->animation.flightAnimationDistance * 200);
-}
-
-CCreatureAnim::EAnimType CCreatureAnimation::getType() const
-{
-	return type;
-}
-
-void CCreatureAnimation::setType(CCreatureAnim::EAnimType type)
-{
-	this->type = type;
-	currentFrame = 0;
-	once = false;
-
-	play();
-}
-
-void CCreatureAnimation::shiftColor(const ColorShifter* shifter)
-{
-	if(forward)
-		forward->shiftColor(shifter);
-
-	if(reverse)
-		reverse->shiftColor(shifter);
-}
-
-CCreatureAnimation::CCreatureAnimation(const std::string & name_, TSpeedController controller)
-	: name(name_),
-	  speed(0.1f),
-	  currentFrame(0),
-	  elapsedTime(0),
-	  type(CCreatureAnim::HOLDING),
-	  border(CSDL_Ext::makeColor(0, 0, 0, 0)),
-	  speedController(controller),
-	  once(false)
-{
-	forward = std::make_shared<CAnimation>(name_);
-	reverse = std::make_shared<CAnimation>(name_);
-
-	//todo: optimize
-	forward->preload();
-	reverse->preload();
-
-	// if necessary, add one frame into vcmi-only group DEAD
-	if(forward->size(CCreatureAnim::DEAD) == 0)
-	{
-		forward->duplicateImage(CCreatureAnim::DEATH, forward->size(CCreatureAnim::DEATH)-1, CCreatureAnim::DEAD);
-		reverse->duplicateImage(CCreatureAnim::DEATH, reverse->size(CCreatureAnim::DEATH)-1, CCreatureAnim::DEAD);
-	}
-
-	if(forward->size(CCreatureAnim::DEAD_RANGED) == 0 && forward->size(CCreatureAnim::DEATH_RANGED) != 0)
-	{
-		forward->duplicateImage(CCreatureAnim::DEATH_RANGED, forward->size(CCreatureAnim::DEATH_RANGED)-1, CCreatureAnim::DEAD_RANGED);
-		reverse->duplicateImage(CCreatureAnim::DEATH_RANGED, reverse->size(CCreatureAnim::DEATH_RANGED)-1, CCreatureAnim::DEAD_RANGED);
-	}
-
-	//TODO: get dimensions form CAnimation
-	auto first = forward->getImage(0, type, true);
-
-	if(!first)
-	{
-		fullWidth = 0;
-		fullHeight = 0;
-		return;
-	}
-	fullWidth = first->width();
-	fullHeight = first->height();
-
-	reverse->verticalFlip();
-
-	play();
-}
-
-void CCreatureAnimation::endAnimation()
-{
-	once = false;
-	auto copy = onAnimationReset;
-	onAnimationReset.clear();
-	copy();
-}
-
-bool CCreatureAnimation::incrementFrame(float timePassed)
-{
-	elapsedTime += timePassed;
-	currentFrame += timePassed * speed;
-	const auto framesNumber = framesInGroup(type);
-
-	if(framesNumber <= 0)
-	{
-		endAnimation();
-	}
-	else if(currentFrame >= float(framesNumber))
-	{
-		// just in case of extremely low fps (or insanely high speed)
-		while(currentFrame >= float(framesNumber))
-			currentFrame -= framesNumber;
-
-		if(once)
-			setType(CCreatureAnim::HOLDING);
-
-		endAnimation();
-		return true;
-	}
-	return false;
-}
-
-void CCreatureAnimation::setBorderColor(SDL_Color palette)
-{
-	border = palette;
-}
-
-int CCreatureAnimation::getWidth() const
-{
-	return fullWidth;
-}
-
-int CCreatureAnimation::getHeight() const
-{
-	return fullHeight;
-}
-
-float CCreatureAnimation::getCurrentFrame() const
-{
-	return currentFrame;
-}
-
-void CCreatureAnimation::playOnce( CCreatureAnim::EAnimType type )
-{
-	setType(type);
-	once = true;
-}
-
-inline int getBorderStrength(float time)
-{
-	float borderStrength = fabs(vstd::round(time) - time) * 2; // generate value in range 0-1
-
-	return static_cast<int>(borderStrength * 155 + 100); // scale to 0-255
-}
-
-static SDL_Color genShadow(ui8 alpha)
-{
-	return CSDL_Ext::makeColor(0, 0, 0, alpha);
-}
-
-static SDL_Color genBorderColor(ui8 alpha, const SDL_Color & base)
-{
-	return CSDL_Ext::makeColor(base.r, base.g, base.b, ui8(base.a * alpha / 256));
-}
-
-static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2)
-{
-	return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256;
-}
-
-static SDL_Color addColors(const SDL_Color & base, const SDL_Color & over)
-{
-	return CSDL_Ext::makeColor(
-			mixChannels(over.r, base.r, over.a, base.a),
-			mixChannels(over.g, base.g, over.a, base.a),
-			mixChannels(over.b, base.b, over.a, base.a),
-			ui8(over.a + base.a * (255 - over.a) / 256)
-			);
-}
-
-void CCreatureAnimation::genBorderPalette(IImage::BorderPallete & target)
-{
-	target[0] = genBorderColor(getBorderStrength(elapsedTime), border);
-	target[1] = addColors(genShadow(128), genBorderColor(getBorderStrength(elapsedTime), border));
-	target[2] = addColors(genShadow(64),  genBorderColor(getBorderStrength(elapsedTime), border));
-}
-
-void CCreatureAnimation::nextFrame(SDL_Surface * dest, bool attacker)
-{
-	size_t frame = static_cast<size_t>(floor(currentFrame));
-
-	std::shared_ptr<IImage> image;
-
-	if(attacker)
-		image = forward->getImage(frame, type);
-	else
-		image = reverse->getImage(frame, type);
-
-	if(image)
-	{
-		IImage::BorderPallete borderPallete;
-		genBorderPalette(borderPallete);
-
-		image->setBorderPallete(borderPallete);
-
-		image->draw(dest, pos.x, pos.y);
-	}
-}
-
-int CCreatureAnimation::framesInGroup(CCreatureAnim::EAnimType group) const
-{
-	return static_cast<int>(forward->size(group));
-}
-
-bool CCreatureAnimation::isDead() const
-{
-	return getType() == CCreatureAnim::DEAD
-	    || getType() == CCreatureAnim::DEATH
-	    || getType() == CCreatureAnim::DEAD_RANGED
-	    || getType() == CCreatureAnim::DEATH_RANGED;
-}
-
-bool CCreatureAnimation::isIdle() const
-{
-	return getType() == CCreatureAnim::HOLDING
-	    || getType() == CCreatureAnim::MOUSEON;
-}
-
-bool CCreatureAnimation::isMoving() const
-{
-	return getType() == CCreatureAnim::MOVE_START
-	    || getType() == CCreatureAnim::MOVING
-	    || getType() == CCreatureAnim::MOVE_END;
-}
-
-bool CCreatureAnimation::isShooting() const
-{
-	return getType() == CCreatureAnim::SHOOT_UP
-	    || getType() == CCreatureAnim::SHOOT_FRONT
-	    || getType() == CCreatureAnim::SHOOT_DOWN;
-}
-
-void CCreatureAnimation::pause()
-{
-	speed = 0;
-}
-
-void CCreatureAnimation::play()
-{
-	//logAnim->trace("Play %s group %d at %d:%d", name, static_cast<int>(getType()), pos.x, pos.y);
-    speed = 0;
-    if(speedController(this, type) != 0)
-        speed = 1 / speedController(this, type);
-}

+ 0 - 124
client/battle/CCreatureAnimation.h

@@ -1,124 +0,0 @@
-/*
- * CCreatureAnimation.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/FunctionList.h"
-#include "../widgets/Images.h"
-#include "../gui/CAnimation.h"
-
-class CIntObject;
-class CCreatureAnimation;
-
-/// Namespace for some common controls of animations
-namespace AnimationControls
-{
-	/// get SDL_Color for creature selection borders
-	SDL_Color getBlueBorder();
-	SDL_Color getGoldBorder();
-	SDL_Color getNoBorder();
-
-	/// creates animation object with preset speed control
-	std::shared_ptr<CCreatureAnimation> getAnimation(const CCreature * creature);
-
-	/// returns animation speed of specific group, taking in mind game setting (in frames per second)
-	float getCreatureAnimationSpeed(const CCreature * creature, const CCreatureAnimation * anim, size_t groupID);
-
-	/// returns how far projectile should move each frame
-	/// TODO: make it time-based
-	float getProjectileSpeed();
-
-	/// returns speed of any spell effects, including any special effects like morale (in frames per second)
-	float getSpellEffectSpeed();
-
-	/// returns duration of full movement animation, in seconds. Needed to move animation on screen
-	float getMovementDuration(const CCreature * creature);
-
-	/// Returns distance on which flying creatures should during one animation loop
-	float getFlightDistance(const CCreature * creature);
-}
-
-/// Class which manages animations of creatures/units inside battles
-/// TODO: split into constant image container and class that does *control* of animation
-class CCreatureAnimation : public CIntObject
-{
-public:
-	typedef std::function<float(CCreatureAnimation *, size_t)> TSpeedController;
-
-private:
-	std::string name;
-	std::shared_ptr<CAnimation> forward;
-	std::shared_ptr<CAnimation> reverse;
-
-	int fullWidth;
-	int fullHeight;
-
-	// speed of animation, measure in frames per second
-	float speed;
-
-	// currently displayed frame. Float to allow H3-style animations where frames
-	// don't display for integer number of frames
-	float currentFrame;
-	// cumulative, real-time duration of animation. Used for effects like selection border
-	float elapsedTime;
-	CCreatureAnim::EAnimType type; //type of animation being displayed
-
-	// border color, disabled if alpha = 0
-	SDL_Color border;
-
-	TSpeedController speedController;
-
-	bool once; // animation will be played once and the reset to idling
-
-	void endAnimation();
-
-
-	void genBorderPalette(IImage::BorderPallete & target);
-public:
-
-	// function(s) that will be called when animation ends, after reset to 1st frame
-	// NOTE that these function will be fired only once
-	CFunctionList<void()> onAnimationReset;
-
-	int getWidth() const;
-	int getHeight() const;
-
-	/// Constructor
-	/// name - path to .def file, relative to SPRITES/ directory
-	/// controller - function that will return for how long *each* frame
-	/// in specified group of animation should be played, measured in seconds
-	CCreatureAnimation(const std::string & name_, TSpeedController speedController);
-
-	void setType(CCreatureAnim::EAnimType type); //sets type of animation and cleares framecount
-	CCreatureAnim::EAnimType getType() const; //returns type of animation
-
-	void nextFrame(SDL_Surface * dest, bool attacker);
-
-	// should be called every frame, return true when animation was reset to beginning
-	bool incrementFrame(float timePassed);
-	void setBorderColor(SDL_Color palette);
-
-	// tint color effect
-	void shiftColor(const ColorShifter * shifter);
-
-	float getCurrentFrame() const; // Gets the current frame ID relative to frame group.
-
-	void playOnce(CCreatureAnim::EAnimType type); //plays once given stage of animation, then resets to 2
-
-	int framesInGroup(CCreatureAnim::EAnimType group) const;
-
-	void pause();
-	void play();
-
-	//helpers. TODO: move them somewhere else
-	bool isDead() const;
-	bool isIdle() const;
-	bool isMoving() const;
-	bool isShooting() const;
-};

+ 412 - 0
client/battle/CreatureAnimation.cpp

@@ -0,0 +1,412 @@
+/*
+ * CCreatureAnimation.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 "CreatureAnimation.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CCreatureHandler.h"
+
+#include "../gui/Canvas.h"
+#include "../gui/ColorFilter.h"
+
+static const SDL_Color creatureBlueBorder = { 0, 255, 255, 255 };
+static const SDL_Color creatureGoldBorder = { 255, 255, 0, 255 };
+static const SDL_Color creatureNoBorder  =  { 0, 0, 0, 0 };
+
+static SDL_Color genShadow(ui8 alpha)
+{
+	return CSDL_Ext::makeColor(0, 0, 0, alpha);
+}
+
+SDL_Color AnimationControls::getBlueBorder()
+{
+	return creatureBlueBorder;
+}
+
+SDL_Color AnimationControls::getGoldBorder()
+{
+	return creatureGoldBorder;
+}
+
+SDL_Color AnimationControls::getNoBorder()
+{
+	return creatureNoBorder;
+}
+
+std::shared_ptr<CreatureAnimation> AnimationControls::getAnimation(const CCreature * creature)
+{
+	auto func = std::bind(&AnimationControls::getCreatureAnimationSpeed, creature, _1, _2);
+	return std::make_shared<CreatureAnimation>(creature->animDefName, func);
+}
+
+float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType type)
+{
+	assert(creature->animation.walkAnimationTime != 0);
+	assert(creature->animation.attackAnimationTime != 0);
+	assert(anim->framesInGroup(type) != 0);
+
+	// possible new fields for creature format:
+	//split "Attack time" into "Shoot Time" and "Cast Time"
+
+	// a lot of arbitrary multipliers, mostly to make animation speed closer to H3
+	const float baseSpeed = 0.1f;
+	const float speedMult = static_cast<float>(settings["battle"]["animationSpeed"].Float());
+	const float speed = baseSpeed / speedMult;
+
+	switch (type)
+	{
+	case ECreatureAnimType::MOVING:
+		return static_cast<float>(speed * 2 * creature->animation.walkAnimationTime / anim->framesInGroup(type));
+
+	case ECreatureAnimType::MOUSEON:
+		return baseSpeed;
+	case ECreatureAnimType::HOLDING:
+		return static_cast<float>(baseSpeed * creature->animation.idleAnimationTime / anim->framesInGroup(type));
+
+	case ECreatureAnimType::SHOOT_UP:
+	case ECreatureAnimType::SHOOT_FRONT:
+	case ECreatureAnimType::SHOOT_DOWN:
+	case ECreatureAnimType::SPECIAL_UP:
+	case ECreatureAnimType::SPECIAL_FRONT:
+	case ECreatureAnimType::SPECIAL_DOWN:
+	case ECreatureAnimType::CAST_DOWN:
+	case ECreatureAnimType::CAST_FRONT:
+	case ECreatureAnimType::CAST_UP:
+		return static_cast<float>(speed * 4 * creature->animation.attackAnimationTime / anim->framesInGroup(type));
+
+	// as strange as it looks like "attackAnimationTime" does not affects melee attacks
+	// necessary because length of these animations must be same for all creatures for synchronization
+	case ECreatureAnimType::ATTACK_UP:
+	case ECreatureAnimType::ATTACK_FRONT:
+	case ECreatureAnimType::ATTACK_DOWN:
+	case ECreatureAnimType::HITTED:
+	case ECreatureAnimType::DEFENCE:
+	case ECreatureAnimType::DEATH:
+	case ECreatureAnimType::DEATH_RANGED:
+	case ECreatureAnimType::RESURRECTION:
+	case ECreatureAnimType::GROUP_ATTACK_DOWN:
+	case ECreatureAnimType::GROUP_ATTACK_FRONT:
+	case ECreatureAnimType::GROUP_ATTACK_UP:
+		return speed * 3 / anim->framesInGroup(type);
+
+	case ECreatureAnimType::TURN_L:
+	case ECreatureAnimType::TURN_R:
+		return speed / 3;
+
+	case ECreatureAnimType::MOVE_START:
+	case ECreatureAnimType::MOVE_END:
+		return speed / 3;
+
+	case ECreatureAnimType::DEAD:
+	case ECreatureAnimType::DEAD_RANGED:
+		return speed;
+
+	default:
+		return speed;
+	}
+}
+
+float AnimationControls::getProjectileSpeed()
+{
+	return static_cast<float>(settings["battle"]["animationSpeed"].Float() * 4000);
+}
+
+float AnimationControls::getCatapultSpeed()
+{
+	return static_cast<float>(settings["battle"]["animationSpeed"].Float() * 1000);
+}
+
+float AnimationControls::getSpellEffectSpeed()
+{
+	return static_cast<float>(settings["battle"]["animationSpeed"].Float() * 30);
+}
+
+float AnimationControls::getMovementDuration(const CCreature * creature)
+{
+	return static_cast<float>(settings["battle"]["animationSpeed"].Float() * 4 / creature->animation.walkAnimationTime);
+}
+
+float AnimationControls::getFlightDistance(const CCreature * creature)
+{
+	return static_cast<float>(creature->animation.flightAnimationDistance * 200);
+}
+
+float AnimationControls::getFadeInDuration()
+{
+	return 1.0f / settings["battle"]["animationSpeed"].Float();
+}
+
+float AnimationControls::getObstaclesSpeed()
+{
+	return 10.0;// does not seems to be affected by animaiton speed settings
+}
+
+ECreatureAnimType CreatureAnimation::getType() const
+{
+	return type;
+}
+
+void CreatureAnimation::setType(ECreatureAnimType type)
+{
+	this->type = type;
+	currentFrame = 0;
+	once = false;
+
+	play();
+}
+
+CreatureAnimation::CreatureAnimation(const std::string & name_, TSpeedController controller)
+	: name(name_),
+	  speed(0.1f),
+	  shadowAlpha(128),
+	  currentFrame(0),
+	  elapsedTime(0),
+	  type(ECreatureAnimType::HOLDING),
+	  border(CSDL_Ext::makeColor(0, 0, 0, 0)),
+	  speedController(controller),
+	  once(false)
+{
+	forward = std::make_shared<CAnimation>(name_);
+	reverse = std::make_shared<CAnimation>(name_);
+
+	//todo: optimize
+	forward->preload();
+	reverse->preload();
+
+	// if necessary, add one frame into vcmi-only group DEAD
+	if(forward->size(size_t(ECreatureAnimType::DEAD)) == 0)
+	{
+		forward->duplicateImage(size_t(ECreatureAnimType::DEATH), forward->size(size_t(ECreatureAnimType::DEATH))-1, size_t(ECreatureAnimType::DEAD));
+		reverse->duplicateImage(size_t(ECreatureAnimType::DEATH), reverse->size(size_t(ECreatureAnimType::DEATH))-1, size_t(ECreatureAnimType::DEAD));
+	}
+
+	if(forward->size(size_t(ECreatureAnimType::DEAD_RANGED)) == 0 && forward->size(size_t(ECreatureAnimType::DEATH_RANGED)) != 0)
+	{
+		forward->duplicateImage(size_t(ECreatureAnimType::DEATH_RANGED), forward->size(size_t(ECreatureAnimType::DEATH_RANGED))-1, size_t(ECreatureAnimType::DEAD_RANGED));
+		reverse->duplicateImage(size_t(ECreatureAnimType::DEATH_RANGED), reverse->size(size_t(ECreatureAnimType::DEATH_RANGED))-1, size_t(ECreatureAnimType::DEAD_RANGED));
+	}
+
+	if(forward->size(size_t(ECreatureAnimType::FROZEN)) == 0)
+	{
+		forward->duplicateImage(size_t(ECreatureAnimType::HOLDING), 0, size_t(ECreatureAnimType::FROZEN));
+		reverse->duplicateImage(size_t(ECreatureAnimType::HOLDING), 0, size_t(ECreatureAnimType::FROZEN));
+	}
+
+	if(forward->size(size_t(ECreatureAnimType::RESURRECTION)) == 0)
+	{
+		for (size_t i = 0; i < forward->size(size_t(ECreatureAnimType::DEATH)); ++i)
+		{
+			size_t current = forward->size(size_t(ECreatureAnimType::DEATH)) - 1 - i;
+
+			forward->duplicateImage(size_t(ECreatureAnimType::DEATH), current, size_t(ECreatureAnimType::RESURRECTION));
+			reverse->duplicateImage(size_t(ECreatureAnimType::DEATH), current, size_t(ECreatureAnimType::RESURRECTION));
+		}
+	}
+
+	//TODO: get dimensions form CAnimation
+	auto first = forward->getImage(0, size_t(type), true);
+
+	if(!first)
+	{
+		fullWidth = 0;
+		fullHeight = 0;
+		return;
+	}
+	fullWidth = first->width();
+	fullHeight = first->height();
+
+	reverse->verticalFlip();
+
+	play();
+}
+
+void CreatureAnimation::endAnimation()
+{
+	once = false;
+	auto copy = onAnimationReset;
+	onAnimationReset.clear();
+	copy();
+}
+
+bool CreatureAnimation::incrementFrame(float timePassed)
+{
+	elapsedTime += timePassed;
+	currentFrame += timePassed * speed;
+	const auto framesNumber = framesInGroup(type);
+
+	if(framesNumber <= 0)
+	{
+		endAnimation();
+	}
+	else if(currentFrame >= float(framesNumber))
+	{
+		// just in case of extremely low fps (or insanely high speed)
+		while(currentFrame >= float(framesNumber))
+			currentFrame -= framesNumber;
+
+		if(once)
+			setType(ECreatureAnimType::HOLDING);
+
+		endAnimation();
+		return true;
+	}
+	return false;
+}
+
+void CreatureAnimation::setBorderColor(SDL_Color palette)
+{
+	border = palette;
+}
+
+int CreatureAnimation::getWidth() const
+{
+	return fullWidth;
+}
+
+int CreatureAnimation::getHeight() const
+{
+	return fullHeight;
+}
+
+float CreatureAnimation::getCurrentFrame() const
+{
+	return currentFrame;
+}
+
+void CreatureAnimation::playOnce( ECreatureAnimType type )
+{
+	setType(type);
+	once = true;
+}
+
+inline int getBorderStrength(float time)
+{
+	float borderStrength = fabs(vstd::round(time) - time) * 2; // generate value in range 0-1
+
+	return static_cast<int>(borderStrength * 155 + 100); // scale to 0-255
+}
+
+static SDL_Color genBorderColor(ui8 alpha, const SDL_Color & base)
+{
+	return CSDL_Ext::makeColor(base.r, base.g, base.b, ui8(base.a * alpha / 256));
+}
+
+static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2)
+{
+	return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256;
+}
+
+static SDL_Color addColors(const SDL_Color & base, const SDL_Color & over)
+{
+	return CSDL_Ext::makeColor(
+			mixChannels(over.r, base.r, over.a, base.a),
+			mixChannels(over.g, base.g, over.a, base.a),
+			mixChannels(over.b, base.b, over.a, base.a),
+			ui8(over.a + base.a * (255 - over.a) / 256)
+			);
+}
+
+void CreatureAnimation::genSpecialPalette(IImage::SpecialPalette & target)
+{
+	target[0] = genShadow(shadowAlpha / 2);
+	target[1] = genShadow(shadowAlpha / 2);
+	target[2] = genShadow(shadowAlpha);
+	target[3] = genShadow(shadowAlpha);
+	target[4] = genBorderColor(getBorderStrength(elapsedTime), border);
+	target[5] = addColors(genShadow(shadowAlpha),     genBorderColor(getBorderStrength(elapsedTime), border));
+	target[6] = addColors(genShadow(shadowAlpha / 2), genBorderColor(getBorderStrength(elapsedTime), border));
+}
+
+void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight)
+{
+	SDL_Color shadowTest = shifter.shiftColor(genShadow(128));
+	shadowAlpha = shadowTest.a;
+
+	size_t frame = static_cast<size_t>(floor(currentFrame));
+
+	std::shared_ptr<IImage> image;
+
+	if(facingRight)
+		image = forward->getImage(frame, size_t(type));
+	else
+		image = reverse->getImage(frame, size_t(type));
+
+	if(image)
+	{
+		IImage::SpecialPalette SpecialPalette;
+		genSpecialPalette(SpecialPalette);
+
+		image->setSpecialPallete(SpecialPalette);
+		image->adjustPalette(shifter);
+
+		canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h));
+
+	}
+}
+
+int CreatureAnimation::framesInGroup(ECreatureAnimType group) const
+{
+	return static_cast<int>(forward->size(size_t(group)));
+}
+
+bool CreatureAnimation::isDead() const
+{
+	return getType() == ECreatureAnimType::DEAD
+		|| getType() == ECreatureAnimType::DEAD_RANGED;
+}
+
+bool CreatureAnimation::isDying() const
+{
+	return getType() == ECreatureAnimType::DEATH
+		|| getType() == ECreatureAnimType::DEATH_RANGED;
+}
+
+bool CreatureAnimation::isDeadOrDying() const
+{
+	return getType() == ECreatureAnimType::DEAD
+		|| getType() == ECreatureAnimType::DEATH
+		|| getType() == ECreatureAnimType::DEAD_RANGED
+		|| getType() == ECreatureAnimType::DEATH_RANGED;
+}
+
+bool CreatureAnimation::isIdle() const
+{
+	return getType() == ECreatureAnimType::HOLDING
+		|| getType() == ECreatureAnimType::MOUSEON;
+}
+
+bool CreatureAnimation::isMoving() const
+{
+	return getType() == ECreatureAnimType::MOVE_START
+		|| getType() == ECreatureAnimType::MOVING
+		|| getType() == ECreatureAnimType::MOVE_END
+		|| getType() == ECreatureAnimType::TURN_L
+		|| getType() == ECreatureAnimType::TURN_R;
+}
+
+bool CreatureAnimation::isShooting() const
+{
+	return getType() == ECreatureAnimType::SHOOT_UP
+		|| getType() == ECreatureAnimType::SHOOT_FRONT
+		|| getType() == ECreatureAnimType::SHOOT_DOWN;
+}
+
+void CreatureAnimation::pause()
+{
+	speed = 0;
+}
+
+void CreatureAnimation::play()
+{
+	//logAnim->trace("Play %s group %d at %d:%d", name, static_cast<int>(getType()), pos.x, pos.y);
+    speed = 0;
+	if(speedController(this, type) != 0)
+		speed = 1 / speedController(this, type);
+}

+ 149 - 0
client/battle/CreatureAnimation.h

@@ -0,0 +1,149 @@
+/*
+ * CCreatureAnimation.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/FunctionList.h"
+#include "../widgets/Images.h"
+#include "../gui/CAnimation.h"
+
+class CIntObject;
+class CreatureAnimation;
+class Canvas;
+
+/// Namespace for some common controls of animations
+namespace AnimationControls
+{
+	/// get SDL_Color for creature selection borders
+	SDL_Color getBlueBorder();
+	SDL_Color getGoldBorder();
+	SDL_Color getNoBorder();
+
+	/// creates animation object with preset speed control
+	std::shared_ptr<CreatureAnimation> getAnimation(const CCreature * creature);
+
+	/// returns animation speed of specific group, taking in mind game setting (in frames per second)
+	float getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType groupID);
+
+	/// returns how far projectile should move per second
+	float getProjectileSpeed();
+
+	/// returns speed of catapult projectile, in pixels per second (horizontal axis only)
+	float getCatapultSpeed();
+
+	/// returns speed of any spell effects, including any special effects like morale (in frames per second)
+	float getSpellEffectSpeed();
+
+	/// returns duration of full movement animation, in seconds. Needed to move animation on screen
+	float getMovementDuration(const CCreature * creature);
+
+	/// Returns distance on which flying creatures should during one animation loop
+	float getFlightDistance(const CCreature * creature);
+
+	/// Returns total time for full fade-in effect on newly summoned creatures, in seconds
+	float getFadeInDuration();
+
+	/// Returns animation speed for obstacles, in frames per second
+	float getObstaclesSpeed();
+}
+
+/// Class which manages animations of creatures/units inside battles
+/// TODO: split into constant image container and class that does *control* of animation
+class CreatureAnimation : public CIntObject
+{
+public:
+	typedef std::function<float(CreatureAnimation *, ECreatureAnimType)> TSpeedController;
+
+private:
+	std::string name;
+
+	/// animation for rendering stack in default orientation - facing right
+	std::shared_ptr<CAnimation> forward;
+
+	/// animation that has all its frames flipped for rendering stack facing left
+	std::shared_ptr<CAnimation> reverse;
+
+	int fullWidth;
+	int fullHeight;
+
+	/// speed of animation, measure in frames per second
+	float speed;
+
+	/// currently displayed frame. Float to allow H3-style animations where frames
+	/// don't display for integer number of frames
+	float currentFrame;
+
+	/// cumulative, real-time duration of animation. Used for effects like selection border
+	float elapsedTime;
+
+	///type of animation being displayed
+	ECreatureAnimType type;
+
+	/// current value of shadow transparency
+	uint8_t shadowAlpha;
+
+	/// border color, disabled if alpha = 0
+	SDL_Color border;
+
+	TSpeedController speedController;
+
+	/// animation will be played once and the reset to idling
+	bool once;
+
+	void endAnimation();
+
+	void genSpecialPalette(IImage::SpecialPalette & target);
+public:
+
+	/// function(s) that will be called when animation ends, after reset to 1st frame
+	/// NOTE that these functions will be fired only once
+	CFunctionList<void()> onAnimationReset;
+
+	int getWidth() const;
+	int getHeight() const;
+
+	/// Constructor
+	/// name - path to .def file, relative to SPRITES/ directory
+	/// controller - function that will return for how long *each* frame
+	/// in specified group of animation should be played, measured in seconds
+	CreatureAnimation(const std::string & name_, TSpeedController speedController);
+
+	/// sets type of animation and resets framecount
+	void setType(ECreatureAnimType type);
+
+	/// returns currently rendered type of animation
+	ECreatureAnimType getType() const;
+
+	void nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight);
+
+	/// should be called every frame, return true when animation was reset to beginning
+	bool incrementFrame(float timePassed);
+
+	void setBorderColor(SDL_Color palette);
+
+	/// Gets the current frame ID within current group.
+	float getCurrentFrame() const;
+
+	/// plays once given type of animation, then resets to idle
+	void playOnce(ECreatureAnimType type);
+
+	/// returns number of frames in selected animation type
+	int framesInGroup(ECreatureAnimType group) const;
+
+	void pause();
+	void play();
+
+	/// helpers to classify current type of animation
+	bool isDead() const;
+	bool isDying() const;
+	bool isDeadOrDying() const;
+	bool isIdle() const;
+	bool isMoving() const;
+	bool isShooting() const;
+};

+ 51 - 53
client/gui/CAnimation.cpp

@@ -12,6 +12,7 @@
 
 #include "SDL_Extensions.h"
 #include "SDL_Pixels.h"
+#include "ColorFilter.h"
 
 #include "../CBitmapHandler.h"
 #include "../Graphics.h"
@@ -33,6 +34,7 @@ class CDefFile
 {
 private:
 
+	PACKED_STRUCT_BEGIN
 	struct SSpriteDef
 	{
 		ui32 size;
@@ -43,7 +45,7 @@ private:
 		ui32 height;
 		si32 leftMargin;
 		si32 topMargin;
-	} PACKED_STRUCT;
+	} PACKED_STRUCT_END;
 	//offset[group][frame] - offset of frame data in file
 	std::map<size_t, std::vector <size_t> > offset;
 
@@ -92,23 +94,24 @@ public:
 	// Keep the original palette, in order to do color switching operation
 	void savePalette();
 
-	void draw(SDL_Surface * where, int posX=0, int posY=0, Rect *src=nullptr, ui8 alpha=255) const override;
-	void draw(SDL_Surface * where, SDL_Rect * dest, SDL_Rect * src, ui8 alpha=255) const override;
+	void draw(SDL_Surface * where, int posX=0, int posY=0, const Rect *src=nullptr) const override;
+	void draw(SDL_Surface * where, const SDL_Rect * dest, const SDL_Rect * src) const override;
 	std::shared_ptr<IImage> scaleFast(float scale) const override;
 	void exportBitmap(const boost::filesystem::path & path) const override;
 	void playerColored(PlayerColor player) override;
 	void setFlagColor(PlayerColor player) override;
-	int width() const override;
-	int height() const override;
+	bool isTransparent(const Point & coords) const override;
+	Point dimensions() const override;
 
 	void horizontalFlip() override;
 	void verticalFlip() override;
 
 	void shiftPalette(int from, int howMany) override;
-	void adjustPalette(const ColorShifter * shifter) override;
+	void adjustPalette(const ColorFilter & shifter) override;
+	void resetPalette(int colorID) override;
 	void resetPalette() override;
 
-	void setBorderPallete(const BorderPallete & borderPallete) override;
+	void setSpecialPallete(const SpecialPalette & SpecialPalette) override;
 
 	friend class SDLImageLoader;
 
@@ -134,6 +137,11 @@ public:
 	~SDLImageLoader();
 };
 
+std::shared_ptr<IImage> IImage::createFromFile( const std::string & path )
+{
+	return std::shared_ptr<IImage>(new SDLImage(path));
+}
+
 // Extremely simple file cache. TODO: smarter, more general solution
 class CFileCache
 {
@@ -207,32 +215,17 @@ CDefFile::CDefFile(std::string Name):
 	data(nullptr),
 	palette(nullptr)
 {
-
-	#if 0
-	static SDL_Color H3_ORIG_PALETTE[8] =
-	{
-	   {  0, 255, 255, SDL_ALPHA_OPAQUE},
-	   {255, 150, 255, SDL_ALPHA_OPAQUE},
-	   {255, 100, 255, SDL_ALPHA_OPAQUE},
-	   {255,  50, 255, SDL_ALPHA_OPAQUE},
-	   {255,   0, 255, SDL_ALPHA_OPAQUE},
-	   {255, 255, 0,   SDL_ALPHA_OPAQUE},
-	   {180,   0, 255, SDL_ALPHA_OPAQUE},
-	   {  0, 255, 0,   SDL_ALPHA_OPAQUE}
-	};
-	#endif // 0
-
 	//First 8 colors in def palette used for transparency
 	static SDL_Color H3Palette[8] =
 	{
-		{   0,   0,   0,   0},// 100% - transparency
-		{   0,   0,   0,  32},//  75% - shadow border,
-		{   0,   0,   0,  64},// TODO: find exact value
-		{   0,   0,   0, 128},// TODO: for transparency
-		{   0,   0,   0, 128},//  50% - shadow body
-		{   0,   0,   0,   0},// 100% - selection highlight
-		{   0,   0,   0, 128},//  50% - shadow body   below selection
-		{   0,   0,   0,  64} // 75% - shadow border below selection
+		{   0,   0,   0,   0},// transparency                  ( used in most images )
+		{   0,   0,   0,  64},// shadow border                 ( used in battle, adventure map def's )
+		{   0,   0,   0,  64},// shadow border                 ( used in fog-of-war def's )
+		{   0,   0,   0, 128},// shadow body                   ( used in fog-of-war def's )
+		{   0,   0,   0, 128},// shadow body                   ( used in battle, adventure map def's )
+		{   0,   0,   0,   0},// selection                     ( used in battle def's )
+		{   0,   0,   0, 128},// shadow body   below selection ( used in battle def's )
+		{   0,   0,   0,  64} // shadow border below selection ( used in battle def's )
 	};
 	data = animationCache.getCachedFile(ResourceID(std::string("SPRITES/") + Name, EResType::ANIMATION));
 
@@ -554,6 +547,15 @@ SDLImageLoader::~SDLImageLoader()
 IImage::IImage() = default;
 IImage::~IImage() = default;
 
+int IImage::width() const
+{
+	return dimensions().x;
+}
+
+int IImage::height() const
+{
+	return dimensions().y;
+}
 
 SDLImage::SDLImage(CDefFile * data, size_t frame, size_t group)
 	: surf(nullptr),
@@ -640,17 +642,16 @@ SDLImage::SDLImage(std::string filename)
 	}
 }
 
-void SDLImage::draw(SDL_Surface *where, int posX, int posY, Rect *src, ui8 alpha) const
+void SDLImage::draw(SDL_Surface *where, int posX, int posY, const Rect *src) const
 {
 	if(!surf)
 		return;
 
 	Rect destRect(posX, posY, surf->w, surf->h);
-
 	draw(where, &destRect, src);
 }
 
-void SDLImage::draw(SDL_Surface* where, SDL_Rect* dest, SDL_Rect* src, ui8 alpha) const
+void SDLImage::draw(SDL_Surface* where, const SDL_Rect* dest, const SDL_Rect* src) const
 {
 	if (!surf)
 		return;
@@ -730,14 +731,14 @@ void SDLImage::setFlagColor(PlayerColor player)
 		CSDL_Ext::setPlayerColor(surf, player);
 }
 
-int SDLImage::width() const
+bool SDLImage::isTransparent(const Point & coords) const
 {
-	return fullSize.x;
+	return CSDL_Ext::isTransparent(surf, coords.x, coords.y);
 }
 
-int SDLImage::height() const
+Point SDLImage::dimensions() const
 {
-	return fullSize.y;
+	return fullSize;
 }
 
 void SDLImage::horizontalFlip()
@@ -790,7 +791,7 @@ void SDLImage::shiftPalette(int from, int howMany)
 	}
 }
 
-void SDLImage::adjustPalette(const ColorShifter * shifter)
+void SDLImage::adjustPalette(const ColorFilter & shifter)
 {
 	if(originalPalette == nullptr)
 		return;
@@ -800,7 +801,7 @@ void SDLImage::adjustPalette(const ColorShifter * shifter)
 	// Note: here we skip the first 8 colors in the palette that predefined in H3Palette
 	for(int i = 8; i < palette->ncolors; i++)
 	{
-		palette->colors[i] = shifter->shiftColor(originalPalette->colors[i]);
+		palette->colors[i] = shifter.shiftColor(originalPalette->colors[i]);
 	}
 }
 
@@ -813,11 +814,20 @@ void SDLImage::resetPalette()
 	SDL_SetPaletteColors(surf->format->palette, originalPalette->colors, 0, originalPalette->ncolors);
 }
 
-void SDLImage::setBorderPallete(const IImage::BorderPallete & borderPallete)
+void SDLImage::resetPalette( int colorID )
+{
+	if(originalPalette == nullptr)
+		return;
+
+	// Always keept the original palette not changed, copy a new palette to assign to surface
+	SDL_SetPaletteColors(surf->format->palette, originalPalette->colors + colorID, colorID, 1);
+}
+
+void SDLImage::setSpecialPallete(const IImage::SpecialPalette & SpecialPalette)
 {
 	if(surf->format->palette)
 	{
-		SDL_SetColors(surf, const_cast<SDL_Color *>(borderPallete.data()), 5, 3);
+		SDL_SetColors(surf, const_cast<SDL_Color *>(SpecialPalette.data()), 1, 7);
 	}
 }
 
@@ -1080,18 +1090,6 @@ void CAnimation::duplicateImage(const size_t sourceGroup, const size_t sourceFra
 		load(index, targetGroup);
 }
 
-void CAnimation::shiftColor(const ColorShifter * shifter)
-{
-	for(auto groupIter = images.begin(); groupIter != images.end(); groupIter++)
-	{
-		for(auto frameIter = groupIter->second.begin(); frameIter != groupIter->second.end(); frameIter++)
-		{
-			std::shared_ptr<IImage> image = frameIter->second;
-			image->adjustPalette(shifter);
-		}
-	}
-}
-
 void CAnimation::setCustom(std::string filename, size_t frame, size_t group)
 {
 	if (source[group].size() <= frame)

+ 17 - 12
client/gui/CAnimation.h

@@ -29,7 +29,7 @@ VCMI_LIB_NAMESPACE_END
 
 struct SDL_Surface;
 class CDefFile;
-class ColorShifter;
+class ColorFilter;
 
 /*
  * Base class for images, can be used for non-animation pictures as well
@@ -37,11 +37,11 @@ class ColorShifter;
 class IImage
 {
 public:
-	using BorderPallete = std::array<SDL_Color, 3>;
+	using SpecialPalette = std::array<SDL_Color, 7>;
 
 	//draws image on surface "where" at position
-	virtual void draw(SDL_Surface * where, int posX = 0, int posY = 0, Rect * src = nullptr, ui8 alpha = 255) const=0;
-	virtual void draw(SDL_Surface * where, SDL_Rect * dest, SDL_Rect * src, ui8 alpha = 255) const = 0;
+	virtual void draw(SDL_Surface * where, int posX = 0, int posY = 0, const Rect * src = nullptr) const = 0;
+	virtual void draw(SDL_Surface * where, const SDL_Rect * dest, const SDL_Rect * src) const = 0;
 
 	virtual std::shared_ptr<IImage> scaleFast(float scale) const = 0;
 
@@ -53,22 +53,30 @@ public:
 	//set special color for flag
 	virtual void setFlagColor(PlayerColor player)=0;
 
-	virtual int width() const=0;
-	virtual int height() const=0;
+	//test transparency of specific pixel
+	virtual bool isTransparent(const Point & coords) const = 0;
+
+	virtual Point dimensions() const = 0;
+	int width() const;
+	int height() const;
 
 	//only indexed bitmaps, 16 colors maximum
 	virtual void shiftPalette(int from, int howMany) = 0;
-	virtual void adjustPalette(const ColorShifter * shifter) = 0;
+	virtual void adjustPalette(const ColorFilter & shifter) = 0;
+	virtual void resetPalette(int colorID) = 0;
 	virtual void resetPalette() = 0;
 
-	//only indexed bitmaps, colors 5,6,7 must be special
-	virtual void setBorderPallete(const BorderPallete & borderPallete) = 0;
+	//only indexed bitmaps with 7 special colors
+	virtual void setSpecialPallete(const SpecialPalette & SpecialPalette) = 0;
 
 	virtual void horizontalFlip() = 0;
 	virtual void verticalFlip() = 0;
 
 	IImage();
 	virtual ~IImage();
+
+	/// loads image from specified file. Returns 0-sized images on failure
+	static std::shared_ptr<IImage> createFromFile( const std::string & path );
 };
 
 /// Class for handling animation
@@ -114,9 +122,6 @@ public:
 	//and loads it if animation is preloaded
 	void duplicateImage(const size_t sourceGroup, const size_t sourceFrame, const size_t targetGroup);
 
-	// adjust the color of the animation, used in battle spell effects, e.g. Cloned objects
-	void shiftColor(const ColorShifter * shifter);
-
 	//add custom surface to the selected position.
 	void setCustom(std::string filename, size_t frame, size_t group=0);
 

+ 78 - 32
client/gui/CCursorHandler.cpp

@@ -38,34 +38,44 @@ void CCursorHandler::replaceBuffer(CIntObject * payload)
 	updateBuffer(payload);
 }
 
-void CCursorHandler::initCursor()
+CCursorHandler::CCursorHandler()
+	: needUpdate(true)
+	, buffer(nullptr)
+	, cursorLayer(nullptr)
+	, frameTime(0.f)
+	, showing(false)
 {
 	cursorLayer = SDL_CreateTexture(mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 40, 40);
 	SDL_SetTextureBlendMode(cursorLayer, SDL_BLENDMODE_BLEND);
 
 	xpos = ypos = 0;
-	type = ECursor::DEFAULT;
+	type = Cursor::Type::DEFAULT;
 	dndObject = nullptr;
 
 	cursors =
 	{
-		make_unique<CAnimImage>("CRADVNTR", 0),
-		make_unique<CAnimImage>("CRCOMBAT", 0),
-		make_unique<CAnimImage>("CRDEFLT",  0),
-		make_unique<CAnimImage>("CRSPELL",  0)
+		std::make_unique<CAnimImage>("CRADVNTR", 0),
+		std::make_unique<CAnimImage>("CRCOMBAT", 0),
+		std::make_unique<CAnimImage>("CRDEFLT",  0),
+		std::make_unique<CAnimImage>("CRSPELL",  0)
 	};
 
-	currentCursor = cursors.at(int(ECursor::DEFAULT)).get();
+	currentCursor = cursors.at(static_cast<size_t>(Cursor::Type::DEFAULT)).get();
 
 	buffer = CSDL_Ext::newSurface(40,40);
 
 	SDL_SetSurfaceBlendMode(buffer, SDL_BLENDMODE_NONE);
 	SDL_ShowCursor(SDL_DISABLE);
 
-	changeGraphic(ECursor::ADVENTURE, 0);
+	set(Cursor::Map::POINTER);
 }
 
-void CCursorHandler::changeGraphic(ECursor::ECursorTypes type, int index)
+Point CCursorHandler::position() const
+{
+	return Point(xpos, ypos);
+}
+
+void CCursorHandler::changeGraphic(Cursor::Type type, size_t index)
 {
 	assert(dndObject == nullptr);
 
@@ -73,7 +83,7 @@ void CCursorHandler::changeGraphic(ECursor::ECursorTypes type, int index)
 	{
 		this->type = type;
 		this->frame = index;
-		currentCursor = cursors.at(int(type)).get();
+		currentCursor = cursors.at(static_cast<size_t>(type)).get();
 		currentCursor->setFrame(index);
 	}
 	else if(index != this->frame)
@@ -85,6 +95,27 @@ void CCursorHandler::changeGraphic(ECursor::ECursorTypes type, int index)
 	replaceBuffer(currentCursor);
 }
 
+void CCursorHandler::set(Cursor::Default index)
+{
+	changeGraphic(Cursor::Type::DEFAULT, static_cast<size_t>(index));
+}
+
+void CCursorHandler::set(Cursor::Map index)
+{
+	changeGraphic(Cursor::Type::ADVENTURE, static_cast<size_t>(index));
+}
+
+void CCursorHandler::set(Cursor::Combat index)
+{
+	changeGraphic(Cursor::Type::COMBAT, static_cast<size_t>(index));
+}
+
+void CCursorHandler::set(Cursor::Spellcast index)
+{
+	//Note: this is animated cursor, ignore specified frame and only change type
+	changeGraphic(Cursor::Type::SPELLBOOK, frame);
+}
+
 void CCursorHandler::dragAndDropCursor(std::unique_ptr<CAnimImage> object)
 {
 	dndObject = std::move(object);
@@ -102,54 +133,57 @@ void CCursorHandler::cursorMove(const int & x, const int & y)
 
 void CCursorHandler::shiftPos( int &x, int &y )
 {
-	if(( type == ECursor::COMBAT && frame != ECursor::COMBAT_POINTER) || type == ECursor::SPELLBOOK)
+	if(( type == Cursor::Type::COMBAT && frame != static_cast<size_t>(Cursor::Combat::POINTER)) || type == Cursor::Type::SPELLBOOK)
 	{
 		x-=16;
 		y-=16;
 
 		// Properly align the melee attack cursors.
-		if (type == ECursor::COMBAT)
+		if (type == Cursor::Type::COMBAT)
 		{
-			switch (frame)
+			switch (static_cast<Cursor::Combat>(frame))
 			{
-			case 7: // Bottom left
+			case Cursor::Combat::HIT_NORTHEAST:
 				x -= 6;
 				y += 16;
 				break;
-			case 8: // Left
+			case Cursor::Combat::HIT_EAST:
 				x -= 16;
 				y += 10;
 				break;
-			case 9: // Top left
+			case Cursor::Combat::HIT_SOUTHEAST:
 				x -= 6;
 				y -= 6;
 				break;
-			case 10: // Top right
+			case Cursor::Combat::HIT_SOUTHWEST:
 				x += 16;
 				y -= 6;
 				break;
-			case 11: // Right
+			case Cursor::Combat::HIT_WEST:
 				x += 16;
 				y += 11;
 				break;
-			case 12: // Bottom right
+			case Cursor::Combat::HIT_NORTHWEST:
 				x += 16;
 				y += 16;
 				break;
-			case 13: // Below
+			case Cursor::Combat::HIT_NORTH:
 				x += 9;
 				y += 16;
 				break;
-			case 14: // Above
+			case Cursor::Combat::HIT_SOUTH:
 				x += 9;
 				y -= 15;
 				break;
 			}
 		}
 	}
-	else if(type == ECursor::ADVENTURE)
+	else if(type == Cursor::Type::ADVENTURE)
 	{
-		if (frame == 0); //to exclude
+		if (frame == 0)
+		{
+			//no-op
+		}
 		else if(frame == 2)
 		{
 			x -= 12;
@@ -221,6 +255,27 @@ void CCursorHandler::render()
 	if(!showing)
 		return;
 
+	if (type == Cursor::Type::SPELLBOOK)
+	{
+		static const float frameDisplayDuration = 0.1f;
+
+		frameTime += GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+		size_t newFrame = frame;
+
+		while (frameTime > frameDisplayDuration)
+		{
+			frameTime -= frameDisplayDuration;
+			newFrame++;
+		}
+
+		auto & animation = cursors.at(static_cast<size_t>(type));
+
+		while (newFrame > animation->size())
+			newFrame -= animation->size();
+
+		changeGraphic(Cursor::Type::SPELLBOOK, newFrame);
+	}
+
 	//the must update texture in the main (renderer) thread, but changes to cursor type may come from other threads
 	updateTexture();
 
@@ -252,15 +307,6 @@ void CCursorHandler::updateTexture()
 	}
 }
 
-CCursorHandler::CCursorHandler()
-	: needUpdate(true),
-	buffer(nullptr),
-	cursorLayer(nullptr),
-	showing(false)
-{
-
-}
-
 CCursorHandler::~CCursorHandler()
 {
 	if(buffer)

+ 119 - 18
client/gui/CCursorHandler.h

@@ -12,16 +12,97 @@ class CIntObject;
 class CAnimImage;
 struct SDL_Surface;
 struct SDL_Texture;
+struct Point;
 
-namespace ECursor
+namespace Cursor
 {
-	enum ECursorTypes { ADVENTURE, COMBAT, DEFAULT, SPELLBOOK };
-
-	enum EBattleCursors { COMBAT_BLOCKED, COMBAT_MOVE, COMBAT_FLY, COMBAT_SHOOT,
-						COMBAT_HERO, COMBAT_QUERY, COMBAT_POINTER,
-						//various attack frames
-						COMBAT_SHOOT_PENALTY = 15, COMBAT_SHOOT_CATAPULT, COMBAT_HEAL,
-						COMBAT_SACRIFICE, COMBAT_TELEPORT};
+	enum class Type {
+		ADVENTURE, // set of various cursors for adventure map
+		COMBAT,    // set of various cursors for combat
+		DEFAULT,   // default arrow and hourglass cursors
+		SPELLBOOK  // animated cursor for spellcasting
+	};
+
+	enum class Default {
+		POINTER      = 0,
+		//ARROW_COPY = 1, // probably unused
+		HOURGLASS  = 2,
+	};
+
+	enum class Combat {
+		INVALID        = -1,
+
+		BLOCKED        = 0,
+		MOVE           = 1,
+		FLY            = 2,
+		SHOOT          = 3,
+		HERO           = 4,
+		QUERY          = 5,
+		POINTER        = 6,
+		HIT_NORTHEAST  = 7,
+		HIT_EAST       = 8,
+		HIT_SOUTHEAST  = 9,
+		HIT_SOUTHWEST  = 10,
+		HIT_WEST       = 11,
+		HIT_NORTHWEST  = 12,
+		HIT_NORTH      = 13,
+		HIT_SOUTH      = 14,
+		SHOOT_PENALTY  = 15,
+		SHOOT_CATAPULT = 16,
+		HEAL           = 17,
+		SACRIFICE      = 18,
+		TELEPORT       = 19
+	};
+
+	enum class Map {
+		POINTER          =  0,
+		HOURGLASS        =  1,
+		HERO             =  2,
+		TOWN             =  3,
+		T1_MOVE          =  4,
+		T1_ATTACK        =  5,
+		T1_SAIL          =  6,
+		T1_DISEMBARK     =  7,
+		T1_EXCHANGE      =  8,
+		T1_VISIT         =  9,
+		T2_MOVE          = 10,
+		T2_ATTACK        = 11,
+		T2_SAIL          = 12,
+		T2_DISEMBARK     = 13,
+		T2_EXCHANGE      = 14,
+		T2_VISIT         = 15,
+		T3_MOVE          = 16,
+		T3_ATTACK        = 17,
+		T3_SAIL          = 18,
+		T3_DISEMBARK     = 19,
+		T3_EXCHANGE      = 20,
+		T3_VISIT         = 21,
+		T4_MOVE          = 22,
+		T4_ATTACK        = 23,
+		T4_SAIL          = 24,
+		T4_DISEMBARK     = 25,
+		T4_EXCHANGE      = 26,
+		T4_VISIT         = 27,
+		T1_SAIL_VISIT    = 28,
+		T2_SAIL_VISIT    = 29,
+		T3_SAIL_VISIT    = 30,
+		T4_SAIL_VISIT    = 31,
+		SCROLL_NORTH     = 32,
+		SCROLL_NORTHEAST = 33,
+		SCROLL_EAST      = 34,
+		SCROLL_SOUTHEAST = 35,
+		SCROLL_SOUTH     = 36,
+		SCROLL_SOUTHWEST = 37,
+		SCROLL_WEST      = 38,
+		SCROLL_NORTHWEST = 39,
+		//POINTER_COPY       = 40, // probably unused
+		TELEPORT         = 41,
+		SCUTTLE_BOAT     = 42
+	};
+
+	enum class Spellcast {
+		SPELL = 0,
+	};
 }
 
 /// handles mouse cursor
@@ -45,19 +126,20 @@ class CCursorHandler final
 	void shiftPos( int &x, int &y );
 
 	void updateTexture();
-public:
-	/// position of cursor
-	int xpos, ypos;
 
 	/// Current cursor
-	ECursor::ECursorTypes type;
+	Cursor::Type type;
 	size_t frame;
+	float frameTime;
 
-	/// inits cursorHandler - run only once, it's not memleak-proof (rev 1333)
-	void initCursor();
+	void changeGraphic(Cursor::Type type, size_t index);
 
-	/// changes cursor graphic for type type (0 - adventure, 1 - combat, 2 - default, 3 - spellbook) and frame index (not used for type 3)
-	void changeGraphic(ECursor::ECursorTypes type, int index);
+	/// position of cursor
+	int xpos, ypos;
+
+public:
+	CCursorHandler();
+	~CCursorHandler();
 
 	/**
 	 * Replaces the cursor with a custom image.
@@ -67,6 +149,27 @@ public:
 	 */
 	void dragAndDropCursor (std::unique_ptr<CAnimImage> image);
 
+	/// Returns current position of the cursor
+	Point position() const;
+
+	/// Changes cursor to specified index
+	void set(Cursor::Default index);
+	void set(Cursor::Map index);
+	void set(Cursor::Combat index);
+	void set(Cursor::Spellcast index);
+
+	/// Returns current index of cursor
+	template<typename Index>
+	Index get()
+	{
+		assert((std::is_same<Index, Cursor::Default>::value   )|| type != Cursor::Type::DEFAULT );
+		assert((std::is_same<Index, Cursor::Map>::value       )|| type != Cursor::Type::ADVENTURE );
+		assert((std::is_same<Index, Cursor::Combat>::value    )|| type != Cursor::Type::COMBAT );
+		assert((std::is_same<Index, Cursor::Spellcast>::value )|| type != Cursor::Type::SPELLBOOK );
+
+		return static_cast<Index>(frame);
+	}
+
 	void render();
 
 	void hide() { showing=false; };
@@ -77,6 +180,4 @@ public:
 	/// Move cursor to screen center
 	void centerCursor();
 
-	CCursorHandler();
-	~CCursorHandler();
 };

+ 7 - 7
client/gui/CGuiHandler.cpp

@@ -21,7 +21,7 @@
 #include "../../lib/CConfigHandler.h"
 #include "../CMT.h"
 #include "../CPlayerInterface.h"
-#include "../battle/CBattleInterface.h"
+#include "../battle/BattleInterface.h"
 
 extern std::queue<SDL_Event> SDLEventsQueue;
 extern boost::mutex eventsM;
@@ -121,7 +121,7 @@ void CGuiHandler::pushInt(std::shared_ptr<IShowActivatable> newInt)
 	if(!listInt.empty())
 		listInt.front()->deactivate();
 	listInt.push_front(newInt);
-	CCS->curh->changeGraphic(ECursor::ADVENTURE, 0);
+	CCS->curh->set(Cursor::Map::POINTER);
 	newInt->activate();
 	objsToBlit.push_back(newInt);
 	totalRedraw();
@@ -238,7 +238,7 @@ void CGuiHandler::handleCurrentEvent()
 				break;
 
 			case SDLK_F9:
-				//not working yet since CClient::run remain locked after CBattleInterface removal
+				//not working yet since CClient::run remain locked after BattleInterface removal
 //				if(LOCPLINT->battleInt)
 //				{
 //					GH.popInts(1);
@@ -451,7 +451,7 @@ void CGuiHandler::renderFrame()
 
 	bool acquiredTheLockOnPim = false; //for tracking whether pim mutex locking succeeded
 	while(!terminate_cond->get() && !(acquiredTheLockOnPim = CPlayerInterface::pim->try_lock())) //try acquiring long until it succeeds or we are told to terminate
-		boost::this_thread::sleep(boost::posix_time::milliseconds(15));
+		boost::this_thread::sleep(boost::posix_time::milliseconds(1));
 
 	if(acquiredTheLockOnPim)
 	{
@@ -489,7 +489,7 @@ CGuiHandler::CGuiHandler()
 	statusbar = nullptr;
 
 	// Creates the FPS manager and sets the framerate to 48 which is doubled the value of the original Heroes 3 FPS rate
-	mainFPSmng = new CFramerateManager(48);
+	mainFPSmng = new CFramerateManager(60);
 	//do not init CFramerateManager here --AVS
 
 	terminate_cond = new CondSh<bool>(false);
@@ -623,8 +623,8 @@ void CFramerateManager::framerateDelay()
 
 	currentTicks = SDL_GetTicks();
 	// recalculate timeElapsed for external calls via getElapsed()
-	// limit it to 1000 ms to avoid breaking animation in case of huge lag (e.g. triggered breakpoint)
-	timeElapsed = std::min<ui32>(currentTicks - lastticks, 1000);
+	// limit it to 100 ms to avoid breaking animation in case of huge lag (e.g. triggered breakpoint)
+	timeElapsed = std::min<ui32>(currentTicks - lastticks, 100);
 
 	lastticks = SDL_GetTicks();
 

+ 3 - 2
client/gui/CGuiHandler.h

@@ -20,7 +20,7 @@ template <typename T> struct CondSh;
 VCMI_LIB_NAMESPACE_END
 
 class CFramerateManager;
-class CGStatusBar;
+class IStatusBar;
 class CIntObject;
 class IUpdateable;
 class IShowActivatable;
@@ -65,7 +65,7 @@ class CGuiHandler
 public:
 	CFramerateManager * mainFPSmng; //to keep const framerate
 	std::list<std::shared_ptr<IShowActivatable>> listInt; //list of interfaces - front=foreground; back = background (includes adventure map, window interfaces, all kind of active dialogs, and so on)
-	std::shared_ptr<CGStatusBar> statusbar;
+	std::shared_ptr<IStatusBar> statusbar;
 
 private:
 	std::vector<std::shared_ptr<IShowActivatable>> disposed;
@@ -165,6 +165,7 @@ struct SSetCaptureState
 };
 
 #define OBJ_CONSTRUCTION SObjectConstruction obj__i(this)
+#define OBJ_CONSTRUCTION_TARGETED(obj) SObjectConstruction obj__i(obj)
 #define OBJECT_CONSTRUCTION_CAPTURING(actions) defActions = actions; SSetCaptureState obj__i1(true, actions); SObjectConstruction obj__i(this)
 #define OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(actions) SSetCaptureState obj__i1(true, actions); SObjectConstruction obj__i(this)
 

+ 5 - 2
client/gui/CIntObject.cpp

@@ -267,7 +267,7 @@ void CIntObject::addChild(CIntObject * child, bool adjustPosition)
 	children.push_back(child);
 	child->parent_m = this;
 	if(adjustPosition)
-		child->pos += pos;
+		child->pos += pos.topLeft();
 
 	if (!active && child->active)
 		child->deactivate();
@@ -289,7 +289,7 @@ void CIntObject::removeChild(CIntObject * child, bool adjustPosition)
 	children -= child;
 	child->parent_m = nullptr;
 	if(adjustPosition)
-		child->pos -= pos;
+		child->pos -= pos.topLeft();
 }
 
 void CIntObject::redraw()
@@ -373,3 +373,6 @@ void WindowBase::close()
 		logGlobal->error("Only top interface must be closed");
 	GH.popInts(1);
 }
+
+IStatusBar::~IStatusBar()
+{}

+ 22 - 2
client/gui/CIntObject.h

@@ -165,8 +165,6 @@ public:
 	//request complete redraw of this object
 	void redraw() override;
 
-	enum EAlignment {TOPLEFT, CENTER, BOTTOMRIGHT};
-
 	bool isItInLoc(const SDL_Rect &rect, int x, int y);
 	bool isItInLoc(const SDL_Rect &rect, const Point &p);
 	const Rect & center(const Rect &r, bool propagate = true); //sets pos so that r will be in the center of screen, assigns sizes of r to pos, returns new position
@@ -217,3 +215,25 @@ public:
 protected:
 	void close();
 };
+
+class IStatusBar
+{
+public:
+	virtual ~IStatusBar();
+
+	/// set current text for the status bar
+	virtual void write(const std::string & text) = 0;
+
+	/// remove any current text from the status bar
+	virtual void clear() = 0;
+
+	/// remove text from status bar if current text matches tested text
+	virtual void clearIfMatching(const std::string & testedText) = 0;
+
+	/// enables mode for entering text instead of showing hover text
+	virtual void setEnteringMode(bool on) = 0;
+
+	/// overrides hover text from controls with text entered into in-game console (for chat/cheats)
+	virtual void setEnteredText(const std::string & text) = 0;
+
+};

+ 102 - 0
client/gui/Canvas.cpp

@@ -0,0 +1,102 @@
+/*
+ * Canvas.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 "Canvas.h"
+
+#include "SDL_Extensions.h"
+#include "Geometries.h"
+#include "CAnimation.h"
+
+#include "../Graphics.h"
+
+Canvas::Canvas(SDL_Surface * surface):
+	surface(surface),
+	renderOffset(0,0)
+{
+	surface->refcount++;
+}
+
+Canvas::Canvas(Canvas & other):
+	surface(other.surface),
+	renderOffset(other.renderOffset)
+{
+	surface->refcount++;
+}
+
+Canvas::Canvas(Canvas & other, const Rect & newClipRect):
+	Canvas(other)
+{
+	clipRect.emplace();
+	SDL_GetClipRect(surface, clipRect.get_ptr());
+
+	Rect currClipRect = newClipRect + renderOffset;
+	SDL_SetClipRect(surface, &currClipRect);
+
+	renderOffset += newClipRect.topLeft();
+}
+
+Canvas::Canvas(const Point & size):
+	renderOffset(0,0)
+{
+	surface = CSDL_Ext::newSurface(size.x, size.y);
+}
+
+Canvas::~Canvas()
+{
+	if (clipRect)
+		SDL_SetClipRect(surface, clipRect.get_ptr());
+
+	SDL_FreeSurface(surface);
+}
+
+void Canvas::draw(std::shared_ptr<IImage> image, const Point & pos)
+{
+	assert(image);
+	if (image)
+		image->draw(surface, renderOffset.x + pos.x, renderOffset.y + pos.y);
+}
+
+void Canvas::draw(std::shared_ptr<IImage> image, const Point & pos, const Rect & sourceRect)
+{
+	assert(image);
+	if (image)
+		image->draw(surface, renderOffset.x + pos.x, renderOffset.y + pos.y, &sourceRect);
+}
+
+void Canvas::draw(Canvas & image, const Point & pos)
+{
+	blitAt(image.surface, renderOffset.x + pos.x, renderOffset.y + pos.y, surface);
+}
+
+void Canvas::drawLine(const Point & from, const Point & dest, const SDL_Color & colorFrom, const SDL_Color & colorDest)
+{
+	CSDL_Ext::drawLine(surface, renderOffset.x + from.x, renderOffset.y + from.y, renderOffset.x + dest.x, renderOffset.y + dest.y, colorFrom, colorDest);
+}
+
+void Canvas::drawText(const Point & position, const EFonts & font, const SDL_Color & colorDest, ETextAlignment alignment, const std::string & text )
+{
+	switch (alignment)
+	{
+	case ETextAlignment::TOPLEFT:      return graphics->fonts[font]->renderTextLeft  (surface, text, colorDest, renderOffset + position);
+	case ETextAlignment::CENTER:       return graphics->fonts[font]->renderTextCenter(surface, text, colorDest, renderOffset + position);
+	case ETextAlignment::BOTTOMRIGHT:  return graphics->fonts[font]->renderTextRight (surface, text, colorDest, renderOffset + position);
+	}
+}
+
+void Canvas::drawText(const Point & position, const EFonts & font, const SDL_Color & colorDest, ETextAlignment alignment, const std::vector<std::string> & text )
+{
+	switch (alignment)
+	{
+	case ETextAlignment::TOPLEFT:      return graphics->fonts[font]->renderTextLinesLeft  (surface, text, colorDest, renderOffset + position);
+	case ETextAlignment::CENTER:       return graphics->fonts[font]->renderTextLinesCenter(surface, text, colorDest, renderOffset + position);
+	case ETextAlignment::BOTTOMRIGHT:  return graphics->fonts[font]->renderTextLinesRight (surface, text, colorDest, renderOffset + position);
+	}
+}
+

+ 65 - 0
client/gui/Canvas.h

@@ -0,0 +1,65 @@
+/*
+ * Canvas.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 "Geometries.h"
+
+struct SDL_Color;
+struct SDL_Surface;
+class IImage;
+enum EFonts : int;
+
+/// Class that represents surface for drawing on
+class Canvas
+{
+	/// Target surface
+	SDL_Surface * surface;
+
+	/// Clip rect that was in use on surface originally and needs to be restored on destruction
+	boost::optional<Rect> clipRect;
+
+	/// Current rendering area offset, all rendering operations will be moved into selected area
+	Point renderOffset;
+
+	Canvas & operator = (Canvas & other) = delete;
+public:
+
+	/// constructs canvas using existing surface. Caller maintains ownership on the surface
+	Canvas(SDL_Surface * surface);
+
+	/// copy contructor
+	Canvas(Canvas & other);
+
+	/// creates canvas that only covers specified subsection of a surface
+	Canvas(Canvas & other, const Rect & clipRect);
+
+	/// constructs canvas of specified size
+	Canvas(const Point & size);
+
+	~Canvas();
+
+	/// renders image onto this canvas at specified position
+	void draw(std::shared_ptr<IImage> image, const Point & pos);
+
+	/// renders section of image bounded by sourceRect at specified position
+	void draw(std::shared_ptr<IImage> image, const Point & pos, const Rect & sourceRect);
+
+	/// renders another canvas onto this canvas
+	void draw(Canvas & image, const Point & pos);
+
+	/// renders continuous, 1-pixel wide line with color gradient
+	void drawLine(const Point & from, const Point & dest, const SDL_Color & colorFrom, const SDL_Color & colorDest);
+
+	/// renders single line of text with specified parameters
+	void drawText(const Point & position, const EFonts & font, const SDL_Color & colorDest, ETextAlignment alignment, const std::string & text );
+
+	/// renders multiple lines of text with specified parameters
+	void drawText(const Point & position, const EFonts & font, const SDL_Color & colorDest, ETextAlignment alignment, const std::vector<std::string> & text );
+};

+ 162 - 0
client/gui/ColorFilter.cpp

@@ -0,0 +1,162 @@
+/*
+ * Canvas.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 "ColorFilter.h"
+
+#include <SDL2/SDL_pixels.h>
+
+#include "../../lib/JsonNode.h"
+
+SDL_Color ColorFilter::shiftColor(const SDL_Color & in) const
+{
+	int r_out = in.r * r.r + in.g * r.g + in.b * r.b + 255 * r.a;
+	int g_out = in.r * g.r + in.g * g.g + in.b * g.b + 255 * g.a;
+	int b_out = in.r * b.r + in.g * b.g + in.b * b.b + 255 * b.a;
+	int a_out = in.a * a;
+
+	vstd::abetween(r_out, 0, 255);
+	vstd::abetween(g_out, 0, 255);
+	vstd::abetween(b_out, 0, 255);
+	vstd::abetween(a_out, 0, 255);
+
+	return {
+		static_cast<uint8_t>(r_out),
+		static_cast<uint8_t>(g_out),
+		static_cast<uint8_t>(b_out),
+		static_cast<uint8_t>(a_out)
+	};
+}
+
+bool ColorFilter::operator != (const ColorFilter & other) const
+{
+	return !(this->operator==(other));
+}
+
+bool ColorFilter::operator == (const ColorFilter & other) const
+{
+	return
+		r.r == other.r.r && r.g && other.r.g && r.b == other.r.b && r.a == other.r.a &&
+		g.r == other.g.r && g.g && other.g.g && g.b == other.g.b && g.a == other.g.a &&
+		b.r == other.b.r && b.g && other.b.g && b.b == other.b.b && b.a == other.b.a &&
+		a == other.a;
+}
+
+ColorFilter ColorFilter::genEmptyShifter( )
+{
+	return genAlphaShifter( 1.f);
+}
+
+ColorFilter ColorFilter::genAlphaShifter( float alpha )
+{
+	return genMuxerShifter(
+				{ 1.f, 0.f, 0.f, 0.f },
+				{ 0.f, 1.f, 0.f, 0.f },
+				{ 0.f, 0.f, 1.f, 0.f },
+				alpha);
+}
+
+ColorFilter ColorFilter::genRangeShifter( float minR, float minG, float minB, float maxR, float maxG, float maxB )
+{
+	return genMuxerShifter(
+				{ maxR - minR, 0.f, 0.f, minR },
+				{ 0.f, maxG - minG, 0.f, minG },
+				{ 0.f, 0.f, maxB - minB, minB },
+				  1.f);
+}
+
+ColorFilter ColorFilter::genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a )
+{
+	return ColorFilter(r, g, b, a);
+}
+
+ColorFilter ColorFilter::genInterpolated(const ColorFilter & left, const ColorFilter & right, float power)
+{
+	auto lerpMuxer = [=]( const ChannelMuxer & left, const ChannelMuxer & right ) -> ChannelMuxer
+	{
+		return {
+			vstd::lerp(left.r, right.r, power),
+			vstd::lerp(left.g, right.g, power),
+			vstd::lerp(left.b, right.b, power),
+			vstd::lerp(left.a, right.a, power)
+		};
+	};
+
+	return genMuxerShifter(
+		lerpMuxer(left.r, right.r),
+		lerpMuxer(left.g, right.g),
+		lerpMuxer(left.b, right.b),
+		vstd::lerp(left.a, right.a, power)
+	);
+}
+
+ColorFilter ColorFilter::genCombined(const ColorFilter & left, const ColorFilter & right)
+{
+	// matrix multiplication
+	ChannelMuxer r{
+		left.r.r * right.r.r + left.g.r * right.r.g + left.b.r * right.r.b,
+		left.r.g * right.r.r + left.g.g * right.r.g + left.b.g * right.r.b,
+		left.r.b * right.r.r + left.g.b * right.r.g + left.b.b * right.r.b,
+		left.r.a * right.r.r + left.g.a * right.r.g + left.b.a * right.r.b + 1.f * right.r.a,
+	};
+
+	ChannelMuxer g{
+		left.r.r * right.g.r + left.g.r * right.g.g + left.b.r * right.g.b,
+		left.r.g * right.g.r + left.g.g * right.g.g + left.b.g * right.g.b,
+		left.r.b * right.g.r + left.g.b * right.g.g + left.b.b * right.g.b,
+		left.r.a * right.g.r + left.g.a * right.g.g + left.b.a * right.g.b + 1.f * right.g.a,
+	};
+
+	ChannelMuxer b{
+		left.r.r * right.b.r + left.g.r * right.b.g + left.b.r * right.b.b,
+		left.r.g * right.b.r + left.g.g * right.b.g + left.b.g * right.b.b,
+		left.r.b * right.b.r + left.g.b * right.b.g + left.b.b * right.b.b,
+		left.r.a * right.b.r + left.g.a * right.b.g + left.b.a * right.b.b + 1.f * right.b.a,
+	};
+
+	float a = left.a * right.a;
+	return genMuxerShifter(r,g,b,a);
+}
+
+ColorFilter ColorFilter::genFromJson(const JsonNode & entry)
+{
+	ChannelMuxer r{ 1.f, 0.f, 0.f, 0.f };
+	ChannelMuxer g{ 0.f, 1.f, 0.f, 0.f };
+	ChannelMuxer b{ 0.f, 0.f, 1.f, 0.f };
+	float a{ 1.0};
+
+	if (!entry["red"].isNull())
+	{
+		r.r = entry["red"].Vector()[0].Float();
+		r.g = entry["red"].Vector()[1].Float();
+		r.b = entry["red"].Vector()[2].Float();
+		r.a = entry["red"].Vector()[3].Float();
+	}
+
+	if (!entry["red"].isNull())
+	{
+		g.r = entry["green"].Vector()[0].Float();
+		g.g = entry["green"].Vector()[1].Float();
+		g.b = entry["green"].Vector()[2].Float();
+		g.a = entry["green"].Vector()[3].Float();
+	}
+
+	if (!entry["red"].isNull())
+	{
+		b.r = entry["blue"].Vector()[0].Float();
+		b.g = entry["blue"].Vector()[1].Float();
+		b.b = entry["blue"].Vector()[2].Float();
+		b.a = entry["blue"].Vector()[3].Float();
+	}
+
+	if (!entry["alpha"].isNull())
+		a = entry["alpha"].Float();
+
+	return genMuxerShifter(r,g,b,a);
+}

+ 66 - 0
client/gui/ColorFilter.h

@@ -0,0 +1,66 @@
+/*
+ * ColorFilter.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
+
+struct SDL_Color;
+
+VCMI_LIB_NAMESPACE_BEGIN
+class JsonNode;
+VCMI_LIB_NAMESPACE_END
+
+/// Base class for applying palette transformation on images
+class ColorFilter
+{
+	struct ChannelMuxer {
+		float r, g, b, a;
+	};
+
+	ChannelMuxer r;
+	ChannelMuxer g;
+	ChannelMuxer b;
+	float a;
+
+	ColorFilter(ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a):
+		r(r), g(g), b(b), a(a)
+	{}
+public:
+	SDL_Color shiftColor(const SDL_Color & in) const;
+
+	bool operator == (const ColorFilter & other) const;
+	bool operator != (const ColorFilter & other) const;
+
+	/// Generates empty object that has no effect on image
+	static ColorFilter genEmptyShifter();
+
+	/// Generates object that changes alpha (transparency) of the image
+	static ColorFilter genAlphaShifter( float alpha );
+
+	/// Generates object that transforms each channel independently
+	static ColorFilter genRangeShifter( float minR, float minG, float minB, float maxR, float maxG, float maxB );
+
+	/// Generates object that performs arbitrary mixing between any channels
+	static ColorFilter genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a );
+
+	/// Combines 2 mixers into a single object
+	static ColorFilter genCombined(const ColorFilter & left, const ColorFilter & right);
+
+	/// Scales down strength of a shifter to a specified factor
+	static ColorFilter genInterpolated(const ColorFilter & left, const ColorFilter & right, float power);
+
+	/// Generates object using supplied Json config
+	static ColorFilter genFromJson(const JsonNode & entry);
+};
+
+struct ColorMuxerEffect
+{
+	std::vector<ColorFilter> filters;
+	std::vector<float> timePoints;
+};

+ 7 - 1
client/gui/Geometries.cpp

@@ -11,6 +11,11 @@
 #include "Geometries.h"
 #include "../CMT.h"
 #include <SDL_events.h>
+#include "../../lib/int3.h"
+
+Point::Point(const int3 &a)
+	:x(a.x),y(a.y)
+{}
 
 Point::Point(const SDL_MouseMotionEvent &a)
 	:x(a.x),y(a.y)
@@ -21,7 +26,7 @@ Rect Rect::createCentered( int w, int h )
 	return Rect(screen->w/2 - w/2, screen->h/2 - h/2, w, h);
 }
 
-Rect Rect::around(const Rect &r, int width) /*creates rect around another */
+Rect Rect::around(const Rect &r, int width)
 {
 	return Rect(r.x - width, r.y - width, r.w + width * 2, r.h + width * 2);
 }
@@ -30,3 +35,4 @@ Rect Rect::centerIn(const Rect &r)
 {
 	return Rect(r.x + (r.w - w) / 2, r.y + (r.h - h) / 2, w, h);
 }
+

+ 42 - 53
client/gui/Geometries.h

@@ -9,11 +9,16 @@
  */
 #pragma once
 
-#include <SDL_video.h>
-#include "../../lib/int3.h"
+#include <SDL2/SDL_rect.h>
+
+enum class ETextAlignment {TOPLEFT, CENTER, BOTTOMRIGHT};
 
 struct SDL_MouseMotionEvent;
 
+VCMI_LIB_NAMESPACE_BEGIN
+class int3;
+VCMI_LIB_NAMESPACE_END
+
 // A point with x/y coordinate, used mostly for graphic rendering
 struct Point
 {
@@ -24,13 +29,14 @@ struct Point
 	{
 		x = y = 0;
 	};
+
 	Point(int X, int Y)
 		:x(X),y(Y)
 	{};
-	Point(const int3 &a)
-		:x(a.x),y(a.y)
-	{}
-	Point(const SDL_MouseMotionEvent &a);
+
+	Point(const int3 &a);
+
+	explicit Point(const SDL_MouseMotionEvent &a);
 
 	template<typename T>
 	Point operator+(const T &b) const
@@ -71,10 +77,7 @@ struct Point
 		y -= b.y;
 		return *this;
 	}
-	bool operator<(const Point &b) const //product order
-	{
-		return x < b.x   &&   y < b.y;
-	}
+
 	template<typename T> Point& operator=(const T &t)
 	{
 		x = t.x;
@@ -94,7 +97,7 @@ struct Point
 /// Rectangle class, which have a position and a size
 struct Rect : public SDL_Rect
 {
-	Rect()//default c-tor
+	Rect()
 	{
 		x = y = w = h = -1;
 	}
@@ -119,60 +122,59 @@ struct Rect : public SDL_Rect
 		w = r.w;
 		h = r.h;
 	}
-	Rect(const Rect& r) : Rect(static_cast<const SDL_Rect&>(r))
-	{}
-	explicit Rect(const SDL_Surface * const &surf)
-	{
-		x = y = 0;
-		w = surf->w;
-		h = surf->h;
-	}
+	Rect(const Rect& r) = default;
 
 	Rect centerIn(const Rect &r);
 	static Rect createCentered(int w, int h);
-	static Rect around(const Rect &r, int width = 1); //creates rect around another
+	static Rect around(const Rect &r, int width = 1);
 
-	bool isIn(int qx, int qy) const //determines if given point lies inside rect
+	bool isIn(int qx, int qy) const
 	{
 		if (qx > x   &&   qx<x+w   &&   qy>y   &&   qy<y+h)
 			return true;
 		return false;
 	}
-	bool isIn(const Point & q) const //determines if given point lies inside rect
+	bool isIn(const Point & q) const
 	{
 		return isIn(q.x,q.y);
 	}
-	Point topLeft() const //top left corner of this rect
+	Point topLeft() const
 	{
 		return Point(x,y);
 	}
-	Point topRight() const //top right corner of this rect
+	Point topRight() const
 	{
 		return Point(x+w,y);
 	}
-	Point bottomLeft() const //bottom left corner of this rect
+	Point bottomLeft() const
 	{
 		return Point(x,y+h);
 	}
-	Point bottomRight() const //bottom right corner of this rect
+	Point bottomRight() const
 	{
 		return Point(x+w,y+h);
 	}
-	Rect operator+(const Rect &p) const //moves this rect by p's rect position
+	Point center() const
 	{
-		return Rect(x+p.x,y+p.y,w,h);
+		return Point(x+w/2,y+h/2);
 	}
-	Rect operator+(const Point &p) const //moves this rect by p's point position
+	Point dimensions() const
 	{
-		return Rect(x+p.x,y+p.y,w,h);
+		return Point(w,h);
 	}
-	Rect& operator=(const Point &p) //assignment operator
+
+	void moveTo(const Point & dest)
 	{
-		x = p.x;
-		y = p.y;
-		return *this;
+		x = dest.x;
+		y = dest.y;
 	}
-	Rect& operator=(const Rect &p) //assignment operator
+
+	Rect operator+(const Point &p) const
+	{
+		return Rect(x+p.x,y+p.y,w,h);
+	}
+
+	Rect& operator=(const Rect &p)
 	{
 		x = p.x;
 		y = p.y;
@@ -180,34 +182,21 @@ struct Rect : public SDL_Rect
 		h = p.h;
 		return *this;
 	}
-	Rect& operator+=(const Rect &p) //works as operator+
-	{
-		x += p.x;
-		y += p.y;
-		return *this;
-	}
-	Rect& operator+=(const Point &p) //works as operator+
+
+	Rect& operator+=(const Point &p)
 	{
 		x += p.x;
 		y += p.y;
 		return *this;
 	}
-	Rect& operator-=(const Rect &p) //works as operator+
-	{
-		x -= p.x;
-		y -= p.y;
-		return *this;
-	}
-	Rect& operator-=(const Point &p) //works as operator+
+
+	Rect& operator-=(const Point &p)
 	{
 		x -= p.x;
 		y -= p.y;
 		return *this;
 	}
-	template<typename T> Rect operator-(const T &t)
-	{
-		return Rect(x - t.x, y - t.y, w, h);
-	}
+
 	Rect operator&(const Rect &p) const //rect intersection
 	{
 		bool intersect = true;

+ 425 - 0
client/gui/InterfaceObjectConfigurable.cpp

@@ -0,0 +1,425 @@
+/*
+* InterfaceBuilder.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 "InterfaceObjectConfigurable.h"
+
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+#include "../gui/CAnimation.h"
+#include "../gui/CGuiHandler.h"
+#include "../widgets/CComponent.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/MiscWidgets.h"
+#include "../widgets/ObjectLists.h"
+#include "../widgets/TextControls.h"
+#include "../windows/GUIClasses.h"
+#include "../windows/InfoWindows.h"
+
+#include "../../lib/CGeneralTextHandler.h"
+
+static std::map<std::string, int> KeycodeMap{
+	{"up", SDLK_UP},
+	{"down", SDLK_DOWN},
+	{"left", SDLK_LEFT},
+	{"right", SDLK_RIGHT},
+	{"space", SDLK_SPACE},
+	{"enter", SDLK_RETURN}
+};
+
+
+InterfaceObjectConfigurable::InterfaceObjectConfigurable(const JsonNode & config, int used, Point offset):
+	InterfaceObjectConfigurable(used, offset)
+{
+	build(config);
+}
+
+InterfaceObjectConfigurable::InterfaceObjectConfigurable(int used, Point offset):
+	CIntObject(used, offset)
+{
+	REGISTER_BUILDER("picture", &InterfaceObjectConfigurable::buildPicture);
+	REGISTER_BUILDER("image", &InterfaceObjectConfigurable::buildImage);
+	REGISTER_BUILDER("texture", &InterfaceObjectConfigurable::buildTexture);
+	REGISTER_BUILDER("animation", &InterfaceObjectConfigurable::buildAnimation);
+	REGISTER_BUILDER("label", &InterfaceObjectConfigurable::buildLabel);
+	REGISTER_BUILDER("toggleGroup", &InterfaceObjectConfigurable::buildToggleGroup);
+	REGISTER_BUILDER("toggleButton", &InterfaceObjectConfigurable::buildToggleButton);
+	REGISTER_BUILDER("button", &InterfaceObjectConfigurable::buildButton);
+	REGISTER_BUILDER("labelGroup", &InterfaceObjectConfigurable::buildLabelGroup);
+	REGISTER_BUILDER("slider", &InterfaceObjectConfigurable::buildSlider);
+}
+
+void InterfaceObjectConfigurable::registerBuilder(const std::string & type, BuilderFunction f)
+{
+	builders[type] = f;
+}
+
+void InterfaceObjectConfigurable::addCallback(const std::string & callbackName, std::function<void(int)> callback)
+{
+	callbacks[callbackName] = callback;
+}
+
+void InterfaceObjectConfigurable::deleteWidget(const std::string & name)
+{
+	auto iter = widgets.find(name);
+	if(iter != widgets.end())
+		widgets.erase(iter);
+}
+
+void InterfaceObjectConfigurable::build(const JsonNode &config)
+{
+	OBJ_CONSTRUCTION;
+	logGlobal->debug("Building configurable interface object");
+	auto * items = &config;
+	
+	if(config.getType() == JsonNode::JsonType::DATA_STRUCT)
+	{
+		for(auto & item : config["variables"].Struct())
+		{
+			logGlobal->debug("Read variable named %s", item.first);
+			variables[item.first] = item.second;
+		}
+		
+		items = &config["items"];
+	}
+	
+	const std::string unnamedObjectPrefix = "__widget_";
+	for(const auto & item : items->Vector())
+	{
+		std::string name = item["name"].isNull()
+						? unnamedObjectPrefix + std::to_string(unnamedObjectId++)
+						: item["name"].String();
+		logGlobal->debug("Building widget with name %s", name);
+		widgets[name] = buildWidget(item);
+	}
+}
+
+std::string InterfaceObjectConfigurable::readText(const JsonNode & config) const
+{
+	if(config.isNull())
+		return "";
+	
+	std::string s = config.String();
+	logGlobal->debug("Reading text from translations by key: %s", s);
+	return CGI->generaltexth->translate(s);
+}
+
+Point InterfaceObjectConfigurable::readPosition(const JsonNode & config) const
+{
+	Point p;
+	logGlobal->debug("Reading point");
+	p.x = config["x"].Integer();
+	p.y = config["y"].Integer();
+	return p;
+}
+
+Rect InterfaceObjectConfigurable::readRect(const JsonNode & config) const
+{
+	Rect p;
+	logGlobal->debug("Reading rect");
+	p.x = config["x"].Integer();
+	p.y = config["y"].Integer();
+	p.w = config["w"].Integer();
+	p.h = config["h"].Integer();
+	return p;
+}
+
+ETextAlignment InterfaceObjectConfigurable::readTextAlignment(const JsonNode & config) const
+{
+	logGlobal->debug("Reading text alignment");
+	if(!config.isNull())
+	{
+		if(config.String() == "center")
+			return ETextAlignment::CENTER;
+		if(config.String() == "left")
+			return ETextAlignment::TOPLEFT;
+		if(config.String() == "right")
+			return ETextAlignment::BOTTOMRIGHT;
+	}
+	logGlobal->debug("Uknown text alignment attribute");
+	return ETextAlignment::CENTER;
+}
+
+SDL_Color InterfaceObjectConfigurable::readColor(const JsonNode & config) const
+{
+	logGlobal->debug("Reading color");
+	if(!config.isNull())
+	{
+		if(config.String() == "yellow")
+			return Colors::YELLOW;
+		if(config.String() == "white")
+			return Colors::WHITE;
+		if(config.String() == "gold")
+			return Colors::METALLIC_GOLD;
+		if(config.String() == "green")
+			return Colors::GREEN;
+		if(config.String() == "orange")
+			return Colors::ORANGE;
+		if(config.String() == "bright-yellow")
+			return Colors::BRIGHT_YELLOW;
+	}
+	logGlobal->debug("Uknown color attribute");
+	return Colors::DEFAULT_KEY_COLOR;
+	
+}
+EFonts InterfaceObjectConfigurable::readFont(const JsonNode & config) const
+{
+	logGlobal->debug("Reading font");
+	if(!config.isNull())
+	{
+		if(config.String() == "big")
+			return EFonts::FONT_BIG;
+		if(config.String() == "medium")
+			return EFonts::FONT_MEDIUM;
+		if(config.String() == "small")
+			return EFonts::FONT_SMALL;
+		if(config.String() == "tiny")
+			return EFonts::FONT_TINY;
+	}
+	logGlobal->debug("Uknown font attribute");
+	return EFonts::FONT_TIMES;
+}
+
+std::pair<std::string, std::string> InterfaceObjectConfigurable::readHintText(const JsonNode & config) const
+{
+	logGlobal->debug("Reading hint text");
+	std::pair<std::string, std::string> result;
+	if(!config.isNull())
+	{
+		if(config.getType() == JsonNode::JsonType::DATA_STRUCT)
+		{
+			result.first = readText(config["hover"]);
+			result.second = readText(config["help"]);
+			return result;
+		}
+		if(config.getType() == JsonNode::JsonType::DATA_STRING)
+		{
+			logGlobal->debug("Reading hint text (help) from generaltext handler:%sd", config.String());
+			result.first  = CGI->generaltexth->translate( config.String(), "hover");
+			result.second = CGI->generaltexth->translate( config.String(), "help");
+		}
+	}
+	return result;
+}
+
+int InterfaceObjectConfigurable::readKeycode(const JsonNode & config) const
+{
+	logGlobal->debug("Reading keycode");
+	if(config.getType() == JsonNode::JsonType::DATA_INTEGER)
+		return config.Integer();
+	
+	if(config.getType() == JsonNode::JsonType::DATA_STRING)
+	{
+		auto s = config.String();
+		if(s.size() == 1) //keyboard symbol
+			return s[0];
+		return KeycodeMap[s];
+	}
+	
+	return 0;
+}
+
+std::shared_ptr<CPicture> InterfaceObjectConfigurable::buildPicture(const JsonNode & config) const
+{
+	logGlobal->debug("Building widget CPicture");
+	auto image = config["image"].String();
+	auto position = readPosition(config["position"]);
+	auto pic = std::make_shared<CPicture>(image, position.x, position.y);
+	if(!config["visible"].isNull())
+		pic->visible = config["visible"].Bool();
+
+	if ( config["playerColored"].Bool() && LOCPLINT)
+		pic->colorize(LOCPLINT->playerID);
+	return pic;
+}
+
+std::shared_ptr<CLabel> InterfaceObjectConfigurable::buildLabel(const JsonNode & config) const
+{
+	logGlobal->debug("Building widget CLabel");
+	auto font = readFont(config["font"]);
+	auto alignment = readTextAlignment(config["alignment"]);
+	auto color = readColor(config["color"]);
+	auto text = readText(config["text"]);
+	auto position = readPosition(config["position"]);
+	return std::make_shared<CLabel>(position.x, position.y, font, alignment, color, text);
+}
+
+std::shared_ptr<CToggleGroup> InterfaceObjectConfigurable::buildToggleGroup(const JsonNode & config) const
+{
+	logGlobal->debug("Building widget CToggleGroup");
+	auto position = readPosition(config["position"]);
+	auto group = std::make_shared<CToggleGroup>(0);
+	group->pos += position;
+	if(!config["items"].isNull())
+	{
+		OBJ_CONSTRUCTION_TARGETED(group.get());
+		int itemIdx = -1;
+		for(const auto & item : config["items"].Vector())
+		{
+			itemIdx = item["index"].isNull() ? itemIdx + 1 : item["index"].Integer();
+			group->addToggle(itemIdx, std::dynamic_pointer_cast<CToggleBase>(buildWidget(item)));
+		}
+	}
+	if(!config["selected"].isNull())
+		group->setSelected(config["selected"].Integer());
+	if(!config["callback"].isNull())
+		group->addCallback(callbacks.at(config["callback"].String()));
+	return group;
+}
+
+std::shared_ptr<CToggleButton> InterfaceObjectConfigurable::buildToggleButton(const JsonNode & config) const
+{
+	logGlobal->debug("Building widget CToggleButton");
+	auto position = readPosition(config["position"]);
+	auto image = config["image"].String();
+	auto help = readHintText(config["help"]);
+	auto button = std::make_shared<CToggleButton>(position, image, help);
+	if(!config["selected"].isNull())
+		button->setSelected(config["selected"].Bool());
+	if(!config["imageOrder"].isNull())
+	{
+		auto imgOrder = config["imageOrder"].Vector();
+		assert(imgOrder.size() >= 4);
+		button->setImageOrder(imgOrder[0].Integer(), imgOrder[1].Integer(), imgOrder[2].Integer(), imgOrder[3].Integer());
+	}
+	if(!config["callback"].isNull())
+		button->addCallback(callbacks.at(config["callback"].String()));
+	return button;
+}
+
+std::shared_ptr<CButton> InterfaceObjectConfigurable::buildButton(const JsonNode & config) const
+{
+	logGlobal->debug("Building widget CButton");
+	auto position = readPosition(config["position"]);
+	auto image = config["image"].String();
+	auto help = readHintText(config["help"]);
+	auto button = std::make_shared<CButton>(position, image, help);
+	if(!config["items"].isNull())
+	{
+		for(const auto & item : config["items"].Vector())
+		{
+			button->addOverlay(buildWidget(item));
+		}
+	}
+	if(!config["imageOrder"].isNull())
+	{
+		auto imgOrder = config["imageOrder"].Vector();
+		assert(imgOrder.size() >= 4);
+		button->setImageOrder(imgOrder[0].Integer(), imgOrder[1].Integer(), imgOrder[2].Integer(), imgOrder[3].Integer());
+	}
+	if(!config["callback"].isNull())
+		button->addCallback(std::bind(callbacks.at(config["callback"].String()), 0));
+	if(!config["hotkey"].isNull())
+	{
+		if(config["hotkey"].getType() == JsonNode::JsonType::DATA_VECTOR)
+		{
+			for(auto k : config["hotkey"].Vector())
+				button->assignedKeys.insert(readKeycode(k));
+		}
+		else
+			button->assignedKeys.insert(readKeycode(config["hotkey"]));
+	}
+	return button;
+}
+
+std::shared_ptr<CLabelGroup> InterfaceObjectConfigurable::buildLabelGroup(const JsonNode & config) const
+{
+	logGlobal->debug("Building widget CLabelGroup");
+	auto font = readFont(config["font"]);
+	auto alignment = readTextAlignment(config["alignment"]);
+	auto color = readColor(config["color"]);
+	auto group = std::make_shared<CLabelGroup>(font, alignment, color);
+	if(!config["items"].isNull())
+	{
+		for(const auto & item : config["items"].Vector())
+		{
+			auto position = readPosition(item["position"]);
+			auto text = readText(item["text"]);
+			group->add(position.x, position.y, text);
+		}
+	}
+	return group;
+}
+
+std::shared_ptr<CSlider> InterfaceObjectConfigurable::buildSlider(const JsonNode & config) const
+{
+	logGlobal->debug("Building widget CSlider");
+	auto position = readPosition(config["position"]);
+	int length = config["size"].Integer();
+	auto style = config["style"].String() == "brown" ? CSlider::BROWN : CSlider::BLUE;
+	auto itemsVisible = config["itemsVisible"].Integer();
+	auto itemsTotal = config["itemsTotal"].Integer();
+	auto value = config["selected"].Integer();
+	bool horizontal = config["orientation"].String() == "horizontal";
+	return std::make_shared<CSlider>(position, length, callbacks.at(config["callback"].String()), itemsVisible, itemsTotal, value, horizontal, style);
+}
+
+std::shared_ptr<CAnimImage> InterfaceObjectConfigurable::buildImage(const JsonNode & config) const
+{
+	logGlobal->debug("Building widget CAnimImage");
+	auto position = readPosition(config["position"]);
+	auto image = config["image"].String();
+	int group = config["group"].isNull() ? 0 : config["group"].Integer();
+	int frame = config["frame"].isNull() ? 0 : config["frame"].Integer();
+	return std::make_shared<CAnimImage>(image, frame, group, position.x, position.y);
+}
+
+std::shared_ptr<CFilledTexture> InterfaceObjectConfigurable::buildTexture(const JsonNode & config) const
+{
+	logGlobal->debug("Building widget CFilledTexture");
+	auto image = config["image"].String();
+	auto rect = readRect(config["rect"]);
+	return std::make_shared<CFilledTexture>(image, rect);
+}
+
+std::shared_ptr<CShowableAnim> InterfaceObjectConfigurable::buildAnimation(const JsonNode & config) const
+{
+	logGlobal->debug("Building widget CShowableAnim");
+	auto position = readPosition(config["position"]);
+	auto image = config["image"].String();
+	ui8 flags = 0;
+	if(!config["repeat"].Bool())
+		flags |= CShowableAnim::EFlags::PLAY_ONCE;
+	
+	int group = config["group"].isNull() ? 0 : config["group"].Integer();
+	auto anim = std::make_shared<CShowableAnim>(position.x, position.y, image, flags, 4, group);
+	if(!config["alpha"].isNull())
+		anim->setAlpha(config["alpha"].Integer());
+	if(!config["callback"].isNull())
+		anim->callback = std::bind(callbacks.at(config["callback"].String()), 0);
+	if(!config["frames"].isNull())
+	{
+		auto b = config["frames"]["start"].Integer();
+		auto e = config["frames"]["end"].Integer();
+		anim->set(group, b, e);
+	}
+	return anim;
+}
+
+std::shared_ptr<CIntObject> InterfaceObjectConfigurable::buildWidget(JsonNode config) const
+{
+	assert(!config.isNull());
+	logGlobal->debug("Building widget from config");
+	//overrides from variables
+	for(auto & item : config["overrides"].Struct())
+	{
+		logGlobal->debug("Config attribute %s was overriden by variable %s", item.first, item.second.String());
+		config[item.first] = variables[item.second.String()];
+	}
+	
+	auto type = config["type"].String();
+	auto buildIterator = builders.find(type);
+	if(buildIterator != builders.end())
+		return (buildIterator->second)(config);
+
+	logGlobal->error("Builder with type %s is not registered", type);
+	return nullptr;
+}

+ 89 - 0
client/gui/InterfaceObjectConfigurable.h

@@ -0,0 +1,89 @@
+/*
+* InterfaceBuilder.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 "CIntObject.h"
+
+#include "../../lib/JsonNode.h"
+
+class CPicture;
+class CLabel;
+class CToggleGroup;
+class CToggleButton;
+class CButton;
+class CLabelGroup;
+class CSlider;
+class CAnimImage;
+class CShowableAnim;
+class CFilledTexture;
+
+#define REGISTER_BUILDER(type, method) registerBuilder(type, std::bind(method, this, std::placeholders::_1))
+
+class InterfaceObjectConfigurable: public CIntObject
+{
+public:
+	InterfaceObjectConfigurable(int used=0, Point offset=Point());
+	InterfaceObjectConfigurable(const JsonNode & config, int used=0, Point offset=Point());
+
+protected:
+	
+	using BuilderFunction = std::function<std::shared_ptr<CIntObject>(const JsonNode &)>;
+	void registerBuilder(const std::string &, BuilderFunction);
+	
+	//must be called after adding callbacks
+	void build(const JsonNode & config);
+	
+	void addCallback(const std::string & callbackName, std::function<void(int)> callback);
+	JsonNode variables;
+	
+	template<class T>
+	const std::shared_ptr<T> widget(const std::string & name) const
+	{
+		auto iter = widgets.find(name);
+		if(iter == widgets.end())
+			return nullptr;
+		return std::dynamic_pointer_cast<T>(iter->second);
+	}
+	
+	void deleteWidget(const std::string & name);
+		
+	//basic serializers
+	Point readPosition(const JsonNode &) const;
+	Rect readRect(const JsonNode &) const;
+	ETextAlignment readTextAlignment(const JsonNode &) const;
+	SDL_Color readColor(const JsonNode &) const;
+	EFonts readFont(const JsonNode &) const;
+	std::string readText(const JsonNode &) const;
+	std::pair<std::string, std::string> readHintText(const JsonNode &) const;
+	int readKeycode(const JsonNode &) const;
+	
+	//basic widgets
+	std::shared_ptr<CPicture> buildPicture(const JsonNode &) const;
+	std::shared_ptr<CLabel> buildLabel(const JsonNode &) const;
+	std::shared_ptr<CToggleGroup> buildToggleGroup(const JsonNode &) const;
+	std::shared_ptr<CToggleButton> buildToggleButton(const JsonNode &) const;
+	std::shared_ptr<CButton> buildButton(const JsonNode &) const;
+	std::shared_ptr<CLabelGroup> buildLabelGroup(const JsonNode &) const;
+	std::shared_ptr<CSlider> buildSlider(const JsonNode &) const;
+	std::shared_ptr<CAnimImage> buildImage(const JsonNode &) const;
+	std::shared_ptr<CShowableAnim> buildAnimation(const JsonNode &) const;
+	std::shared_ptr<CFilledTexture> buildTexture(const JsonNode &) const;
+		
+	//composite widgets
+	std::shared_ptr<CIntObject> buildWidget(JsonNode config) const;
+	
+private:
+	
+	int unnamedObjectId = 0;
+	std::map<std::string, BuilderFunction> builders;
+	std::map<std::string, std::shared_ptr<CIntObject>> widgets;
+	std::map<std::string, std::function<void(int)>> callbacks;
+};

+ 1 - 1
client/gui/NotificationHandler.h

@@ -1,5 +1,5 @@
 /*
-* NotificationHandler.cpp, part of VCMI engine
+* NotificationHandler.h, part of VCMI engine
 *
 * Authors: listed in file AUTHORS in main folder
 *

Some files were not shown because too many files changed in this diff