Răsfoiți Sursa

Merge branch 'develop' into editor-lasso

Nordsoft91 2 ani în urmă
părinte
comite
ea33fed15c
100 a modificat fișierele cu 4429 adăugiri și 3266 ștergeri
  1. 1 0
      .github/workflows/github.yml
  2. 3 0
      .gitignore
  3. 0 1
      AI/BattleAI/AttackPossibility.cpp
  4. 1 2
      AI/BattleAI/BattleAI.cpp
  5. 2 2
      AI/BattleAI/BattleAI.h
  6. 0 2
      AI/BattleAI/BattleExchangeVariant.cpp
  7. 1 1
      AI/EmptyAI/CEmptyAI.cpp
  8. 1 1
      AI/EmptyAI/CEmptyAI.h
  9. 14 14
      AI/Nullkiller/AIGateway.cpp
  10. 1 1
      AI/Nullkiller/AIGateway.h
  11. 1 1
      AI/Nullkiller/AIUtility.h
  12. 0 1
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  13. 1 1
      AI/Nullkiller/Engine/FuzzyEngines.cpp
  14. 1 3
      AI/Nullkiller/Engine/FuzzyHelper.cpp
  15. 1 1
      AI/Nullkiller/Engine/Nullkiller.cpp
  16. 2 2
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  17. 8 2
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  18. 3 3
      AI/StupidAI/StupidAI.cpp
  19. 3 3
      AI/StupidAI/StupidAI.h
  20. 1 1
      AI/VCAI/FuzzyEngines.cpp
  21. 1 1
      AI/VCAI/Goals/Explore.cpp
  22. 2 2
      AI/VCAI/Pathfinding/AINodeStorage.cpp
  23. 13 11
      AI/VCAI/VCAI.cpp
  24. 1 1
      AI/VCAI/VCAI.h
  25. 1 6
      CCallback.cpp
  26. 0 2
      CCallback.h
  27. 12 0
      CI/conan/base/apple
  28. 5 0
      CI/conan/base/ios
  29. 4 0
      CI/conan/base/macos
  30. 2 11
      CI/conan/ios-arm64
  31. 4 10
      CI/conan/ios-armv7
  32. 2 10
      CI/conan/macos-arm
  33. 2 10
      CI/conan/macos-intel
  34. 2 2
      CI/ios/before_install.sh
  35. 1 1
      CI/linux/before_install.sh
  36. 2 2
      CI/mac/before_install.sh
  37. 7 7
      CI/msvc/before_install.sh
  38. 9 0
      CI/mxe/before_install.sh
  39. 57 15
      CMakeLists.txt
  40. 1 0
      CMakePresets.json
  41. 19 38
      Global.h
  42. 8 14
      client/CBitmapHandler.cpp
  43. 1 1
      client/CBitmapHandler.h
  44. 2 2
      client/CGameInfo.h
  45. 104 105
      client/CMT.cpp
  46. 8 5
      client/CMakeLists.txt
  47. 1 1
      client/CMessage.cpp
  48. 41 5
      client/CMusicHandler.cpp
  49. 72 120
      client/CPlayerInterface.cpp
  50. 5 5
      client/CPlayerInterface.h
  51. 3 3
      client/CServerHandler.cpp
  52. 13 16
      client/Client.cpp
  53. 1 1
      client/CreatureCostBox.cpp
  54. 14 11
      client/NetPacksClient.cpp
  55. 1 1
      client/NetPacksLobbyClient.cpp
  56. 71 55
      client/battle/BattleActionsController.cpp
  57. 7 0
      client/battle/BattleActionsController.h
  58. 297 384
      client/battle/BattleAnimationClasses.cpp
  59. 150 133
      client/battle/BattleAnimationClasses.h
  60. 92 0
      client/battle/BattleConstants.h
  61. 0 328
      client/battle/BattleControlPanel.cpp
  62. 0 76
      client/battle/BattleControlPanel.h
  63. 59 31
      client/battle/BattleEffectsController.cpp
  64. 14 31
      client/battle/BattleEffectsController.h
  65. 231 309
      client/battle/BattleFieldController.cpp
  66. 10 0
      client/battle/BattleFieldController.h
  67. 192 388
      client/battle/BattleInterface.cpp
  68. 100 83
      client/battle/BattleInterface.h
  69. 187 95
      client/battle/BattleInterfaceClasses.cpp
  70. 42 13
      client/battle/BattleInterfaceClasses.h
  71. 21 33
      client/battle/BattleObstacleController.cpp
  72. 7 5
      client/battle/BattleObstacleController.h
  73. 29 32
      client/battle/BattleProjectileController.cpp
  74. 6 6
      client/battle/BattleProjectileController.h
  75. 6 1
      client/battle/BattleRenderer.cpp
  76. 5 5
      client/battle/BattleRenderer.h
  77. 19 16
      client/battle/BattleSiegeController.cpp
  78. 2 2
      client/battle/BattleSiegeController.h
  79. 488 136
      client/battle/BattleStacksController.cpp
  80. 45 12
      client/battle/BattleStacksController.h
  81. 572 0
      client/battle/BattleWindow.cpp
  82. 94 0
      client/battle/BattleWindow.h
  83. 119 91
      client/battle/CreatureAnimation.cpp
  84. 20 15
      client/battle/CreatureAnimation.h
  85. 31 47
      client/gui/CAnimation.cpp
  86. 8 10
      client/gui/CAnimation.h
  87. 0 271
      client/gui/CCursorHandler.cpp
  88. 0 82
      client/gui/CCursorHandler.h
  89. 7 7
      client/gui/CGuiHandler.cpp
  90. 2 2
      client/gui/CIntObject.cpp
  91. 31 13
      client/gui/Canvas.cpp
  92. 10 0
      client/gui/Canvas.h
  93. 162 0
      client/gui/ColorFilter.cpp
  94. 66 0
      client/gui/ColorFilter.h
  95. 402 0
      client/gui/CursorHandler.cpp
  96. 233 0
      client/gui/CursorHandler.h
  97. 7 1
      client/gui/Geometries.cpp
  98. 40 53
      client/gui/Geometries.h
  99. 74 38
      client/gui/InterfaceObjectConfigurable.cpp
  100. 5 1
      client/gui/InterfaceObjectConfigurable.h

+ 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
 			{

+ 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;

+ 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

+ 0 - 1
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -202,7 +202,6 @@ void ObjectClusterizer::clusterize()
 		Obj::WHIRLPOOL,
 		Obj::BUOY,
 		Obj::SIGN,
-		Obj::SIGN,
 		Obj::GARRISON,
 		Obj::MONSTER,
 		Obj::GARRISON2,

+ 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);

+ 12 - 0
CI/conan/base/apple

@@ -0,0 +1,12 @@
+[settings]
+compiler=apple-clang
+compiler.version=14
+compiler.libcxx=libc++
+build_type=Release
+
+# required for Boost.Locale in versions >= 1.81
+compiler.cppstd=11
+
+[conf]
+tools.apple:enable_bitcode = False
+tools.cmake.cmaketoolchain:generator = Ninja

+ 5 - 0
CI/conan/base/ios

@@ -0,0 +1,5 @@
+include(apple)
+
+[settings]
+os=iOS
+os.sdk=iphoneos

+ 4 - 0
CI/conan/base/macos

@@ -0,0 +1,4 @@
+include(apple)
+
+[settings]
+os=Macos

+ 2 - 11
CI/conan/ios-arm64

@@ -1,14 +1,5 @@
+include(base/ios)
+
 [settings]
-os=iOS
 os.version=12.0
-os.sdk=iphoneos
 arch=armv8
-compiler=apple-clang
-compiler.version=13
-compiler.libcxx=libc++
-build_type=Release
-[options]
-[build_requires]
-[env]
-[conf]
-tools.cmake.cmaketoolchain:generator = Ninja

+ 4 - 10
CI/conan/ios-armv7

@@ -1,14 +1,8 @@
+include(base/ios)
+
 [settings]
-os=iOS
 os.version=10.0
-os.sdk=iphoneos
 arch=armv7
-compiler=apple-clang
+
+# Xcode 13.x is the last version that can build for armv7
 compiler.version=13
-compiler.libcxx=libc++
-build_type=Release
-[options]
-[build_requires]
-[env]
-[conf]
-tools.cmake.cmaketoolchain:generator = Ninja

+ 2 - 10
CI/conan/macos-arm

@@ -1,13 +1,5 @@
+include(base/macos)
+
 [settings]
-os=Macos
 os.version=11.0
 arch=armv8
-compiler=apple-clang
-compiler.version=13
-compiler.libcxx=libc++
-build_type=Release
-[options]
-[build_requires]
-[env]
-[conf]
-tools.cmake.cmaketoolchain:generator = Ninja

+ 2 - 10
CI/conan/macos-intel

@@ -1,13 +1,5 @@
+include(base/macos)
+
 [settings]
-os=Macos
 os.version=10.13
 arch=x86_64
-compiler=apple-clang
-compiler.version=13
-compiler.libcxx=libc++
-build_type=Release
-[options]
-[build_requires]
-[env]
-[conf]
-tools.cmake.cmaketoolchain:generator = Ninja

+ 2 - 2
CI/ios/before_install.sh

@@ -1,7 +1,7 @@
 #!/usr/bin/env bash
 
-echo DEVELOPER_DIR=/Applications/Xcode_13.4.1.app >> $GITHUB_ENV
+echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV
 
 mkdir ~/.conan ; cd ~/.conan
-curl -L 'https://github.com/vcmi/vcmi-ios-deps/releases/download/1.1/ios-arm64.xz' \
+curl -L 'https://github.com/vcmi/vcmi-ios-deps/releases/download/1.2/ios-arm64.txz' \
 	| tar -xf -

+ 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

+ 2 - 2
CI/mac/before_install.sh

@@ -1,9 +1,9 @@
 #!/usr/bin/env bash
 
-echo DEVELOPER_DIR=/Applications/Xcode_13.4.1.app >> $GITHUB_ENV
+echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV
 
 brew install ninja
 
 mkdir ~/.conan ; cd ~/.conan
-curl -L "https://github.com/vcmi/vcmi-deps-macos/releases/download/1.1/$DEPS_FILENAME.txz" \
+curl -L "https://github.com/vcmi/vcmi-deps-macos/releases/download/1.2/$DEPS_FILENAME.txz" \
 	| tar -xf -

+ 7 - 7
CI/msvc/before_install.sh

@@ -1,10 +1,10 @@
-curl -LfsS -o "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v140.7z" \
-	"https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.5/vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v140.7z"
-7z x "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v140.7z"
+curl -LfsS -o "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" \
+	"https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.6/vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z"
+7z x "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z"
 
-rm -r -f vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug
-mkdir -p vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug/bin
-cp vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/bin/* vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug/bin
+#rm -r -f vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug
+#mkdir -p vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug/bin
+#cp vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/bin/* vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug/bin
 
 DUMPBIN_DIR=$(vswhere -latest -find **/dumpbin.exe | head -n 1)
-dirname "$DUMPBIN_DIR" > $GITHUB_PATH
+dirname "$DUMPBIN_DIR" > $GITHUB_PATH

+ 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

+ 57 - 15
CMakeLists.txt

@@ -48,7 +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")
@@ -60,6 +62,7 @@ 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)
@@ -126,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             #
@@ -211,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")
@@ -249,19 +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")
+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_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-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
+	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(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
-		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unknown-warning-option")
+	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)
@@ -329,6 +359,15 @@ 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)
@@ -524,6 +563,9 @@ if(WIN32)
 				FILES ${integration_loc}
 				DESTINATION ${BIN_DIR}/platforms
 			)
+			install(
+				FILES "$<TARGET_FILE:Qt${QT_VERSION_MAJOR}::QWindowsVistaStylePlugin>" 
+				DESTINATION ${BIN_DIR}/styles) 
 		endif()
 	endif()
 

+ 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}"
             }
         },

+ 19 - 38
Global.h

@@ -15,13 +15,6 @@
 // Fixed width bool data type is important for serialization
 static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 
-/* ---------------------------------------------------------------------------- */
-/* 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. */
 /* ---------------------------------------------------------------------------- */
@@ -71,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]];
@@ -89,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
@@ -249,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
@@ -490,32 +491,6 @@ namespace vstd
 		ptr = nullptr;
 	}
 
-	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)));
-	}
-
 	template <typename Container>
 	typename Container::const_reference circularAt(const Container &r, size_t index)
 	{
@@ -743,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

+ 8 - 14
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;
 			}
@@ -196,12 +190,12 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fna
 	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);
 }

+ 2 - 2
client/CGameInfo.h

@@ -38,7 +38,7 @@ VCMI_LIB_NAMESPACE_END
 class CMapHandler;
 class CSoundHandler;
 class CMusicHandler;
-class CCursorHandler;
+class CursorHandler;
 class IMainVideoPlayer;
 class CServerHandler;
 
@@ -49,7 +49,7 @@ public:
 	CSoundHandler * soundh;
 	CMusicHandler * musich;
 	CConsoleHandler * consoleh;
-	CCursorHandler * curh;
+	CursorHandler * curh;
 	IMainVideoPlayer * videoh;
 };
 extern CClientState * CCS;

+ 104 - 105
client/CMT.cpp

@@ -25,7 +25,7 @@
 #include "lobby/CSelectionBase.h"
 #include "windows/CCastleInterface.h"
 #include "../lib/CConsoleHandler.h"
-#include "gui/CCursorHandler.h"
+#include "gui/CursorHandler.h"
 #include "../lib/CGameState.h"
 #include "../CCallback.h"
 #include "CPlayerInterface.h"
@@ -208,6 +208,8 @@ int main(int argc, char * argv[])
 		("lobby-host", "if this client hosts session")
 		("lobby-uuid", po::value<std::string>(), "uuid to the server")
 		("lobby-connections", po::value<ui16>(), "connections of server")
+		("lobby-username", po::value<std::string>(), "player name")
+		("lobby-gamemode", po::value<ui16>(), "use 0 for new game and 1 for load game")
 		("uuid", po::value<std::string>(), "uuid for the client");
 
 	if(argc > 1)
@@ -468,10 +470,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 CursorHandler();
 		logGlobal->info("Screen handler: %d ms", pomtime.getDiff());
 		pomtime.getDiff();
 
@@ -490,13 +491,41 @@ int main(int argc, char * argv[])
 	session["autoSkip"].Bool()  = vm.count("autoSkip");
 	session["oneGoodAI"].Bool() = vm.count("oneGoodAI");
 	session["aiSolo"].Bool() = false;
+	std::shared_ptr<CMainMenu> mmenu;
 	
+	if(vm.count("testmap"))
+	{
+		session["testmap"].String() = vm["testmap"].as<std::string>();
+		session["onlyai"].Bool() = true;
+		boost::thread(&CServerHandler::debugStartTest, CSH, session["testmap"].String(), false);
+	}
+	else if(vm.count("testsave"))
+	{
+		session["testsave"].String() = vm["testsave"].as<std::string>();
+		session["onlyai"].Bool() = true;
+		boost::thread(&CServerHandler::debugStartTest, CSH, session["testsave"].String(), true);
+	}
+	else
+	{
+		mmenu = CMainMenu::create();
+		GH.curInt = mmenu.get();
+	}
+	
+	std::vector<std::string> names;
 	session["lobby"].Bool() = false;
 	if(vm.count("lobby"))
 	{
 		session["lobby"].Bool() = true;
 		session["host"].Bool() = false;
 		session["address"].String() = vm["lobby-address"].as<std::string>();
+		if(vm.count("lobby-username"))
+			session["username"].String() = vm["lobby-username"].as<std::string>();
+		else
+			session["username"].String() = settings["launcher"]["lobbyUsername"].String();
+		if(vm.count("lobby-gamemode"))
+			session["gamemode"].Integer() = vm["lobby-gamemode"].as<ui16>();
+		else
+			session["gamemode"].Integer() = 0;
 		CSH->uuid = vm["uuid"].as<std::string>();
 		session["port"].Integer() = vm["lobby-port"].as<ui16>();
 		logGlobal->info("Remote lobby mode at %s:%d, uuid is %s", session["address"].String(), session["port"].Integer(), CSH->uuid);
@@ -511,23 +540,11 @@ int main(int argc, char * argv[])
 		//we should not reconnect to previous game in online mode
 		Settings saveSession = settings.write["server"]["reconnect"];
 		saveSession->Bool() = false;
-	}
-
-	if(vm.count("testmap"))
-	{
-		session["testmap"].String() = vm["testmap"].as<std::string>();
-		session["onlyai"].Bool() = true;
-		boost::thread(&CServerHandler::debugStartTest, CSH, session["testmap"].String(), false);
-	}
-	else if(vm.count("testsave"))
-	{
-		session["testsave"].String() = vm["testsave"].as<std::string>();
-		session["onlyai"].Bool() = true;
-		boost::thread(&CServerHandler::debugStartTest, CSH, session["testsave"].String(), true);
-	}
-	else
-	{
-		GH.curInt = CMainMenu::create().get();
+		
+		//start lobby immediately
+		names.push_back(session["username"].String());
+		ESelectionScreen sscreen = session["gamemode"].Integer() == 0 ? ESelectionScreen::newGame : ESelectionScreen::loadGame;
+		mmenu->openLobby(sscreen, session["host"].Bool(), &names, ELoadMode::MULTI);
 	}
 	
 	// Restore remote session - start game immediately
@@ -585,6 +602,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 +682,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 +867,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());
 	}
@@ -1079,7 +1068,8 @@ static bool recreateWindow(int w, int h, int bpp, bool fullscreen, int displayIn
 		if (displayIndex < 0)
 			displayIndex = 0;
 	}
-#ifdef VCMI_IOS
+
+#if defined(VCMI_ANDROID) || defined(VCMI_IOS)
 	SDL_GetWindowSize(mainWindow, &w, &h);
 #else
 	if(!checkVideoMode(displayIndex, w, h))
@@ -1506,80 +1496,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();
-			}
-
-			cleanupRenderer();
+	vstd::clear_pointer(console);// should be removed after everything else since used by logging
 
-			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
 	{

+ 8 - 5
client/CMakeLists.txt

@@ -4,7 +4,6 @@ set(client_SRCS
 
 		battle/BattleActionsController.cpp
 		battle/BattleAnimationClasses.cpp
-		battle/BattleControlPanel.cpp
 		battle/BattleEffectsController.cpp
 		battle/BattleFieldController.cpp
 		battle/BattleInterfaceClasses.cpp
@@ -14,13 +13,15 @@ set(client_SRCS
 		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/CursorHandler.cpp
 		gui/CGuiHandler.cpp
 		gui/CIntObject.cpp
+		gui/ColorFilter.cpp
 		gui/Fonts.cpp
 		gui/Geometries.cpp
 		gui/SDL_Extensions.cpp
@@ -88,7 +89,6 @@ set(client_HEADERS
 
 		battle/BattleActionsController.h
 		battle/BattleAnimationClasses.h
-		battle/BattleControlPanel.h
 		battle/BattleEffectsController.h
 		battle/BattleFieldController.h
 		battle/BattleInterfaceClasses.h
@@ -98,12 +98,15 @@ set(client_HEADERS
 		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/CursorHandler.h
 		gui/CGuiHandler.h
+		gui/ColorFilter.h
 		gui/CIntObject.h
 		gui/Fonts.h
 		gui/Geometries.h
@@ -229,7 +232,7 @@ if(WIN32)
 		add_custom_command(TARGET vcmiclient POST_BUILD
 			WORKING_DIRECTORY "$<TARGET_FILE_DIR:vcmiclient>"
 			COMMAND ${CMAKE_COMMAND} -E copy AI/fuzzylite.dll fuzzylite.dll
-			COMMAND ${CMAKE_COMMAND} -E copy AI/tbb.dll tbb.dll
+			COMMAND ${CMAKE_COMMAND} -E copy AI/tbb12.dll tbb12.dll
 		)
 	endif()
 elseif(APPLE_IOS)

+ 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++)

+ 41 - 5
client/CMusicHandler.cpp

@@ -89,7 +89,7 @@ CSoundHandler::CSoundHandler():
 		soundBase::battle02, soundBase::battle03, soundBase::battle04,
 		soundBase::battle05, soundBase::battle06, soundBase::battle07
 	};
-	
+
 	//predefine terrain set
 	//TODO: support custom sounds for new terrains and load from json
 	horseSounds =
@@ -409,6 +409,8 @@ void CMusicHandler::release()
 
 void CMusicHandler::playMusic(const std::string & musicURI, bool loop, bool fromStart)
 {
+	boost::mutex::scoped_lock guard(mutex);
+
 	if (current && current->isPlaying() && current->isTrack(musicURI))
 		return;
 
@@ -422,6 +424,8 @@ void CMusicHandler::playMusicFromSet(const std::string & musicSet, const std::st
 
 void CMusicHandler::playMusicFromSet(const std::string & whichSet, bool loop, bool fromStart)
 {
+	boost::mutex::scoped_lock guard(mutex);
+
 	auto selectedSet = musicsSet.find(whichSet);
 	if (selectedSet == musicsSet.end())
 	{
@@ -441,8 +445,6 @@ void CMusicHandler::queueNext(std::unique_ptr<MusicEntry> queued)
 	if (!initialized)
 		return;
 
-	boost::mutex::scoped_lock guard(mutex);
-
 	next = std::move(queued);
 
 	if (current.get() == nullptr || !current->stop(1000))
@@ -456,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)
 	{
@@ -487,13 +489,32 @@ void CMusicHandler::setVolume(ui32 percent)
 
 void CMusicHandler::musicFinishedCallback()
 {
-	boost::mutex::scoped_lock guard(mutex);
+	// boost::mutex::scoped_lock guard(mutex);
+	// FIXME: WORKAROUND FOR A POTENTIAL DEADLOCK
+	// It is possible for:
+	// 1) SDL thread to call this method on end of playback
+	// 2) VCMI code to call queueNext() method to queue new file
+	// this leads to:
+	// 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked)
+	// 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked)
+	// Because of that (and lack of clear way to fix that)
+	// We will try to acquire lock here and if failed - do nothing
+	// This may break music playback till next song is enqued but won't deadlock the game
+
+	if (!mutex.try_lock())
+	{
+		logGlobal->error("Failed to acquire mutex! Unable to restart music!");
+		return;
+	}
 
 	if (current.get() != nullptr)
 	{
 		// if music is looped, play it again
 		if (current->play())
+		{
+			mutex.unlock();
 			return;
+		}
 		else
 			current.reset();
 	}
@@ -503,6 +524,7 @@ void CMusicHandler::musicFinishedCallback()
 		current.reset(next.release());
 		current->play();
 	}
+	mutex.unlock();
 }
 
 MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, std::string musicURI, bool looped, bool fromStart):
@@ -520,6 +542,20 @@ MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, std::string mu
 }
 MusicEntry::~MusicEntry()
 {
+	if (playing)
+	{
+		assert(0);
+		logGlobal->error("Attempt to delete music while playing!");
+		Mix_HaltMusic();
+	}
+
+	if (loop == 0 && Mix_FadingMusic() != MIX_NO_FADING)
+	{
+		assert(0);
+		logGlobal->error("Attempt to delete music while fading out!");
+		Mix_HaltMusic();
+	}
+
 	logGlobal->trace("Del-ing music file %s", currentName);
 	if (music)
 		Mix_FreeMusic(music);

+ 72 - 120
client/CPlayerInterface.cpp

@@ -16,10 +16,10 @@
 #include "battle/BattleEffectsController.h"
 #include "battle/BattleFieldController.h"
 #include "battle/BattleInterfaceClasses.h"
-#include "battle/BattleControlPanel.h"
+#include "battle/BattleWindow.h"
 #include "../CCallback.h"
 #include "windows/CCastleInterface.h"
-#include "gui/CCursorHandler.h"
+#include "gui/CursorHandler.h"
 #include "windows/CKingdomInterface.h"
 #include "CGameInfo.h"
 #include "windows/CHeroWindow.h"
@@ -93,7 +93,7 @@ boost::recursive_mutex * CPlayerInterface::pim = new boost::recursive_mutex;
 
 CPlayerInterface * LOCPLINT;
 
-BattleInterface * 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
@@ -142,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;
@@ -267,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);
@@ -692,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);
@@ -707,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;
@@ -747,8 +749,6 @@ void CPlayerInterface::battleUnitsChanged(const std::vector<UnitChanges> & units
 			break;
 		}
 	}
-
-	battleInt->effectsController->displayCustomEffects(customEffects);
 }
 
 void CPlayerInterface::battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles)
@@ -838,9 +838,9 @@ BattleAction CPlayerInterface::activeStack(const CStack * stack) //called when i
 		autofightingAI.reset();
 	}
 
-	BattleInterface *b = battleInt;
+	assert(battleInt);
 
-	if(!b)
+	if(!battleInt)
 	{
 		return BattleAction::makeDefend(stack); // probably battle is finished already
 	}
@@ -853,7 +853,7 @@ BattleAction CPlayerInterface::activeStack(const CStack * stack) //called when i
 
 	{
 		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
@@ -915,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 )
 {
@@ -944,7 +944,7 @@ void CPlayerInterface::battleTriggerEffect (const BattleTriggerEffect & bte)
 	RETURN_IF_QUICK_COMBAT;
 	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;
@@ -954,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->effectsController->displayEffect(EBattleEffect::EBattleEffect(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 to_put = {defender, elem.damageAmount, elem.killedAmount, attacker, remoteAttack, elem.killed(), elem.willRebirth(), elem.cloneKilled()};
-		arg.push_back(to_put);
+		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();
+
+		if (elem.isSpell())
+			info.spellEffect = elem.spellID;
+
+		arg.push_back(info);
 	}
 	battleInt->stacksAreAttacked(arg);
 }
@@ -982,94 +983,36 @@ void CPlayerInterface::battleAttack(const BattleAttack * ba)
 
 	assert(curAction);
 
-	const CStack * attacker = cb->battleGetStackByID(ba->stackAttacking);
+	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;
 
-	if(!attacker)
-	{
-		logGlobal->error("Attacking stack not found");
-		return;
-	}
+	if (ba->spellLike())
+		info.spellEffect = ba->spellID;
 
-	if(ba->lucky()) //lucky hit
-	{
-		battleInt->controlPanel->console->addText(attacker->formatGeneralMessage(-45));
-		battleInt->effectsController->displayEffect(EBattleEffect::GOOD_LUCK, soundBase::GOODLUCK, attacker->getPosition());
-	}
-	if(ba->unlucky()) //unlucky hit
-	{
-		battleInt->controlPanel->console->addText(attacker->formatGeneralMessage(-44));
-		battleInt->effectsController->displayEffect(EBattleEffect::BAD_LUCK, soundBase::BADLUCK, attacker->getPosition());
-	}
-	if(ba->deathBlow())
+	for(auto & elem : ba->bsa)
 	{
-		battleInt->controlPanel->console->addText(attacker->formatGeneralMessage(365));
-		for(auto & elem : ba->bsa)
+		if(!elem.isSecondary())
 		{
-			const CStack * attacked = cb->battleGetStackByID(elem.stackAttacked);
-			battleInt->effectsController->displayEffect(EBattleEffect::DEATH_BLOW, attacked->getPosition());
+			assert(info.defender == nullptr);
+			info.defender = cb->battleGetStackByID(elem.stackAttacked);
 		}
-		CCS->soundh->playSound(soundBase::deathBlow);
-	}
-
-	battleInt->effectsController->displayCustomEffects(ba->customEffects);
-
-	battleInt->waitForAnims();
-
-	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)
-		{
-			if(!elem.isSecondary()) //display projectile only for primary target
-			{
-				const CStack * attacked = cb->battleGetStackByID(elem.stackAttacked);
-				battleInt->stackAttacking(attacker, attacked->getPosition(), attacked, true);
-			}
-		}
-	}
-	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)
@@ -1551,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);
@@ -1603,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));
@@ -1680,7 +1623,7 @@ int CPlayerInterface::getLastIndex( std::string namePrefix)
 	else
 	for (directory_iterator dir(gamesDir); dir != enddir; ++dir)
 	{
-		if (is_regular(dir->status()))
+		if (is_regular_file(dir->status()))
 		{
 			std::string name = dir->path().filename().string();
 			if (starts_with(name, namePrefix) && ends_with(name, ".vcgm1"))
@@ -1986,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;
@@ -2090,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
@@ -2251,6 +2194,8 @@ void CPlayerInterface::artifactRemoved(const ArtifactLocation &al)
 		if (artWin)
 			artWin->artifactRemoved(al);
 	}
+
+	waitWhileDialog();
 }
 
 void CPlayerInterface::artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst)
@@ -2265,6 +2210,8 @@ void CPlayerInterface::artifactMoved(const ArtifactLocation &src, const Artifact
 	}
 	if(!GH.objsToBlit.empty())
 		GH.objsToBlit.back()->redraw();
+
+	waitWhileDialog();
 }
 
 void CPlayerInterface::artifactPossibleAssembling(const ArtifactLocation & dst)
@@ -2383,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
@@ -2422,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;
@@ -2479,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);
@@ -2524,6 +2473,9 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
 		// (i == 0) means hero went through all the path
 		adventureInt->updateMoveHero(h, (i != 0));
 		adventureInt->updateNextHero(h);
+
+		// ugly workaround to force instant update of adventure map
+		adventureInt->animValHitCount = 8;
 	}
 
 	setMovementStatus(false);

+ 5 - 5
client/CPlayerInterface.h

@@ -84,7 +84,7 @@ public:
 	static const int SAVES_COUNT = 5;
 
 	CCastleInterface * castleInt; //nullptr if castle window isn't opened
-	static BattleInterface * 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
@@ -193,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;
@@ -220,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
 

+ 3 - 3
client/CServerHandler.cpp

@@ -132,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>();
@@ -176,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);
@@ -718,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)

+ 13 - 16
client/Client.cpp

@@ -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<BattleInterface>(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<BattleInterface>(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,14 +755,20 @@ 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
 }
 
-
 #ifdef VCMI_ANDROID
+extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_clientSetupJNI(JNIEnv * env, jobject cls)
+{
+	logNetwork->info("Received clientSetupJNI");
+
+	CAndroidVMHelper::cacheVM(env);
+}
+
 extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerClosed(JNIEnv * env, jobject cls)
 {
 	logNetwork->info("Received server closed signal");

+ 1 - 1
client/CreatureCostBox.cpp

@@ -18,7 +18,7 @@ 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, ETextAlignment::CENTER, Colors::WHITE, titleText);
 }

+ 14 - 11
client/NetPacksClient.cpp

@@ -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)

+ 1 - 1
client/NetPacksLobbyClient.cpp

@@ -63,7 +63,7 @@ void LobbyClientDisconnected::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHa
 
 void LobbyChatMessage::applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler)
 {
-	if(lobby)
+	if(lobby && lobby->card)
 	{
 		lobby->card->chat->addNewMessage(playerName + ": " + message);
 		lobby->card->setChat(true);

+ 71 - 55
client/battle/BattleActionsController.cpp

@@ -10,7 +10,7 @@
 #include "StdInc.h"
 #include "BattleActionsController.h"
 
-#include "BattleControlPanel.h"
+#include "BattleWindow.h"
 #include "BattleStacksController.h"
 #include "BattleInterface.h"
 #include "BattleFieldController.h"
@@ -19,7 +19,7 @@
 
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/CIntObject.h"
 #include "../windows/CCreatureWindow.h"
@@ -60,7 +60,7 @@ void BattleActionsController::endCastingSpell()
 
 		currentSpell = nullptr;
 		spellDestSelectMode = false;
-		CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER);
+		CCS->curh->set(Cursor::Combat::POINTER);
 
 		if(owner.stacksController->getActiveStack())
 		{
@@ -122,7 +122,7 @@ void BattleActionsController::enterCreatureCastingMode()
 			owner.giveCommand(EActionType::MONSTER_SPELL, BattleHex::INVALID, owner.stacksController->activeStackSpellToCast());
 			owner.stacksController->setSelectedStack(nullptr);
 
-			CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER);
+			CCS->curh->set(Cursor::Combat::POINTER);
 		}
 	}
 	else
@@ -171,8 +171,6 @@ void BattleActionsController::reorderPossibleActionsPriority(const CStack * stac
 			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:
@@ -233,7 +231,7 @@ void BattleActionsController::castThisSpell(SpellID spellID)
 
 void BattleActionsController::handleHex(BattleHex myNumber, int eventType)
 {
-	if (!owner.myTurn || !owner.battleActionsStarted) //we are not permit to do anything
+	if (!owner.myTurn) //we are not permit to do anything
 		return;
 
 	// This function handles mouse move over hexes and l-clicking on them.
@@ -245,8 +243,8 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType)
 	std::string newConsoleMsg;
 	//used when hovering -> tooltip message and cursor to be set
 	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?
+	bool spellcastingCursor = false;
+	auto cursorFrame = Cursor::Combat::POINTER;
 
 	//used when l-clicking -> action to be called upon the click
 	std::function<void()> realizeAction;
@@ -263,9 +261,6 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType)
 	if (shere)
 		ourStack = shere->owner == owner.curInt->playerID;
 
-	//stack may have changed, update selection border
-	owner.stacksController->setHoveredStack(shere);
-
 	localActions.clear();
 	illegalActions.clear();
 
@@ -301,7 +296,6 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType)
 				{
 					if (owner.fieldController->isTileAttackable(myNumber)) // move isTileAttackable to be part of battleCanAttack?
 					{
-						owner.fieldController->setBattleCursor(myNumber); // temporary - needed for following function :(
 						BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(myNumber);
 
 						if (attackFromHex >= 0) //we can be in this line when unreachable creature is L - clicked (as of revision 1308)
@@ -378,19 +372,6 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType)
 				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);
@@ -434,12 +415,12 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType)
 			case PossiblePlayerBattleAction::MOVE_STACK:
 				if (owner.stacksController->getActiveStack()->hasBonusOfType(Bonus::FLYING))
 				{
-					cursorFrame = ECursor::COMBAT_FLY;
+					cursorFrame = Cursor::Combat::FLY;
 					newConsoleMsg = (boost::format(CGI->generaltexth->allTexts[295]) % owner.stacksController->getActiveStack()->getName()).str(); //Fly %s here
 				}
 				else
 				{
-					cursorFrame = ECursor::COMBAT_MOVE;
+					cursorFrame = Cursor::Combat::MOVE;
 					newConsoleMsg = (boost::format(CGI->generaltexth->allTexts[294]) % owner.stacksController->getActiveStack()->getName()).str(); //Move %s here
 				}
 
@@ -464,7 +445,7 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType)
 			case PossiblePlayerBattleAction::WALK_AND_ATTACK:
 			case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
 				{
-					owner.fieldController->setBattleCursor(myNumber); //handle direction of cursor and attackable tile
+					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;
@@ -487,9 +468,9 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType)
 			case PossiblePlayerBattleAction::SHOOT:
 			{
 				if (owner.curInt->cb->battleHasShootingPenalty(owner.stacksController->getActiveStack(), myNumber))
-					cursorFrame = ECursor::COMBAT_SHOOT_PENALTY;
+					cursorFrame = Cursor::Combat::SHOOT_PENALTY;
 				else
-					cursorFrame = ECursor::COMBAT_SHOOT;
+					cursorFrame = Cursor::Combat::SHOOT;
 
 				realizeAction = [=](){owner.giveCommand(EActionType::SHOOT, myNumber);};
 				TDmgRange damage = owner.curInt->cb->battleEstimateDamage(owner.stacksController->getActiveStack(), shere);
@@ -524,7 +505,7 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType)
 				break;
 			case PossiblePlayerBattleAction::TELEPORT:
 				newConsoleMsg = CGI->generaltexth->allTexts[25]; //Teleport Here
-				cursorFrame = ECursor::COMBAT_TELEPORT;
+				cursorFrame = Cursor::Combat::TELEPORT;
 				isCastingPossible = true;
 				break;
 			case PossiblePlayerBattleAction::OBSTACLE:
@@ -534,7 +515,7 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType)
 				break;
 			case PossiblePlayerBattleAction::SACRIFICE:
 				newConsoleMsg = (boost::format(CGI->generaltexth->allTexts[549]) % shere->getName()).str(); //sacrifice the %s
-				cursorFrame = ECursor::COMBAT_SACRIFICE;
+				cursorFrame = Cursor::Combat::SACRIFICE;
 				isCastingPossible = true;
 				break;
 			case PossiblePlayerBattleAction::FREE_LOCATION:
@@ -542,24 +523,17 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType)
 				isCastingPossible = true;
 				break;
 			case PossiblePlayerBattleAction::HEAL:
-				cursorFrame = ECursor::COMBAT_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::RISE_DEMONS:
-				cursorType = ECursor::SPELLBOOK;
-				realizeAction = [=]()
-				{
-					owner.giveCommand(EActionType::DAEMON_SUMMONING, myNumber);
-				};
-				break;
 			case PossiblePlayerBattleAction::CATAPULT:
-				cursorFrame = ECursor::COMBAT_SHOOT_CATAPULT;
+				cursorFrame = Cursor::Combat::SHOOT_CATAPULT;
 				realizeAction = [=](){ owner.giveCommand(EActionType::CATAPULT, myNumber); };
 				break;
 			case PossiblePlayerBattleAction::CREATURE_INFO:
 			{
-				cursorFrame = ECursor::COMBAT_QUERY;
+				cursorFrame = Cursor::Combat::QUERY;
 				newConsoleMsg = (boost::format(CGI->generaltexth->allTexts[297]) % shere->getName()).str();
 				realizeAction = [=](){ GH.pushIntT<CStackWindow>(shere, false); };
 				break;
@@ -572,25 +546,25 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType)
 		{
 			case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
 			case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
-				cursorFrame = ECursor::COMBAT_BLOCKED;
+				cursorFrame = Cursor::Combat::BLOCKED;
 				newConsoleMsg = CGI->generaltexth->allTexts[23];
 				break;
 			case PossiblePlayerBattleAction::TELEPORT:
-				cursorFrame = ECursor::COMBAT_BLOCKED;
+				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 = ECursor::COMBAT_BLOCKED;
+				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->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); //set neutral cursor over menu etc.
+					CCS->curh->set(Cursor::Combat::POINTER);
 				else
-					cursorFrame = ECursor::COMBAT_BLOCKED;
+					cursorFrame = Cursor::Combat::BLOCKED;
 				break;
 		}
 	}
@@ -603,8 +577,7 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType)
 			case PossiblePlayerBattleAction::SACRIFICE:
 				break;
 			default:
-				cursorType = ECursor::SPELLBOOK;
-				cursorFrame = 0;
+				spellcastingCursor = true;
 				if (newConsoleMsg.empty() && currentSpell)
 					newConsoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % currentSpell->name); //Cast %s
 				break;
@@ -665,12 +638,17 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType)
 		if (eventType == CIntObject::MOVE)
 		{
 			if (setCursor)
-				CCS->curh->changeGraphic(cursorType, cursorFrame);
+			{
+				if (spellcastingCursor)
+					CCS->curh->set(Cursor::Spellcast::SPELL);
+				else
+					CCS->curh->set(cursorFrame);
+			}
 
 			if (!currentConsoleMsg.empty())
-				owner.controlPanel->console->clearIfMatching(currentConsoleMsg);
+				GH.statusbar->clearIfMatching(currentConsoleMsg);
 			if (!newConsoleMsg.empty())
-				owner.controlPanel->console->write(newConsoleMsg);
+				GH.statusbar->write(newConsoleMsg);
 
 			currentConsoleMsg = newConsoleMsg;
 		}
@@ -683,8 +661,8 @@ void BattleActionsController::handleHex(BattleHex myNumber, int eventType)
 			}
 			realizeAction();
 			if (!secondaryTarget) //do not replace teleport or sacrifice cursor
-				CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER);
-			owner.controlPanel->console->clear();
+				CCS->curh->set(Cursor::Combat::POINTER);
+			GH.statusbar->clear();
 		}
 	}
 }
@@ -759,7 +737,30 @@ 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
@@ -773,3 +774,18 @@ SpellID BattleActionsController::selectedSpell() const
 		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);
+}

+ 7 - 0
client/battle/BattleActionsController.h

@@ -92,5 +92,12 @@ public:
 
 	/// 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);
 
 };

Fișier diff suprimat deoarece este prea mare
+ 297 - 384
client/battle/BattleAnimationClasses.cpp


+ 150 - 133
client/battle/BattleAnimationClasses.h

@@ -10,36 +10,39 @@
 #pragma once
 
 #include "../../lib/battle/BattleHex.h"
-#include "../../lib/CSoundBase.h"
-#include "../widgets/Images.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;
-class CBattleAnimation;
-struct CatapultProjectileInfo;
 struct StackAttackedInfo;
+struct Point;
 
 /// Base class of battle animations
-class CBattleAnimation
+class BattleAnimation
 {
-
 protected:
 	BattleInterface & owner;
 	bool initialized;
 
-	std::vector<CBattleAnimation *> & pendingAnimations();
+	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
-	bool checkInitialConditions(); //determines if this animation is earliest of all
 
 public:
 	ui32 ID; //unique identifier
@@ -47,107 +50,101 @@ public:
 	bool isInitialized();
 	bool tryInitialize();
 	virtual void nextFrame() {} //call every new frame
-	virtual ~CBattleAnimation();
+	virtual ~BattleAnimation();
 
-	CBattleAnimation(BattleInterface & owner);
+	BattleAnimation(BattleInterface & owner);
 };
 
 /// Sub-class which is responsible for managing the battle stack animation.
-class CBattleStackAnimation : public CBattleAnimation
+class BattleStackAnimation : public BattleAnimation
 {
 public:
-	std::shared_ptr<CreatureAnimation> myAnim; //animation for our stack, managed by CBattleInterface
+	std::shared_ptr<CreatureAnimation> myAnim; //animation for our stack, managed by BattleInterface
 	const CStack * stack; //id of stack whose animation it is
 
-	CBattleStackAnimation(BattleInterface & owner, const CStack * _stack);
-
-	void shiftColor(const ColorShifter * shifter);
+	BattleStackAnimation(BattleInterface & owner, const CStack * _stack);
 	void rotateStack(BattleHex hex);
 };
 
-/// This class is responsible for managing the battle attack animation
-class CAttackAnimation : public CBattleStackAnimation
+class StackActionAnimation : public BattleStackAnimation
 {
-	bool soundPlayed;
+	ECreatureAnimType nextGroup;
+	ECreatureAnimType currGroup;
+	std::string sound;
+public:
+	void setNextGroup( ECreatureAnimType group );
+	void setGroup( ECreatureAnimType group );
+	void setSound( std::string sound );
 
-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
+	ECreatureAnimType getGroup() const;
 
-	const CCreature * getCreature() const;
-public:
-	void nextFrame() override;
-	bool checkInitialConditions();
+	StackActionAnimation(BattleInterface & owner, const CStack * _stack);
+	~StackActionAnimation();
 
-	CAttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender);
-	~CAttackAnimation();
+	bool init() override;
 };
 
 /// Animation of a defending unit
-class CDefenceAnimation : public CBattleStackAnimation
+class DefenceAnimation : public StackActionAnimation
 {
-	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;
-
-	CDefenceAnimation(StackAttackedInfo _attackedInfo, BattleInterface & owner);
-	~CDefenceAnimation();
+	DefenceAnimation(BattleInterface & owner, const CStack * stack);
 };
 
-class CDummyAnimation : public CBattleAnimation
+/// Animation of a hit unit
+class HittedAnimation : public StackActionAnimation
 {
-private:
-	int counter;
-	int howMany;
 public:
-	bool init() override;
-	void nextFrame() override;
+	HittedAnimation(BattleInterface & owner, const CStack * stack);
+};
 
-	CDummyAnimation(BattleInterface & owner, int howManyFrames);
+/// Animation of a dying unit
+class DeathAnimation : public StackActionAnimation
+{
+public:
+	DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged);
 };
 
-/// Hand-to-hand attack
-class CMeleeAttackAnimation : public CAttackAnimation
+/// 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;
 
-	CMeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked);
+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 CStackMoveAnimation : public CBattleStackAnimation
+class StackMoveAnimation : public BattleStackAnimation
 {
 public:
-	BattleHex currentHex;
+	BattleHex nextHex;
+	BattleHex prevHex;
 
 protected:
-	CStackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex _currentHex);
+	StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex);
 };
 
 /// Move animation of a creature
-class CMovementAnimation : public CStackMoveAnimation
+class MovementAnimation : public StackMoveAnimation
 {
 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
 
@@ -158,45 +155,73 @@ public:
 	bool init() override;
 	void nextFrame() override;
 
-	CMovementAnimation(BattleInterface & owner, const CStack *_stack, std::vector<BattleHex> _destTiles, int _distance);
-	~CMovementAnimation();
+	MovementAnimation(BattleInterface & owner, const CStack *_stack, std::vector<BattleHex> _destTiles, int _distance);
+	~MovementAnimation();
 };
 
 /// Move end animation of a creature
-class CMovementEndAnimation : public CStackMoveAnimation
+class MovementEndAnimation : public StackMoveAnimation
 {
 public:
 	bool init() override;
 
-	CMovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile);
-	~CMovementEndAnimation();
+	MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile);
+	~MovementEndAnimation();
 };
 
 /// Move start animation of a creature
-class CMovementStartAnimation : public CStackMoveAnimation
+class MovementStartAnimation : public StackMoveAnimation
 {
 public:
 	bool init() override;
 
-	CMovementStartAnimation(BattleInterface & owner, const CStack * _stack);
+	MovementStartAnimation(BattleInterface & owner, const CStack * _stack);
 };
 
 /// Class responsible for animation of stack chaning direction (left <-> right)
-class CReverseAnimation : public CStackMoveAnimation
+class ReverseAnimation : public StackMoveAnimation
 {
+	void setupSecondPart();
 public:
-	bool priority; //true - high, false - low
 	bool init() override;
 
-	void setupSecondPart();
+	ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest);
+};
 
-	CReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest, bool _priority);
-	~CReverseAnimation();
+/// 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);
 };
 
-class CRangedAttackAnimation : public CAttackAnimation
+/// 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();
@@ -205,85 +230,81 @@ class CRangedAttackAnimation : public CAttackAnimation
 protected:
 	bool projectileEmitted;
 
-	virtual CCreatureAnim::EAnimType getUpwardsGroup() const = 0;
-	virtual CCreatureAnim::EAnimType getForwardGroup() const = 0;
-	virtual CCreatureAnim::EAnimType getDownwardsGroup() const = 0;
+	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:
-	CRangedAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender);
-	~CRangedAttackAnimation();
+	RangedAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender);
+	~RangedAttackAnimation();
 
 	bool init() override;
 	void nextFrame() override;
 };
 
 /// Shooting attack
-class CShootingAnimation : public CRangedAttackAnimation
+class ShootingAnimation : public RangedAttackAnimation
 {
-	CCreatureAnim::EAnimType getUpwardsGroup() const override;
-	CCreatureAnim::EAnimType getForwardGroup() const override;
-	CCreatureAnim::EAnimType getDownwardsGroup() const override;
+	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:
-	CShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender);
+	ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender);
 
 };
 
 /// Catapult attack
-class CCatapultAnimation : public CShootingAnimation
+class CatapultAnimation : public ShootingAnimation
 {
 private:
 	bool explosionEmitted;
 	int catapultDamage;
 
 public:
-	CCatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender, int _catapultDmg = 0);
+	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 CCastAnimation : public CRangedAttackAnimation
+class CastAnimation : public RangedAttackAnimation
 {
 	const CSpell * spell;
 
-	CCreatureAnim::EAnimType findValidGroup( const std::vector<CCreatureAnim::EAnimType> candidates ) const;
-	CCreatureAnim::EAnimType getUpwardsGroup() const override;
-	CCreatureAnim::EAnimType getForwardGroup() const override;
-	CCreatureAnim::EAnimType getDownwardsGroup() const override;
+	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:
-	CCastAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest_, const CStack * defender, const CSpell * spell);
+	CastAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest_, const CStack * defender, const CSpell * spell);
 };
 
-struct CPointEffectParameters
+class DummyAnimation : public BattleAnimation
 {
-	std::vector<Point> positions;
-	std::vector<BattleHex> tiles;
-	std::string animation;
-
-	soundBase::soundID sound = soundBase::invalid;
-	BattleHex boundHex = BattleHex::INVALID;
-	bool aligntoBottom = false;
-	bool waitForSound = false;
-	bool screenFill = false;
+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 CPointEffectAnimation : public CBattleAnimation
+class EffectAnimation : public BattleAnimation
 {
-	soundBase::soundID sound;
-	bool soundPlayed;
-	bool soundFinished;
+	std::string soundName;
 	bool effectFinished;
 	int effectFlags;
 
@@ -297,54 +318,50 @@ class CPointEffectAnimation : public CBattleAnimation
 	bool screenFill() const;
 
 	void onEffectFinished();
-	void onSoundFinished();
 	void clearEffect();
-
-	void playSound();
 	void playEffect();
 
 public:
 	enum EEffectFlags
 	{
 		ALIGN_TO_BOTTOM = 1,
-		WAIT_FOR_SOUND  = 2,
-		FORCE_ON_TOP    = 4,
-		SCREEN_FILL     = 8,
+		FORCE_ON_TOP    = 2,
+		SCREEN_FILL     = 4,
 	};
 
 	/// Create animation with screen-wide effect
-	CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, int effects = 0);
+	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
-	CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, Point pos                 , int effects = 0);
-	CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, std::vector<Point> pos    , int effects = 0);
+	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)
-	CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, BattleHex hex             , int effects = 0);
-	CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, std::vector<BattleHex> hex, int effects = 0);
+	EffectAnimation(BattleInterface & owner, std::string animationName, BattleHex hex             , int effects = 0);
+	EffectAnimation(BattleInterface & owner, std::string animationName, std::vector<BattleHex> hex, int effects = 0);
 
-	CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, Point pos, BattleHex hex,   int effects = 0);
-	 ~CPointEffectAnimation();
+	EffectAnimation(BattleInterface & owner, std::string animationName, Point pos, BattleHex hex,   int effects = 0);
+	 ~EffectAnimation();
 
 	bool init() override;
 	void nextFrame() override;
 };
 
-/// Base class (e.g. for use in dynamic_cast's) for "animations" that wait for certain event
-class CWaitingAnimation : public CBattleAnimation
+class HeroCastAnimation : public BattleAnimation
 {
-protected:
-	CWaitingAnimation(BattleInterface & owner);
-public:
-	void nextFrame() override;
-};
+	std::shared_ptr<BattleHero> hero;
+	const CStack * target;
+	const CSpell * spell;
+	BattleHex tile;
+	bool projectileEmitted;
+
+	void initializeProjectile();
+	void emitProjectile();
+	void emitAnimationEvent();
 
-/// Class that waits till projectile of certain shooter hits a target
-class CWaitingProjectileAnimation : public CWaitingAnimation
-{
-	const CStack * shooter;
 public:
-	CWaitingProjectileAnimation(BattleInterface & owner, const CStack * shooter);
+	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
+};

+ 0 - 328
client/battle/BattleControlPanel.cpp

@@ -1,328 +0,0 @@
-/*
- * BattleControlPanel.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 "BattleControlPanel.h"
-
-#include "BattleInterface.h"
-#include "BattleInterfaceClasses.h"
-#include "BattleStacksController.h"
-#include "BattleActionsController.h"
-
-#include "../CGameInfo.h"
-#include "../CPlayerInterface.h"
-#include "../gui/CCursorHandler.h"
-#include "../gui/CGuiHandler.h"
-#include "../windows/CSpellWindow.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"
-
-BattleControlPanel::BattleControlPanel(BattleInterface & owner, const Point & position):
-	owner(owner)
-{
-	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-	pos += position;
-
-	//preparing buttons and console
-	bOptions = std::make_shared<CButton>    (Point(  3,  5), "icm003.def", CGI->generaltexth->zelp[381], std::bind(&BattleControlPanel::bOptionsf,this), SDLK_o);
-	bSurrender = std::make_shared<CButton>  (Point( 54,  5), "icm001.def", CGI->generaltexth->zelp[379], std::bind(&BattleControlPanel::bSurrenderf,this), SDLK_s);
-	bFlee = std::make_shared<CButton>       (Point(105,  5), "icm002.def", CGI->generaltexth->zelp[380], std::bind(&BattleControlPanel::bFleef,this), SDLK_r);
-	bAutofight = std::make_shared<CButton>  (Point(157,  5), "icm004.def", CGI->generaltexth->zelp[382], std::bind(&BattleControlPanel::bAutofightf,this), SDLK_a);
-	bSpell = std::make_shared<CButton>      (Point(645,  5), "icm005.def", CGI->generaltexth->zelp[385], std::bind(&BattleControlPanel::bSpellf,this), SDLK_c);
-	bWait = std::make_shared<CButton>       (Point(696,  5), "icm006.def", CGI->generaltexth->zelp[386], std::bind(&BattleControlPanel::bWaitf,this), SDLK_w);
-	bDefence = std::make_shared<CButton>    (Point(747,  5), "icm007.def", CGI->generaltexth->zelp[387], std::bind(&BattleControlPanel::bDefencef,this), SDLK_d);
-	bConsoleUp = std::make_shared<CButton>  (Point(624,  5), "ComSlide.def", std::make_pair("", ""),     std::bind(&BattleControlPanel::bConsoleUpf,this), SDLK_UP);
-	bConsoleDown = std::make_shared<CButton>(Point(624, 24), "ComSlide.def", std::make_pair("", ""),     std::bind(&BattleControlPanel::bConsoleDownf,this), SDLK_DOWN);
-
-	bDefence->assignedKeys.insert(SDLK_SPACE);
-	bConsoleUp->setImageOrder(0, 1, 0, 0);
-	bConsoleDown->setImageOrder(2, 3, 2, 2);
-
-	console = std::make_shared<BattleConsole>(Rect(211, 4, 406,38));
-	GH.statusbar = console;
-
-	if ( owner.tacticsMode )
-		tacticPhaseStarted();
-	else
-		tacticPhaseEnded();
-}
-
-void BattleControlPanel::show(SDL_Surface * to)
-{
-	//show menu before all other elements to keep it in background
-	menu->show(to);
-	CIntObject::show(to);
-}
-
-void BattleControlPanel::showAll(SDL_Surface * to)
-{
-	//show menu before all other elements to keep it in background
-	menu->showAll(to);
-	CIntObject::showAll(to);
-}
-
-
-void BattleControlPanel::tacticPhaseStarted()
-{
-	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-
-	btactNext = std::make_shared<CButton>(Point(213, 4), "icm011.def", std::make_pair("", ""), [&]() { bTacticNextStack();}, SDLK_SPACE);
-	btactEnd = std::make_shared<CButton>(Point(419,  4), "icm012.def", std::make_pair("", ""),  [&](){ bTacticPhaseEnd();}, SDLK_RETURN);
-	menu = std::make_shared<CPicture>("COPLACBR.BMP", 0, 0);
-	menu->colorize(owner.curInt->playerID);
-	menu->recActions &= ~(SHOWALL | UPDATE);
-}
-void BattleControlPanel::tacticPhaseEnded()
-{
-	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-
-	btactNext.reset();
-	btactEnd.reset();
-
-	menu = std::make_shared<CPicture>("CBAR.BMP", 0, 0);
-	menu->colorize(owner.curInt->playerID);
-	menu->recActions &= ~(SHOWALL | UPDATE);
-}
-
-void BattleControlPanel::bOptionsf()
-{
-	if (owner.actionsController->spellcastingModeActive())
-		return;
-
-	CCS->curh->changeGraphic(ECursor::ADVENTURE,0);
-
-	GH.pushIntT<BattleOptionsWindow>(owner);
-}
-
-void BattleControlPanel::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 BattleControlPanel::bFleef()
-{
-	if (owner.actionsController->spellcastingModeActive())
-		return;
-
-	if ( owner.curInt->cb->battleCanFlee() )
-	{
-		CFunctionList<void()> ony = std::bind(&BattleControlPanel::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 BattleControlPanel::reallyFlee()
-{
-	owner.giveCommand(EActionType::RETREAT);
-	CCS->curh->changeGraphic(ECursor::ADVENTURE, 0);
-}
-
-void BattleControlPanel::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->changeGraphic(ECursor::ADVENTURE, 0);
-	}
-}
-
-void BattleControlPanel::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->init(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 BattleControlPanel::bSpellf()
-{
-	if (owner.actionsController->spellcastingModeActive())
-		return;
-
-	if (!owner.myTurn)
-		return;
-
-	auto myHero = owner.currentHero();
-	if(!myHero)
-		return;
-
-	CCS->curh->changeGraphic(ECursor::ADVENTURE,0);
-
-	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 BattleControlPanel::bWaitf()
-{
-	if (owner.actionsController->spellcastingModeActive())
-		return;
-
-	if (owner.stacksController->getActiveStack() != nullptr)
-		owner.giveCommand(EActionType::WAIT);
-}
-
-void BattleControlPanel::bDefencef()
-{
-	if (owner.actionsController->spellcastingModeActive())
-		return;
-
-	if (owner.stacksController->getActiveStack() != nullptr)
-		owner.giveCommand(EActionType::DEFEND);
-}
-
-void BattleControlPanel::bConsoleUpf()
-{
-	if (owner.actionsController->spellcastingModeActive())
-		return;
-
-	console->scrollUp();
-}
-
-void BattleControlPanel::bConsoleDownf()
-{
-	if (owner.actionsController->spellcastingModeActive())
-		return;
-
-	console->scrollDown();
-}
-
-void BattleControlPanel::bTacticNextStack()
-{
-	owner.tacticNextStack(nullptr);
-}
-
-void BattleControlPanel::bTacticPhaseEnd()
-{
-	owner.tacticPhaseEnd();
-}
-
-void BattleControlPanel::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;
-
-	bOptions->block(on);
-	bFlee->block(on || !owner.curInt->cb->battleCanFlee());
-	bSurrender->block(on || owner.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(!owner.curInt->isAutoFightOn && !owner.stacksController->getActiveStack());
-
-	if (owner.tacticsMode && btactEnd && btactNext)
-	{
-		btactNext->block(on);
-		btactEnd->block(on);
-	}
-	else
-	{
-		bConsoleUp->block(on);
-		bConsoleDown->block(on);
-	}
-
-
-	bSpell->block(on || owner.tacticsMode || !canCastSpells);
-	bWait->block(on || owner.tacticsMode || !canWait);
-	bDefence->block(on || owner.tacticsMode);
-}

+ 0 - 76
client/battle/BattleControlPanel.h

@@ -1,76 +0,0 @@
-/*
- * BattleControlPanel.h, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#pragma once
-
-#include "../gui/CIntObject.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-class CStack;
-
-VCMI_LIB_NAMESPACE_END
-
-class CButton;
-class BattleInterface;
-class BattleConsole;
-
-/// GUI object that handles functionality of panel at the bottom of combat screen
-class BattleControlPanel : public CIntObject
-{
-	BattleInterface & owner;
-
-	std::shared_ptr<CPicture> menu;
-
-	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;
-
-	/// button press handling functions
-	void bOptionsf();
-	void bSurrenderf();
-	void bFleef();
-	void bAutofightf();
-	void bSpellf();
-	void bWaitf();
-	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();
-
-public:
-	std::shared_ptr<BattleConsole> console;
-
-	/// block all UI elements when player is not allowed to act, e.g. during enemy turn
-	void blockUI(bool on);
-
-	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();
-
-	BattleControlPanel(BattleInterface & owner, const Point & position);
-};
-

+ 59 - 31
client/battle/BattleEffectsController.cpp

@@ -11,7 +11,7 @@
 #include "BattleEffectsController.h"
 
 #include "BattleAnimationClasses.h"
-#include "BattleControlPanel.h"
+#include "BattleWindow.h"
 #include "BattleInterface.h"
 #include "BattleInterfaceClasses.h"
 #include "BattleFieldController.h"
@@ -26,6 +26,7 @@
 
 #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"
@@ -33,35 +34,30 @@
 
 BattleEffectsController::BattleEffectsController(BattleInterface & owner):
 	owner(owner)
-{}
-
-void BattleEffectsController::displayEffect(EBattleEffect::EBattleEffect effect, const BattleHex & destTile)
 {
-	displayEffect(effect, soundBase::invalid, destTile);
+	loadColorMuxers();
 }
 
-void BattleEffectsController::displayEffect(EBattleEffect::EBattleEffect effect, uint32_t soundID, const BattleHex & destTile)
+void BattleEffectsController::displayEffect(EBattleEffect effect, const BattleHex & destTile)
 {
-	std::string customAnim = graphics->battleACToDef[effect][0];
-
-	owner.stacksController->addNewAnim(new CPointEffectAnimation(owner, soundBase::soundID(soundID), customAnim, destTile));
+	displayEffect(effect, "", destTile);
 }
 
-void BattleEffectsController::displayCustomEffects(const std::vector<CustomEffectInfo> & customEffects)
+void BattleEffectsController::displayEffect(EBattleEffect effect, std::string soundFile, const BattleHex & destTile)
 {
-	for(const CustomEffectInfo & one : customEffects)
-	{
-		const CStack * s = owner.curInt->cb->battleGetStackByID(one.stack, false);
+	size_t effectID = static_cast<size_t>(effect);
 
-		assert(s);
-		assert(one.effect != 0);
+	std::string customAnim = graphics->battleACToDef[effectID][0];
 
-		displayEffect(EBattleEffect::EBattleEffect(one.effect), soundBase::soundID(one.sound), s->getPosition());
-	}
+	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)
 	{
@@ -71,45 +67,46 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt
 	//don't show animation when no HP is regenerated
 	switch(bte.effect)
 	{
-		//TODO: move to bonus type handler
 		case Bonus::HP_REGENERATION:
-			displayEffect(EBattleEffect::REGENERATION, soundBase::REGENER, stack->getPosition());
+			displayEffect(EBattleEffect::REGENERATION, "REGENER", stack->getPosition());
 			break;
 		case Bonus::MANA_DRAIN:
-			displayEffect(EBattleEffect::MANA_DRAIN, soundBase::MANADRAI, stack->getPosition());
+			displayEffect(EBattleEffect::MANA_DRAIN, "MANADRAI", stack->getPosition());
 			break;
 		case Bonus::POISON:
-			displayEffect(EBattleEffect::POISON, soundBase::POISON, stack->getPosition());
+			displayEffect(EBattleEffect::POISON, "POISON", stack->getPosition());
 			break;
 		case Bonus::FEAR:
-			displayEffect(EBattleEffect::FEAR, soundBase::FEAR, stack->getPosition());
+			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, soundBase::GOODMRLE, stack->getPosition());
-			owner.controlPanel->console->addText(hlp);
+			displayEffect(EBattleEffect::GOOD_MORALE, "GOODMRLE", stack->getPosition());
+			owner.appendBattleLog(hlp);
 			break;
 		}
 		default:
 			return;
 	}
-	//waitForAnims(); //fixme: freezes game :?
+	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.controlPanel->console->addText(stack->formatGeneralMessage(136));
+		owner.appendBattleLog(stack->formatGeneralMessage(136));
 		break;
 	case EActionType::BAD_MORALE:
-		owner.controlPanel->console->addText(stack->formatGeneralMessage(-34));
-		displayEffect(EBattleEffect::BAD_MORALE, soundBase::BADMRLE, stack->getPosition());
+		owner.appendBattleLog(stack->formatGeneralMessage(-34));
+		displayEffect(EBattleEffect::BAD_MORALE, "BADMRLE", stack->getPosition());
 		break;
 	}
 
@@ -118,23 +115,54 @@ void BattleEffectsController::startAction(const BattleAction* action)
 	switch(action->actionType)
 	{
 		case EActionType::STACK_HEAL:
-			displayEffect(EBattleEffect::REGENERATION, soundBase::REGENER, actionTarget.at(0).hexValue);
+			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.position, [&elem](BattleRenderer::RendererRef canvas)
+		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, Point(elem.x, elem.y));
+			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;
+}

+ 14 - 31
client/battle/BattleEffectsController.h

@@ -10,50 +10,31 @@
 #pragma once
 
 #include "../../lib/battle/BattleHex.h"
+#include "../gui/Geometries.h"
+#include "BattleConstants.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
 class BattleAction;
-struct CustomEffectInfo;
 struct BattleTriggerEffect;
 
 VCMI_LIB_NAMESPACE_END
 
+struct ColorMuxerEffect;
 class CAnimation;
 class Canvas;
 class BattleInterface;
 class BattleRenderer;
-class CPointEffectAnimation;
-
-namespace EBattleEffect
-{
-	enum EBattleEffect
-	{
-		// list of battle effects that have hardcoded triggers
-		FEAR         = 15,
-		GOOD_LUCK    = 18,
-		GOOD_MORALE  = 20,
-		BAD_MORALE   = 30,
-		BAD_LUCK     = 48,
-		RESURRECT    = 50,
-		DRAIN_LIFE   = 52, // hardcoded constant in CGameHandler
-		POISON       = 67,
-		DEATH_BLOW   = 73,
-		REGENERATION = 74,
-		MANA_DRAIN   = 77,
-
-		INVALID      = -1,
-	};
-}
+class EffectAnimation;
 
 /// Struct for battle effect animation e.g. morale, prayer, armageddon, bless,...
 struct BattleEffect
 {
-	int x, y; //position on the screen
+	Point pos; //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
+	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
@@ -64,21 +45,23 @@ class BattleEffectsController
 	/// 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);
 
-	void displayCustomEffects(const std::vector<CustomEffectInfo> & customEffects);
-
 	//displays custom effect on the battlefield
-	void displayEffect(EBattleEffect::EBattleEffect effect, const BattleHex & destTile);
-	void displayEffect(EBattleEffect::EBattleEffect effect, uint32_t soundID, const BattleHex & destTile);
-	//void displayEffects(EBattleEffect::EBattleEffect effect, uint32_t soundID, const std::vector<BattleHex> & destTiles);
+	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 CPointEffectAnimation;
+	friend class EffectAnimation;
 };

+ 231 - 309
client/battle/BattleFieldController.cpp

@@ -22,10 +22,11 @@
 
 #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 "../gui/CursorHandler.h"
 
 #include "../../CCallback.h"
 #include "../../lib/BattleFieldHandler.h"
@@ -34,12 +35,10 @@
 #include "../../lib/spells/ISpellMechanics.h"
 
 BattleFieldController::BattleFieldController(BattleInterface & owner):
-	owner(owner),
-	attackingHex(BattleHex::INVALID)
+	owner(owner)
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-	pos.w = owner.pos.w;
-	pos.h = owner.pos.h;
+	strongInterest = true;
 
 	//preparing cells and hexes
 	cellBorder = IImage::createFromFile("CCELLGRD.BMP");
@@ -59,6 +58,8 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 		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()));
@@ -87,17 +88,42 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 	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)
 {
-	showBackground(canvas);
+	Canvas clippedCanvas(canvas, pos);
+
+	showBackground(clippedCanvas);
 
 	BattleRenderer renderer(owner);
 
-	renderer.execute(canvas);
+	renderer.execute(clippedCanvas);
 
-	owner.projectilesController->showProjectiles(canvas);
+	owner.projectilesController->showProjectiles(clippedCanvas);
 }
 
 void BattleFieldController::showBackground(Canvas & canvas)
@@ -113,19 +139,19 @@ void BattleFieldController::showBackground(Canvas & canvas)
 
 void BattleFieldController::showBackgroundImage(Canvas & canvas)
 {
-	canvas.draw(background, owner.pos.topLeft());
+	canvas.draw(background, Point(0, 0));
 
-	owner.obstacleController->showAbsoluteObstacles(canvas, pos.topLeft());
+	owner.obstacleController->showAbsoluteObstacles(canvas);
 	if ( owner.siegeController )
-		owner.siegeController->showAbsoluteObstacles(canvas, pos.topLeft());
+		owner.siegeController->showAbsoluteObstacles(canvas);
 
 	if (settings["battle"]["cellBorders"].Bool())
-		canvas.draw(*cellBorders, owner.pos.topLeft());
+		canvas.draw(*cellBorders, Point(0, 0));
 }
 
 void BattleFieldController::showBackgroundImageWithHexes(Canvas & canvas)
 {
-	canvas.draw(*backgroundWithHexes.get(), owner.pos.topLeft());
+	canvas.draw(*backgroundWithHexes.get(), Point(0, 0));
 }
 
 void BattleFieldController::redrawBackgroundWithHexes()
@@ -142,9 +168,9 @@ void BattleFieldController::redrawBackgroundWithHexes()
 
 	//prepare background graphic with hexes and shaded hexes
 	backgroundWithHexes->draw(background, Point(0,0));
-	owner.obstacleController->showAbsoluteObstacles(*backgroundWithHexes, Point(0,0));
+	owner.obstacleController->showAbsoluteObstacles(*backgroundWithHexes);
 	if ( owner.siegeController )
-		owner.siegeController->showAbsoluteObstacles(*backgroundWithHexes, Point(0,0));
+		owner.siegeController->showAbsoluteObstacles(*backgroundWithHexes);
 
 	if (settings["battle"]["stackRange"].Bool())
 	{
@@ -162,7 +188,7 @@ void BattleFieldController::redrawBackgroundWithHexes()
 
 void BattleFieldController::showHighlightedHex(Canvas & canvas, BattleHex hex, bool darkBorder)
 {
-	Point hexPos = hexPositionAbsolute(hex).topLeft();
+	Point hexPos = hexPositionLocal(hex).topLeft();
 
 	canvas.draw(cellShade, hexPos);
 	if(!darkBorder && settings["battle"]["cellBorders"].Bool())
@@ -233,19 +259,58 @@ std::set<BattleHex> BattleFieldController::getHighlightedHexesSpellRange()
 				result.insert(shadedHex);
 		}
 	}
-	else if(owner.active) //always highlight pointed hex
+	return result;
+}
+
+std::set<BattleHex> BattleFieldController::getHighlightedHexesMovementTarget()
+{
+	const CStack * stack = owner.stacksController->getActiveStack();
+	auto hoveredHex = getHoveredHex();
+
+	if (stack)
 	{
-		if(hoveredHex.getX() != 0 && hoveredHex.getX() != GameConstants::BFIELD_WIDTH - 1)
-			result.insert(hoveredHex);
-	}
+		std::vector<BattleHex> v = owner.curInt->cb->battleGetAvailableHexes(stack, false, nullptr);
 
-	return result;
+		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> hoveredMouse = getHighlightedHexesSpellRange();
+	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)
 	{
@@ -280,7 +345,7 @@ Rect BattleFieldController::hexPositionLocal(BattleHex hex) const
 
 Rect BattleFieldController::hexPositionAbsolute(BattleHex hex) const
 {
-	return hexPositionLocal(hex) + owner.pos.topLeft();
+	return hexPositionLocal(hex) + pos.topLeft();
 }
 
 bool BattleFieldController::isPixelInHex(Point const & position)
@@ -299,323 +364,165 @@ BattleHex BattleFieldController::getHoveredHex()
 
 void BattleFieldController::setBattleCursor(BattleHex myNumber)
 {
-	Rect hoveredHexPos = hexPositionAbsolute(myNumber);
-	CCursorHandler *cursor = CCS->curh;
-
-	const double subdividingAngle = 2.0*M_PI/6.0; // Divide a hex into six sectors.
-	const double hexMidX = hoveredHexPos.x + hoveredHexPos.w/2.0;
-	const double hexMidY = hoveredHexPos.y + hoveredHexPos.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);
+	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();
-	bool aboveAttackable = true, belowAttackable = true;
+	auto neighbours = myNumber.allNeighbouringTiles();
+	//   0 1
+	//  5 x 2
+	//   4 3
 
-	// 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;
-		}
-	}
+	// if true - our current stack can move into this hex (and attack)
+	std::array<bool, 8> attackAvailability;
 
-	// 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;
+		// 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
 	{
-		cursorIndex = static_cast<int>(sector);
-	}
+		for (size_t i = 0; i < 6; ++i)
+			attackAvailability[i] = vstd::contains(occupyableHexes, neighbours[i]);
 
-	// 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;
+		attackAvailability[6] = false;
+		attackAvailability[7] = false;
 	}
 
-	// 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)
+	// Zero available tiles to attack from
+	if ( vstd::find(attackAvailability, true) == attackAvailability.end())
 	{
-		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;
+		logGlobal->error("Error: cannot find a hex to attack hex %d from!", myNumber);
+		return BattleHex::NONE;
 	}
-	BattleHex hex(attackingHex);
-	if (!hex.isValid())
-		attackingHex = -1;
+
+	// 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 myNumber)
+BattleHex BattleFieldController::fromWhichHexAttack(BattleHex attackTarget)
 {
-	//TODO far too much repeating code
-	BattleHex destHex;
-	switch(CCS->curh->frame)
+	BattleHex::EDir direction = selectAttackDirection(attackTarget, CCS->curh->position());
+
+	const CStack * attacker = owner.stacksController->getActiveStack();
+
+	assert(direction != BattleHex::NONE);
+	assert(attacker);
+
+	if (!attacker->doubleWide())
 	{
-	case 12: //from bottom right
-		{
-			bool doubleWide = owner.stacksController->getActiveStack()->doubleWide();
-			destHex = myNumber + ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH : GameConstants::BFIELD_WIDTH+1 ) +
-				(owner.stacksController->getActiveStack()->side == BattleSide::ATTACKER && doubleWide ? 1 : 0);
-			if(vstd::contains(occupyableHexes, destHex))
-				return destHex;
-			else if(owner.stacksController->getActiveStack()->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
+		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)
 		{
-			destHex = myNumber + ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH-1 : GameConstants::BFIELD_WIDTH );
-			if (vstd::contains(occupyableHexes, destHex))
-				return destHex;
-			else if(owner.stacksController->getActiveStack()->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
+		case BattleHex::TOP_LEFT:
+		case BattleHex::LEFT:
+		case BattleHex::BOTTOM_LEFT:
 		{
-			if(owner.stacksController->getActiveStack()->doubleWide() && owner.stacksController->getActiveStack()->side == BattleSide::DEFENDER)
-			{
-				std::vector<BattleHex> acc = owner.curInt->cb->battleGetAvailableHexes(owner.stacksController->getActiveStack());
-				if (vstd::contains(acc, myNumber))
-					return myNumber - 1;
-				else
-					return myNumber - 2;
-			}
+			if ( attacker->side == BattleSide::ATTACKER )
+				return attackTarget.cloneInDirection(direction);
 			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(owner.stacksController->getActiveStack()->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 attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::LEFT);
 		}
-	case 10: //from top right
+
+		case BattleHex::TOP_RIGHT:
+		case BattleHex::RIGHT:
+		case BattleHex::BOTTOM_RIGHT:
 		{
-			bool doubleWide = owner.stacksController->getActiveStack()->doubleWide();
-			destHex = myNumber - ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH : GameConstants::BFIELD_WIDTH-1 ) +
-				(owner.stacksController->getActiveStack()->side == BattleSide::ATTACKER && doubleWide ? 1 : 0);
-			if(vstd::contains(occupyableHexes, destHex))
-				return destHex;
-			else if(owner.stacksController->getActiveStack()->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;
+			if ( attacker->side == BattleSide::ATTACKER )
+				return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::RIGHT);
+			else
+				return attackTarget.cloneInDirection(direction);
 		}
-	case 11: //from right
+
+		case BattleHex::TOP:
 		{
-			if(owner.stacksController->getActiveStack()->doubleWide() && owner.stacksController->getActiveStack()->side == BattleSide::ATTACKER)
-			{
-				std::vector<BattleHex> acc = owner.curInt->cb->battleGetAvailableHexes(owner.stacksController->getActiveStack());
-				if(vstd::contains(acc, myNumber))
-					return myNumber + 1;
-				else
-					return myNumber + 2;
-			}
+			if ( attacker->side == BattleSide::ATTACKER )
+				return attackTarget.cloneInDirection(BattleHex::TOP_RIGHT);
 			else
-			{
-				return myNumber + 1;
-			}
-			break;
+				return attackTarget.cloneInDirection(BattleHex::TOP_LEFT);
 		}
-	case 13: //from bottom
+
+		case BattleHex::BOTTOM:
 		{
-			destHex = myNumber + ( (myNumber/GameConstants::BFIELD_WIDTH)%2 ? GameConstants::BFIELD_WIDTH : GameConstants::BFIELD_WIDTH+1 );
-			if(vstd::contains(occupyableHexes, destHex))
-				return destHex;
-			else if(owner.stacksController->getActiveStack()->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;
+			if ( attacker->side == BattleSide::ATTACKER )
+				return attackTarget.cloneInDirection(BattleHex::BOTTOM_RIGHT);
+			else
+				return attackTarget.cloneInDirection(BattleHex::BOTTOM_LEFT);
 		}
-	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(owner.stacksController->getActiveStack()->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;
+		default:
+			assert(0);
+			return attackTarget.cloneInDirection(BattleHex::LEFT);
 		}
 	}
-	return -1;
 }
 
 bool BattleFieldController::isTileAttackable(const BattleHex & number) const
@@ -632,3 +539,18 @@ 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);
+}

+ 10 - 0
client/battle/BattleFieldController.h

@@ -22,6 +22,7 @@ struct Rect;
 struct Point;
 
 class ClickableHex;
+class BattleHero;
 class Canvas;
 class IImage;
 class BattleInterface;
@@ -56,15 +57,24 @@ class BattleFieldController : public CIntObject
 
 	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);
 

+ 192 - 388
client/battle/BattleInterface.cpp

@@ -19,7 +19,7 @@
 #include "BattleObstacleController.h"
 #include "BattleSiegeController.h"
 #include "BattleFieldController.h"
-#include "BattleControlPanel.h"
+#include "BattleWindow.h"
 #include "BattleStacksController.h"
 #include "BattleRenderer.h"
 
@@ -28,7 +28,7 @@
 #include "../CMusicHandler.h"
 #include "../CPlayerInterface.h"
 #include "../gui/Canvas.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../windows/CAdvmapInterface.h"
 
@@ -42,18 +42,23 @@
 #include "../../lib/NetPacks.h"
 #include "../../lib/UnlockGuard.h"
 
-CondSh<bool> BattleInterface::animsAreDisplayed(false);
 CondSh<BattleAction *> BattleInterface::givenCommand(nullptr);
 
 BattleInterface::BattleInterface(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)
-	: attackingHeroInstance(hero1), defendingHeroInstance(hero2), animCount(0),
-	attackerInt(att), defenderInt(defen), curInt(att),
-	myTurn(false), moveStarted(false), moveSoundHander(-1), bresult(nullptr), battleActionsStarted(false)
-{
-	OBJ_CONSTRUCTION;
+		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)
 	{
@@ -65,9 +70,6 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *
 		curInt = defenderInt;
 	}
 
-	animsAreDisplayed.setn(false);
-	pos = myRect;
-	strongInterest = true;
 	givenCommand.setn(nullptr);
 
 	//hot-seat -> check tactics for both players (defender may be local human)
@@ -79,27 +81,6 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *
 	//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<StackQueue>(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));
-	}
-
-
-	CPlayerInterface::battleInt = this;
-
 	//initializing armies
 	this->army1 = army1;
 	this->army2 = army2;
@@ -108,84 +89,43 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *
 	if(town && town->hasFort())
 		siegeController.reset(new BattleSiegeController(*this, town));
 
-	controlPanel = std::make_shared<BattleControlPanel>(*this, Point(0, 556));
+	windowObject = std::make_shared<BattleWindow>(*this);
 	projectilesController.reset(new BattleProjectileController(*this));
-	fieldController.reset( new BattleFieldController(*this));
 	stacksController.reset( new BattleStacksController(*this));
 	actionsController.reset( new BattleActionsController(*this));
 	effectsController.reset(new BattleEffectsController(*this));
-
-	//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<BattleHero>(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<BattleHero>(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);
-	}
-
 	obstacleController.reset(new BattleObstacleController(*this));
 
-	if(tacticsMode)
-		tacticNextStack(nullptr);
-
 	CCS->musich->stopMusic();
+	setAnimationCondition(EAnimationEvents::OPENING, true);
 	battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds);
-	auto onIntroPlayed = [&]()
+	auto onIntroPlayed = [this]()
 	{
 		if(LOCPLINT->battleInt)
 		{
-			CCS->musich->playMusicFromSet("battle", true, true);
-			battleActionsStarted = true;
-			activateStack();
-			controlPanel->blockUI(settings["session"]["spectate"].Bool() || stacksController->getActiveStack() == nullptr);
-			battleIntroSoundChannel = -1;
+			boost::unique_lock<boost::recursive_mutex> un(*CPlayerInterface::pim);
+			onIntroSoundPlayed();
 		}
 	};
 
-	CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed);
+	GH.pushInt(windowObject);
+	windowObject->blockUI(true);
+	windowObject->updateQueue();
+
+	if (battleIntroSoundChannel != -1)
+		CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed);
+	else
+		onIntroSoundPlayed();
+}
 
-	addUsedEvents(RCLICK | MOVE | KEYBOARD);
-	controlPanel->blockUI(true);
-	queue->update();
+void BattleInterface::onIntroSoundPlayed()
+{
+	setAnimationCondition(EAnimationEvents::OPENING, false);
+	CCS->musich->playMusicFromSet("battle", true, true);
+	if(tacticsMode)
+		tacticNextStack(nullptr);
+	activateStack();
+	battleIntroSoundChannel = -1;
 }
 
 BattleInterface::~BattleInterface()
@@ -193,18 +133,17 @@ BattleInterface::~BattleInterface()
 	CPlayerInterface::battleInt = nullptr;
 	givenCommand.cond.notify_all(); //that two lines should make any stacksController->getActiveStack() waiting thread to finish
 
-	if (active) //dirty fix for #485
-	{
-		deactivate();
-	}
-
 	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);
 	}
-	animsAreDisplayed.setn(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)
@@ -231,84 +170,6 @@ void BattleInterface::setPrintMouseShadow(bool set)
 	shadow->Bool() = set;
 }
 
-void BattleInterface::activate()
-{
-	controlPanel->activate();
-
-	if (curInt->isAutoFightOn)
-		return;
-
-	CIntObject::activate();
-
-	if (attackingHero)
-		attackingHero->activate();
-	if (defendingHero)
-		defendingHero->activate();
-
-	fieldController->activate();
-
-	if (settings["battle"]["showQueue"].Bool())
-		queue->activate();
-
-	LOCPLINT->cingconsole->activate();
-}
-
-void BattleInterface::deactivate()
-{
-	controlPanel->deactivate();
-	CIntObject::deactivate();
-
-	fieldController->deactivate();
-
-	if (attackingHero)
-		attackingHero->deactivate();
-	if (defendingHero)
-		defendingHero->deactivate();
-	if (settings["battle"]["showQueue"].Bool())
-		queue->deactivate();
-
-	LOCPLINT->cingconsole->deactivate();
-}
-
-void BattleInterface::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)
-	{
-		actionsController->enterCreatureCastingMode();
-	}
-	else if(key.keysym.sym == SDLK_ESCAPE)
-	{
-		if(!battleActionsStarted)
-			CCS->soundh->stopSound(battleIntroSoundChannel);
-		else
-			actionsController->endCastingSpell();
-	}
-}
-void BattleInterface::mouseMoved(const SDL_MouseMotionEvent &event)
-{
-	BattleHex selectedHex = fieldController->getHoveredHex();
-
-	actionsController->handleHex(selectedHex, MOVE);
-
-	controlPanel->mouseMoved(event);
-}
-
-void BattleInterface::clickRight(tribool down, bool previousState)
-{
-	if (!down)
-	{
-		actionsController->endCastingSpell();
-	}
-}
-
 void BattleInterface::stackReset(const CStack * stack)
 {
 	stacksController->stackReset(stack);
@@ -316,24 +177,27 @@ void BattleInterface::stackReset(const CStack * stack)
 
 void BattleInterface::stackAdded(const CStack * stack)
 {
-	stacksController->stackAdded(stack);
+	stacksController->stackAdded(stack, false);
 }
 
 void BattleInterface::stackRemoved(uint32_t stackID)
 {
 	stacksController->stackRemoved(stackID);
 	fieldController->redrawBackgroundWithHexes();
-	queue->update();
+	windowObject->updateQueue();
 }
 
-void BattleInterface::stackActivated(const CStack *stack) //TODO: check it all before game state is changed due to abilities
+void BattleInterface::stackActivated(const CStack *stack)
 {
 	stacksController->stackActivated(stack);
 }
 
-void BattleInterface::stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance)
+void BattleInterface::stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance, bool teleport)
 {
-	stacksController->stackMoved(stack, destHex, distance);
+	if (teleport)
+		stacksController->stackTeleported(stack, destHex, distance);
+	else
+		stacksController->stackMoved(stack, destHex, distance);
 }
 
 void BattleInterface::stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos)
@@ -354,25 +218,25 @@ void BattleInterface::stacksAreAttacked(std::vector<StackAttackedInfo> attackedI
 	for(ui8 side = 0; side < 2; side++)
 	{
 		if(killedBySide.at(side) > killedBySide.at(1-side))
-			setHeroAnimation(side, CCreatureAnim::HERO_DEFEAT);
+			setHeroAnimation(side, EHeroAnimType::DEFEAT);
 		else if(killedBySide.at(side) < killedBySide.at(1-side))
-			setHeroAnimation(side, CCreatureAnim::HERO_VICTORY);
+			setHeroAnimation(side, EHeroAnimType::VICTORY);
 	}
 }
 
-void BattleInterface::stackAttacking( const CStack *attacker, BattleHex dest, const CStack *attacked, bool shooting )
+void BattleInterface::stackAttacking( const StackAttackInfo & attackInfo )
 {
-	stacksController->stackAttacking(attacker, dest, attacked, shooting);
+	stacksController->stackAttacking(attackInfo);
 }
 
 void BattleInterface::newRoundFirst( int round )
 {
-	waitForAnims();
+	waitForAnimationCondition(EAnimationEvents::OPENING, false);
 }
 
 void BattleInterface::newRound(int number)
 {
-	controlPanel->console->addText(CGI->generaltexth->allTexts[412]);
+	console->addText(CGI->generaltexth->allTexts[412]);
 }
 
 void BattleInterface::giveCommand(EActionType action, BattleHex tile, si32 additional)
@@ -435,11 +299,6 @@ const CGHeroInstance * BattleInterface::getActiveHero()
 	return defendingHeroInstance;
 }
 
-void BattleInterface::hexLclicked(int whichOne)
-{
-	actionsController->handleHex(whichOne, LCLICK);
-}
-
 void BattleInterface::stackIsCatapulting(const CatapultAttack & ca)
 {
 	if (siegeController)
@@ -454,33 +313,31 @@ void BattleInterface::gateStateChanged(const EGateState state)
 
 void BattleInterface::battleFinished(const BattleResult& br)
 {
-	bresult = &br;
-	{
-		auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim);
-		animsAreDisplayed.waitUntil(false);
-	}
+	assert(getAnimationCondition(EAnimationEvents::ACTION) == false);
+	waitForAnimationCondition(EAnimationEvents::ACTION, false);
 	stacksController->setActiveStack(nullptr);
-	displayBattleFinished();
-}
 
-void BattleInterface::displayBattleFinished()
-{
-	CCS->curh->changeGraphic(ECursor::ADVENTURE,0);
+	CCS->curh->set(Cursor::Map::POINTER);
+	curInt->waitWhileDialog();
+
 	if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-skip-battle-result"].Bool())
 	{
-		close();
+		windowObject->close();
 		return;
 	}
 
-	GH.pushInt(std::make_shared<BattleResultWindow>(*bresult, *(this->curInt)));
+	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)
@@ -489,7 +346,15 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 	const std::string & castSoundPath = spell->getCastSound();
 
 	if (!castSoundPath.empty())
-		CCS->soundh->playSound(castSoundPath);
+	{
+		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 )
 	{
@@ -497,56 +362,77 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 
 		if(casterStack != nullptr )
 		{
-			displaySpellCast(spellID, casterStack->getPosition());
-
-			stacksController->addNewAnim(new CCastAnimation(*this, casterStack, sc->tile, curInt->cb->battleGetStackByPos(sc->tile), spell));
+			executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]()
+			{
+				stacksController->addNewAnim(new CastAnimation(*this, casterStack, targetedTile, curInt->cb->battleGetStackByPos(targetedTile), spell));
+				displaySpellCast(spell, casterStack->getPosition());
+			});
 		}
 		else
-		if (sc->tile.isValid() && !spell->animationInfo.projectile.empty())
 		{
-			// this is spell cast by hero with valid destination & valid projectile -> play animation
-
-			const CStack * target = curInt->cb->battleGetStackByPos(sc->tile);
-			Point srccoord = (sc->side ? Point(770, 60) : Point(30, 60)) + pos;	//hero position
-			Point destcoord = stacksController->getStackPositionAtHex(sc->tile, target); //position attacked by projectile
-			destcoord += Point(250, 240); // FIXME: what are these constants?
-
-			projectilesController->createSpellProjectile( nullptr, srccoord, destcoord, spell);
-			projectilesController->emitStackProjectile( nullptr );
+			auto hero = sc->side ? defendingHero : attackingHero;
+			assert(hero);
 
-			// wait fo projectile to end
-			stacksController->addNewAnim(new CWaitingProjectileAnimation(*this, nullptr));
+			executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]()
+			{
+				stacksController->addNewAnim(new HeroCastAnimation(*this, hero, targetedTile, curInt->cb->battleGetStackByPos(targetedTile), spell));
+			});
 		}
 	}
 
-	waitForAnims(); //wait for projectile animation
-
-	displaySpellHit(spellID, sc->tile);
+	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)
-			displaySpellEffect(spellID, stack->getPosition());
+		{
+			executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+				displaySpellEffect(spell, stack->getPosition());
+			});
+		}
 	}
 
-	//queuing additional animation
-	for(auto & elem : sc->customEffects)
+	for(auto & elem : sc->reflectedCres)
 	{
-		auto stack = curInt->cb->battleGetStackByID(elem.stack, false);
-		if(stack)
-			effectsController->displayEffect(EBattleEffect::EBattleEffect(elem.effect), stack->getPosition());
+		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());
+		});
 	}
 
-	waitForAnims();
 	//mana absorption
 	if (sc->manaGained > 0)
 	{
-		Point leftHero = Point(15, 30) + pos;
-		Point rightHero = Point(755, 30) + pos;
-		stacksController->addNewAnim(new CPointEffectAnimation(*this, soundBase::invalid, sc->side ? "SP07_A.DEF" : "SP07_B.DEF", leftHero));
-		stacksController->addNewAnim(new CPointEffectAnimation(*this, soundBase::invalid, sc->side ? "SP07_B.DEF" : "SP07_A.DEF", rightHero));
+		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));
+		});
 	}
 }
 
@@ -556,7 +442,7 @@ void BattleInterface::battleStacksEffectsSet(const SetStackEffect & sse)
 		fieldController->redrawBackgroundWithHexes();
 }
 
-void BattleInterface::setHeroAnimation(ui8 side, int phase)
+void BattleInterface::setHeroAnimation(ui8 side, EHeroAnimType phase)
 {
 	if(side == BattleSide::ATTACKER)
 	{
@@ -576,60 +462,62 @@ void BattleInterface::displayBattleLog(const std::vector<MetaString> & battleLog
 	{
 		std::string formatted = line.toString();
 		boost::algorithm::trim(formatted);
-		if(!controlPanel->console->addText(formatted))
-			logGlobal->warn("Too long battle log line");
+		appendBattleLog(formatted);
 	}
 }
 
-void BattleInterface::displaySpellAnimationQueue(const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit)
+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 CDummyAnimation(*this, animation.pause));
-		else
+			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 |= CPointEffectAnimation::FORCE_ON_TOP;
+				flags |= EffectAnimation::FORCE_ON_TOP;
 
 			if (animation.verticalPosition == VerticalPosition::BOTTOM)
-				flags |= CPointEffectAnimation::ALIGN_TO_BOTTOM;
+				flags |= EffectAnimation::ALIGN_TO_BOTTOM;
 
 			if (!destinationTile.isValid())
-				flags |= CPointEffectAnimation::SCREEN_FILL;
+				flags |= EffectAnimation::SCREEN_FILL;
 
 			if (!destinationTile.isValid())
-				stacksController->addNewAnim(new CPointEffectAnimation(*this, soundBase::invalid, animation.resourceName, flags));
+				stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, flags));
 			else
-				stacksController->addNewAnim(new CPointEffectAnimation(*this, soundBase::invalid, animation.resourceName, destinationTile, flags));
+				stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, destinationTile, flags));
 		}
 	}
 }
 
-void BattleInterface::displaySpellCast(SpellID spellID, BattleHex destinationTile)
+void BattleInterface::displaySpellCast(const CSpell * spell, BattleHex destinationTile)
 {
-	const CSpell * spell = spellID.toSpell();
-
 	if(spell)
-		displaySpellAnimationQueue(spell->animationInfo.cast, destinationTile, false);
+		displaySpellAnimationQueue(spell, spell->animationInfo.cast, destinationTile, false);
 }
 
-void BattleInterface::displaySpellEffect(SpellID spellID, BattleHex destinationTile)
+void BattleInterface::displaySpellEffect(const CSpell * spell, BattleHex destinationTile)
 {
-	const CSpell *spell = spellID.toSpell();
-
 	if(spell)
-		displaySpellAnimationQueue(spell->animationInfo.affect, destinationTile, false);
+		displaySpellAnimationQueue(spell, spell->animationInfo.affect, destinationTile, false);
 }
 
-void BattleInterface::displaySpellHit(SpellID spellID, BattleHex destinationTile)
+void BattleInterface::displaySpellHit(const CSpell * spell, BattleHex destinationTile)
 {
-	const CSpell * spell = spellID.toSpell();
-
 	if(spell)
-		displaySpellAnimationQueue(spell->animationInfo.hit, destinationTile, true);
+		displaySpellAnimationQueue(spell, spell->animationInfo.hit, destinationTile, true);
 }
 
 void BattleInterface::setAnimSpeed(int set)
@@ -662,9 +550,6 @@ void BattleInterface::trySetActivePlayer( PlayerColor player )
 
 void BattleInterface::activateStack()
 {
-	if(!battleActionsStarted)
-		return; //"show" function should re-call this function
-
 	stacksController->activateStack();
 
 	const CStack * s = stacksController->getActiveStack();
@@ -672,7 +557,8 @@ void BattleInterface::activateStack()
 		return;
 
 	myTurn = true;
-	queue->update();
+	windowObject->updateQueue();
+	windowObject->blockUI(false);
 	fieldController->redrawBackgroundWithHexes();
 	actionsController->activateStack();
 	GH.fakeMouseMove();
@@ -682,66 +568,28 @@ void BattleInterface::endAction(const BattleAction* action)
 {
 	const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber);
 
-	if(action->actionType == EActionType::HERO_SPELL)
-		setHeroAnimation(action->side, CCreatureAnim::HERO_HOLDING);
-
 	stacksController->endAction(action);
+	windowObject->updateQueue();
 
-	queue->update();
-
-	if (tacticsMode) //stack ended movement in tactics phase -> select the next one
+	//stack ended movement in tactics phase -> select the next one
+	if (tacticsMode)
 		tacticNextStack(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
+	//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();
-
-//	if (stacksController->getActiveStack() && !animsAreDisplayed.get() && pendingAnims.empty() && !active)
-//	{
-//		logGlobal->warn("Something wrong... interface was deactivated but there is no animation. Reactivating...");
-//		controlPanel->blockUI(false);
-//	}
-//	else
-//	{
-		// block UI if no active stack (e.g. enemy turn);
-	controlPanel->blockUI(stacksController->getActiveStack() == nullptr);
-//	}
 }
 
-void BattleInterface::hideQueue()
+void BattleInterface::appendBattleLog(const std::string & newEntry)
 {
-	Settings showQueue = settings.write["battle"]["showQueue"];
-	showQueue->Bool() = false;
-
-	queue->deactivate();
-
-	if (!queue->embedded)
-	{
-		moveBy(Point(0, -queue->pos.h / 2));
-		GH.totalRedraw();
-	}
-}
-
-void BattleInterface::showQueue()
-{
-	Settings showQueue = settings.write["battle"]["showQueue"];
-	showQueue->Bool() = true;
-
-	queue->activate();
-
-	if (!queue->embedded)
-	{
-		moveBy(Point(0, +queue->pos.h / 2));
-		GH.totalRedraw();
-	}
+	console->addText(newEntry);
 }
 
 void BattleInterface::startAction(const BattleAction* action)
 {
-	controlPanel->blockUI(true);
-
 	if(action->actionType == EActionType::END_TACTIC_PHASE)
 	{
-		controlPanel->tacticPhaseEnded();
+		windowObject->tacticPhaseEnded();
 		return;
 	}
 
@@ -749,7 +597,7 @@ void BattleInterface::startAction(const BattleAction* action)
 
 	if (stack)
 	{
-		queue->update();
+		windowObject->updateQueue();
 	}
 	else
 	{
@@ -758,13 +606,8 @@ void BattleInterface::startAction(const BattleAction* action)
 
 	stacksController->startAction(action);
 
-	redraw(); // redraw after deactivation, including proper handling of hovered hexes
-
 	if(action->actionType == EActionType::HERO_SPELL) //when hero casts spell
-	{
-		setHeroAnimation(action->side, CCreatureAnim::HERO_CAST_SPELL);
 		return;
-	}
 
 	if (!stack)
 	{
@@ -775,16 +618,9 @@ void BattleInterface::startAction(const BattleAction* action)
 	effectsController->startAction(action);
 }
 
-void BattleInterface::waitForAnims()
-{
-	auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim);
-	animsAreDisplayed.waitWhileTrue();
-}
-
 void BattleInterface::tacticPhaseEnd()
 {
 	stacksController->setActiveStack(nullptr);
-	controlPanel->blockUI(true);
 	tacticsMode = false;
 }
 
@@ -799,7 +635,8 @@ void BattleInterface::tacticNextStack(const CStack * current)
 		current = stacksController->getActiveStack();
 
 	//no switching stacks when the current one is moving
-	waitForAnims();
+	assert(getAnimationCondition(EAnimationEvents::ACTION) == false);
+	waitForAnimationCondition(EAnimationEvents::ACTION, false);
 
 	TStacks stacksOfMine = tacticianInterface->cb->battleGetStacks(CBattleCallback::ONLY_MINE);
 	vstd::erase_if (stacksOfMine, &immobile);
@@ -850,7 +687,7 @@ void BattleInterface::requestAutofightingAIToTakeAction()
 
 	boost::thread aiThread([&]()
 	{
-		auto ba = make_unique<BattleAction>(curInt->autofightingAI->activeStack(stacksController->getActiveStack()));
+		auto ba = std::make_unique<BattleAction>(curInt->autofightingAI->activeStack(stacksController->getActiveStack()));
 
 		if(curInt->cb->battleIsFinished())
 		{
@@ -866,7 +703,6 @@ void BattleInterface::requestAutofightingAIToTakeAction()
 				//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);
-				controlPanel->blockUI(true);
 				tacticsMode = false;
 			}
 			else
@@ -884,81 +720,49 @@ void BattleInterface::requestAutofightingAIToTakeAction()
 	aiThread.detach();
 }
 
-void BattleInterface::showAll(SDL_Surface *to)
+void BattleInterface::castThisSpell(SpellID spellID)
 {
-	show(to);
+	actionsController->castThisSpell(spellID);
 }
 
-void BattleInterface::show(SDL_Surface *to)
+void BattleInterface::setAnimationCondition( EAnimationEvents event, bool state)
 {
-	Canvas canvas(to);
-	assert(to);
+	logAnim->debug("setAnimationCondition: %d -> %s", static_cast<int>(event), state ? "ON" : "OFF");
 
-	SDL_Rect buf;
-	SDL_GetClipRect(to, &buf);
-	SDL_SetClipRect(to, &pos);
+	size_t index = static_cast<size_t>(event);
+	animationEvents[index].setn(state);
 
-	++animCount;
-
-	fieldController->renderBattlefield(canvas);
-
-	if(battleActionsStarted)
-		stacksController->updateBattleAnimations();
-
-	SDL_SetClipRect(to, &buf); //restoring previous clip_rect
-
-	showInterface(to);
-
-	//activation of next stack, if any
-	//TODO: should be moved to the very start of this method?
-	//activateStack();
-}
+	decltype(awaitingEvents) executingEvents;
 
-void BattleInterface::collectRenderableObjects(BattleRenderer & renderer)
-{
-	if (attackingHero)
-	{
-		renderer.insert(EBattleFieldLayer::HEROES, BattleHex(0),[this](BattleRenderer::RendererRef canvas)
-		{
-			attackingHero->render(canvas);
-		});
-	}
-	if (defendingHero)
+	for (auto it = awaitingEvents.begin(); it != awaitingEvents.end();)
 	{
-		renderer.insert(EBattleFieldLayer::HEROES, BattleHex(GameConstants::BFIELD_WIDTH-1),[this](BattleRenderer::RendererRef canvas)
+		if (it->event == event && it->eventState == state)
 		{
-			defendingHero->render(canvas);
-		});
+			executingEvents.push_back(*it);
+			it = awaitingEvents.erase(it);
+		}
+		else
+			++it;
 	}
+
+	for (auto const & event : executingEvents)
+		event.action();
 }
 
-void BattleInterface::showInterface(SDL_Surface * to)
+bool BattleInterface::getAnimationCondition( EAnimationEvents event)
 {
-	//showing in-game console
-	LOCPLINT->cingconsole->show(to);
-	controlPanel->showAll(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);
-	}
+	size_t index = static_cast<size_t>(event);
+	return animationEvents[index].get();
+}
 
-	//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);
-	}
+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::castThisSpell(SpellID spellID)
+void BattleInterface::executeOnAnimationCondition( EAnimationEvents event, bool state, const AwaitingAnimationAction & action)
 {
-	actionsController->castThisSpell(spellID);
+	awaitingEvents.push_back({action, event, state});
 }

+ 100 - 83
client/battle/BattleInterface.h

@@ -9,8 +9,10 @@
  */
 #pragma once
 
+#include "BattleConstants.h"
 #include "../gui/CIntObject.h"
 #include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation
+#include "../../lib/CondSh.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -20,7 +22,6 @@ class CStack;
 struct BattleResult;
 struct BattleSpellCast;
 struct CObstacleInstance;
-template <typename T> struct CondSh;
 struct SetStackEffect;
 class BattleAction;
 class CGTownInstance;
@@ -28,7 +29,6 @@ struct CatapultAttack;
 struct BattleTriggerEffect;
 struct BattleHex;
 struct InfoAboutHero;
-struct CustomEffectInfo;
 
 VCMI_LIB_NAMESPACE_END
 
@@ -48,60 +48,91 @@ class BattleSiegeController;
 class BattleObstacleController;
 class BattleFieldController;
 class BattleRenderer;
-class BattleControlPanel;
+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; //attacked stack
-	int64_t dmg; //damage dealt
-	unsigned int amountKilled; //how many creatures in stack has been killed
-	const CStack *attacker; //attacking stack
+	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;
 };
 
-/// Big class which handles the overall battle interface actions and it is also responsible for
-/// drawing everything correctly.
-class BattleInterface : public WindowBase
+struct StackAttackInfo
 {
-private:
-	std::shared_ptr<BattleHero> attackingHero;
-	std::shared_ptr<BattleHero> defendingHero;
-	std::shared_ptr<StackQueue> queue;
-	std::shared_ptr<BattleControlPanel> controlPanel;
-
-	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
-	std::shared_ptr<CPlayerInterface> attackerInt, defenderInt; //because LOCPLINT is not enough in hotSeat
-	std::shared_ptr<CPlayerInterface> curInt; //current player interface
+	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;
+};
 
-	const CCreatureSet *army1, *army2; //copy of initial armies (for result window)
-	const CGHeroInstance *attackingHeroInstance, *defendingHeroInstance;
+/// Main class for battles, responsible for relaying information from server to various battle entities
+class BattleInterface
+{
+	using AwaitingAnimationAction = std::function<void()>;
 
-	ui8 animCount;
+	struct AwaitingAnimationEvents {
+		AwaitingAnimationAction action;
+		EAnimationEvents event;
+		bool eventState;
+	};
 
-	bool tacticsMode;
-	bool battleActionsStarted; //used for delaying battle actions until intro sound stops
-	int battleIntroSoundChannel; //required as variable for disabling it via ESC key
+	/// Conditional variables that are set depending on ongoing animations on the battlefield
+	std::array< CondSh<bool>, static_cast<size_t>(EAnimationEvents::COUNT)> animationEvents;
 
-	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();
+	/// List of events that are waiting to be triggered
+	std::vector<AwaitingAnimationEvents> awaitingEvents;
 
-	void giveCommand(EActionType action, BattleHex tile = BattleHex(), si32 additional = -1);
-	void sendCommand(BattleAction *& command, const CStack * actor = nullptr);
+	/// 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;
 
-	const CGHeroInstance *getActiveHero(); //returns hero that can currently cast a spell
+	/// attacker interface, not null if attacker is human in our vcmiclient
+	std::shared_ptr<CPlayerInterface> attackerInt;
 
-	void showInterface(SDL_Surface * to);
+	/// defender interface, not null if attacker is human in our vcmiclient
+	std::shared_ptr<CPlayerInterface> defenderInt;
 
-	void setHeroAnimation(ui8 side, int phase);
+	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;
@@ -110,17 +141,33 @@ public:
 	std::unique_ptr<BattleActionsController> actionsController;
 	std::unique_ptr<BattleEffectsController> effectsController;
 
-	static CondSh<bool> animsAreDisplayed; //for waiting with the end of battle for end of anims
+	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)
-	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
+	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
 
-	BattleInterface(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 ~BattleInterface();
+	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
@@ -131,19 +178,18 @@ public:
 
 	void tacticNextStack(const CStack *current);
 	void tacticPhaseEnd();
-	void waitForAnims();
 
-	//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;
+	/// sets condition to targeted state and executes any awaiting actions
+	void setAnimationCondition( EAnimationEvents event, bool state);
 
-	void show(SDL_Surface *to) override;
-	void showAll(SDL_Surface *to) override;
+	/// returns current state of condition
+	bool getAnimationCondition( EAnimationEvents event);
 
-	void collectRenderableObjects(BattleRenderer & renderer);
+	/// 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);
@@ -151,29 +197,25 @@ public:
 	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); //stack with id number moved to destHex
+	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 CStack *attacker, BattleHex dest, const CStack *attacked, bool shooting); //called when stack with id ID is attacking something on hex dest
+	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 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 displaySpellAnimationQueue(const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit);
-	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 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 hideQueue();
-	void showQueue();
 
 	void obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> oi);
 
@@ -181,29 +223,4 @@ public:
 
 	const CGHeroInstance *currentHero() const;
 	InfoAboutHero enemyHero() const;
-
-	// TODO: cleanup this list
-	friend class CPlayerInterface;
-	friend class CInGameConsole;
-	friend class StackQueue;
-	friend class BattleResultWindow;
-	friend class BattleHero;
-	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 ClickableHex;
-	friend class BattleProjectileController;
-	friend class BattleSiegeController;
-	friend class BattleObstacleController;
-	friend class BattleFieldController;
-	friend class BattleControlPanel;
-	friend class BattleStacksController;
-	friend class BattleActionsController;
-	friend class BattleEffectsController;
 };

+ 187 - 95
client/battle/BattleInterfaceClasses.cpp

@@ -12,10 +12,11 @@
 
 #include "BattleInterface.h"
 #include "BattleActionsController.h"
+#include "BattleRenderer.h"
 #include "BattleSiegeController.h"
 #include "BattleFieldController.h"
 #include "BattleStacksController.h"
-#include "BattleControlPanel.h"
+#include "BattleWindow.h"
 
 #include "../CGameInfo.h"
 #include "../CMessage.h"
@@ -25,7 +26,7 @@
 #include "../Graphics.h"
 #include "../gui/CAnimation.h"
 #include "../gui/Canvas.h"
-#include "../gui/CCursorHandler.h"
+#include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../widgets/AdventureMapClasses.h"
 #include "../widgets/Buttons.h"
@@ -41,6 +42,7 @@
 #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"
@@ -48,70 +50,99 @@
 
 void BattleConsole::showAll(SDL_Surface * to)
 {
-	Point consolePos(pos.x + 10,      pos.y + 17);
-	Point textPos   (pos.x + pos.w/2, pos.y + 17);
+	CIntObject::showAll(to);
 
-	if (!consoleText.empty())
-	{
-		graphics->fonts[FONT_SMALL]->renderTextLinesLeft(to, CMessage::breakText(consoleText, pos.w, FONT_SMALL), Colors::WHITE, consolePos);
-	}
-	else if(!hoverText.empty())
+	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(hoverText, 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(logEntries.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(logEntries.size()==1)
+		if (graphics->fonts[FONT_SMALL]->getStringWidth(text) < pos.w)
 		{
-			graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(logEntries[0], pos.w, FONT_SMALL), Colors::WHITE, textPos);
+			output.push_back(line);
 		}
 		else
 		{
-			graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(logEntries[scrollPosition - 1], pos.w, FONT_SMALL), Colors::WHITE, textPos);
-			textPos.y += 16;
-			graphics->fonts[FONT_SMALL]->renderTextLinesCenter(to, CMessage::breakText(logEntries[scrollPosition], 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 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)
-		{
-			logEntries.push_back( text.substr(firstInToken, i-firstInToken) );
-			firstInToken = (int)i+1;
-		}
-	}
 
-	logEntries.push_back( text.substr(firstInToken, text.size()) );
+	auto newLines = splitText(text);
+
+	logEntries.insert(logEntries.end(), newLines.begin(), newLines.end());
 	scrollPosition = (int)logEntries.size()-1;
+	redraw();
 	return true;
 }
 void BattleConsole::scrollUp(ui32 by)
 {
 	if(scrollPosition > static_cast<int>(by))
 		scrollPosition -= by;
+	redraw();
 }
 
 void BattleConsole::scrollDown(ui32 by)
 {
 	if(scrollPosition + by < logEntries.size())
 		scrollPosition += by;
+	redraw();
 }
 
-BattleConsole::BattleConsole(const Rect & position)
+BattleConsole::BattleConsole(std::shared_ptr<CPicture> backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size)
 	: scrollPosition(-1)
 	, enteringText(false)
 {
-	pos += position.topLeft();
-	pos.w = position.w;
-	pos.h = position.h;
+	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 BattleConsole::deactivate()
@@ -137,17 +168,20 @@ void BattleConsole::setEnteringMode(bool on)
 		CSDL_Ext::stopTextInput();
 	}
 	enteringText = on;
+	redraw();
 }
 
 void BattleConsole::setEnteredText(const std::string & text)
 {
 	assert(enteringText == true);
 	consoleText = text;
+	redraw();
 }
 
 void BattleConsole::write(const std::string & Text)
 {
 	hoverText = Text;
+	redraw();
 }
 
 void BattleConsole::clearIfMatching(const std::string & Text)
@@ -161,75 +195,110 @@ void BattleConsole::clear()
 	write({});
 }
 
+const CGHeroInstance * BattleHero::instance()
+{
+	return hero;
+}
+
 void BattleHero::render(Canvas & canvas)
 {
-	auto flagFrame = flagAnimation->getImage(flagAnim, 0, true);
+	size_t groupIndex = static_cast<size_t>(phase);
 
-	if(!flagFrame)
-		return;
+	auto flagFrame = flagAnimation->getImage(flagCurrentFrame, 0, true);
+	auto heroFrame = animation->getImage(currentFrame, groupIndex, true);
 
-	//animation of flag
-	Point flagPosition = pos.topLeft();
+	Point heroPosition = pos.center() - parent->pos.topLeft() - heroFrame->dimensions() / 2;
+	Point flagPosition = pos.center() - parent->pos.topLeft() - flagFrame->dimensions() / 2;
 
-	if(flip)
-		flagPosition += Point(61, 39);
+	if(defender)
+		flagPosition += Point(-4, -41);
 	else
-		flagPosition += Point(72, 39);
+		flagPosition += Point(4, -41);
 
+	canvas.draw(flagFrame, flagPosition);
+	canvas.draw(heroFrame, heroPosition);
 
-	auto heroFrame = animation->getImage(currentFrame, phase, true);
+	flagCurrentFrame += currentSpeed;
+	currentFrame += currentSpeed;
 
-	canvas.draw(flagFrame, flagPosition);
-	canvas.draw(heroFrame, pos.topLeft());
+	if(flagCurrentFrame >= flagAnimation->size(0))
+		flagCurrentFrame -= flagAnimation->size(0);
 
-	if(++animCount >= 4)
+	if(currentFrame >= animation->size(groupIndex))
 	{
-		animCount = 0;
-		if(++flagAnim >= flagAnimation->size(0))
-			flagAnim = 0;
-
-		if(++currentFrame >= lastFrame)
-			switchToNextPhase();
+		currentFrame -= animation->size(groupIndex);
+		switchToNextPhase();
 	}
 }
 
-void BattleHero::setPhase(int newPhase)
+void BattleHero::pause()
+{
+	currentSpeed = 0.f;
+}
+
+void BattleHero::play()
+{
+	//FIXME: un-hardcode speed
+	currentSpeed = 0.25f;
+}
+
+float BattleHero::getFrame() const
+{
+	return currentFrame;
+}
+
+void BattleHero::collectRenderableObjects(BattleRenderer & renderer)
+{
+	auto hex = defender ? BattleHex(GameConstants::BFIELD_WIDTH-1) : BattleHex(0);
+
+	renderer.insert(EBattleFieldLayer::HEROES, hex, [this](BattleRenderer::RendererRef canvas)
+	{
+		render(canvas);
+	});
+}
+
+void BattleHero::onPhaseFinished(const std::function<void()> & callback)
+{
+	phaseFinishedCallback = callback;
+}
+
+void BattleHero::setPhase(EHeroAnimType newPhase)
 {
 	nextPhase = newPhase;
 	switchToNextPhase(); //immediately switch to next phase and then restore idling phase
-	nextPhase = 0;
+	nextPhase = EHeroAnimType::HOLDING;
 }
 
 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 BattleHero::clickLeft(tribool down, bool previousState)
 {
-	if(myOwner->actionsController->spellcastingModeActive()) //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
 	{
-		BattleHex hoveredHex = myOwner->fieldController->getHoveredHex();
+		BattleHex hoveredHex = owner.fieldController->getHoveredHex();
 		//do nothing when any hex is hovered - hero's animation overlaps battlefield
 		if ( hoveredHex != BattleHex::INVALID )
 			return;
 
-		CCS->curh->changeGraphic(ECursor::ADVENTURE, 0);
+		CCS->curh->set(Cursor::Map::POINTER);
 
-		GH.pushIntT<CSpellWindow>(myHero, myOwner->getCurrentPlayerInterface());
+		GH.pushIntT<CSpellWindow>(hero, owner.getCurrentPlayerInterface());
 	}
 }
 
@@ -239,13 +308,13 @@ void BattleHero::clickRight(tribool down, bool previousState)
 		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<HeroInfoWindow>(targetHero, &windowPosition);
 	}
@@ -253,43 +322,57 @@ void BattleHero::clickRight(tribool down, bool previousState)
 
 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();
 }
 
-BattleHero::BattleHero(const std::string & animationPath, bool flipG, PlayerColor player, const CGHeroInstance * hero, const BattleInterface & 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();
 }
 
 HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position)
@@ -584,7 +667,7 @@ void BattleResultWindow::bExitf()
 
 	close();
 
-	if(dynamic_cast<BattleInterface*>(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,
@@ -599,7 +682,7 @@ void ClickableHex::hover(bool on)
 	//Hoverable::hover(on);
 	if(!on && setAlterText)
 	{
-		myInterface->controlPanel->console->clear();
+		GH.statusbar->clear();
 		setAlterText = false;
 	}
 }
@@ -623,13 +706,13 @@ void ClickableHex::mouseMoved(const SDL_MouseMotionEvent &sEvent)
 			MetaString text;
 			text.addTxt(MetaString::GENERAL_TXT, 220);
 			attackedStack->addNameReplacement(text);
-			myInterface->controlPanel->console->write(text.toString());
+			GH.statusbar->write(text.toString());
 			setAlterText = true;
 		}
 	}
 	else if(setAlterText)
 	{
-		myInterface->controlPanel->console->clear();
+		GH.statusbar->clear();
 		setAlterText = false;
 	}
 }
@@ -638,7 +721,7 @@ void ClickableHex::clickLeft(tribool down, bool previousState)
 {
 	if(!down && hovered && strictHovered) //we've been really clicked!
 	{
-		myInterface->hexLclicked(myNumber);
+		myInterface->actionsController->handleHex(myNumber, LCLICK);
 	}
 }
 
@@ -662,10 +745,10 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & 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");
@@ -674,6 +757,8 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & 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));
 
@@ -688,10 +773,17 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & 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));
 	}
 }
 
+void StackQueue::show(SDL_Surface * to)
+{
+	if (embedded)
+		showAll(to);
+	CIntObject::show(to);
+}
+
 void StackQueue::update()
 {
 	std::vector<battle::Units> queueData;

+ 42 - 13
client/battle/BattleInterfaceClasses.h

@@ -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"
 
@@ -38,11 +40,14 @@ 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 BattleConsole : public CIntObject, public IStatusBar
 {
 private:
+	std::shared_ptr<CPicture> background;
+
 	/// List of all texts added during battle, essentially - log of entire battle
 	std::vector< std::string > logEntries;
 
@@ -57,8 +62,14 @@ private:
 
 	/// 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:
-	BattleConsole(const Rect & position);
+	BattleConsole(std::shared_ptr<CPicture> backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size);
 
 	void showAll(SDL_Surface * to) override;
 	void deactivate() override;
@@ -78,27 +89,43 @@ public:
 /// Hero battle animation
 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 BattleInterface * 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();
 
-	size_t flagAnim;
-	ui8 animCount; //for flag animation
 	void render(Canvas & canvas); //prints next frame of animation to to
-	void setPhase(int newPhase); //sets phase of hero animation
+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();
+
 	void hover(bool on) override;
 	void clickLeft(tribool down, bool previousState) override; //call-in
 	void clickRight(tribool down, bool previousState) override; //call-in
-	BattleHero(const std::string & animationPath, bool filpG, PlayerColor player, const CGHeroInstance * hero, const BattleInterface & owner);
+	BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender);
 };
 
 class HeroInfoWindow : public CWindowObject
@@ -193,4 +220,6 @@ public:
 
 	StackQueue(bool Embedded, BattleInterface & owner);
 	void update();
+
+	void show(SDL_Surface * to) override;
 };

+ 21 - 33
client/battle/BattleObstacleController.cpp

@@ -15,17 +15,22 @@
 #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)
+	owner(owner),
+	timePassed(0.f)
 {
 	auto obst = owner.curInt->cb->battleGetAllObstacles();
 	for(auto & elem : obst)
@@ -72,10 +77,6 @@ void BattleObstacleController::loadObstacleImage(const CObstacleInstance & oi)
 
 void BattleObstacleController::obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> & obstacles)
 {
-	assert(obstaclesBeingPlaced.empty());
-	for (auto const & oi : obstacles)
-		obstaclesBeingPlaced.push_back(oi->uniqueID);
-
 	for (auto const & oi : obstacles)
 	{
 		auto spellObstacle = dynamic_cast<const SpellCreatedObstacle*>(oi.get());
@@ -83,7 +84,6 @@ void BattleObstacleController::obstaclePlaced(const std::vector<std::shared_ptr<
 		if (!spellObstacle)
 		{
 			logGlobal->error("I don't know how to animate appearing obstacle of type %d", (int)oi->obstacleType);
-			obstaclesBeingPlaced.erase(obstaclesBeingPlaced.begin());
 			continue;
 		}
 
@@ -92,29 +92,22 @@ void BattleObstacleController::obstaclePlaced(const std::vector<std::shared_ptr<
 
 		auto first = animation->getImage(0, 0);
 		if(!first)
-		{
-			obstaclesBeingPlaced.erase(obstaclesBeingPlaced.begin());
 			continue;
-		}
-
-		//TODO: sound
-		//soundBase::QUIKSAND
-		//soundBase::LANDMINE
 
 		//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);
-		owner.stacksController->addNewAnim(new CPointEffectAnimation(owner, soundBase::invalid, spellObstacle->appearAnimation, whereTo, oi->pos, CPointEffectAnimation::WAIT_FOR_SOUND));
+		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.waitForAnims();
+		owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 
-		obstaclesBeingPlaced.erase(obstaclesBeingPlaced.begin());
 		loadObstacleImage(*spellObstacle);
 	}
 }
 
-void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas, const Point & offset)
+void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas)
 {
 	//Blit absolute obstacles
 	for(auto & oi : owner.curInt->cb->battleGetAllObstacles())
@@ -123,7 +116,7 @@ void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas, const Poin
 		{
 			auto img = getObstacleImage(*oi);
 			if(img)
-				canvas.draw(img, Point(offset.x + oi->getInfo().width, offset.y + oi->getInfo().height));
+				canvas.draw(img, Point(oi->getInfo().width, oi->getInfo().height));
 		}
 	}
 }
@@ -149,31 +142,26 @@ void BattleObstacleController::collectRenderableObjects(BattleRenderer & rendere
 	}
 }
 
+void BattleObstacleController::update()
+{
+	timePassed += GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+}
+
 std::shared_ptr<IImage> BattleObstacleController::getObstacleImage(const CObstacleInstance & oi)
 {
-	int frameIndex = (owner.animCount+1) *25 / owner.getAnimSpeed();
+	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)
-	{
-		if (boost::range::find(obstaclesBeingPlaced, oi.uniqueID) != obstaclesBeingPlaced.end())
-		{
-			// obstacle is not loaded yet, don't show anything
-			return nullptr;
-		}
-		else
-		{
-			assert(0); // how?
-			loadObstacleImage(oi);
-		}
-	}
+		return nullptr;
 
 	animation = obstacleAnimations[oi.uniqueID];
 	assert(animation);
 
 	if(animation)
 	{
-		frameIndex %= animation->size(0);
+		int frameIndex = framesCount % animation->size(0);
 		return animation->getImage(frameIndex, 0);
 	}
 	return nullptr;
@@ -183,7 +171,7 @@ Point BattleObstacleController::getObstaclePosition(std::shared_ptr<IImage> imag
 {
 	int offset = obstacle.getAnimationYOffset(image->height());
 
-	Rect r = owner.fieldController->hexPositionAbsolute(obstacle.pos);
+	Rect r = owner.fieldController->hexPositionLocal(obstacle.pos);
 	r.y += 42 - image->height() + offset;
 
 	return r.topLeft();

+ 7 - 5
client/battle/BattleObstacleController.h

@@ -29,16 +29,15 @@ 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;
 
-	/// semi-debug member, contains obstacles that should not yet be visible due to ongoing placement animation
-	/// used only for sanity checks to ensure that there are no invisible obstacles
-	std::vector<si32> obstaclesBeingPlaced;
-
 	void loadObstacleImage(const CObstacleInstance & oi);
 
 	std::shared_ptr<IImage> getObstacleImage(const CObstacleInstance & oi);
@@ -47,11 +46,14 @@ class BattleObstacleController
 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, const Point & offset);
+	void showAbsoluteObstacles(Canvas & canvas);
 
 	/// adds all non-"absolute" visible obstacles for rendering
 	void collectRenderableObjects(BattleRenderer & renderer);

+ 29 - 32
client/battle/BattleProjectileController.cpp

@@ -48,22 +48,21 @@ static double calculateCatapultParabolaY(const Point & from, const Point & dest,
 
 void ProjectileMissile::show(Canvas & canvas)
 {
-	logAnim->info("Projectile rendering, %d / %d", step, steps);
 	size_t group = reverse ? 1 : 0;
 	auto image = animation->getImage(frameNum, group, true);
 
 	if(image)
 	{
-		float progress = float(step) / steps;
-
 		Point pos {
-			CSDL_Ext::lerp(from.x, dest.x, progress) - image->width() / 2,
-			CSDL_Ext::lerp(from.y, dest.y, progress) - image->height() / 2,
+			vstd::lerp(from.x, dest.x, progress) - image->width() / 2,
+			vstd::lerp(from.y, dest.y, progress) - image->height() / 2,
 		};
 
 		canvas.draw(image, pos);
 	}
-	++step;
+
+	float timePassed = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+	progress += timePassed * speed;
 }
 
 void ProjectileAnimatedMissile::show(Canvas & canvas)
@@ -83,9 +82,7 @@ void ProjectileCatapult::show(Canvas & canvas)
 
 	if(image)
 	{
-		float progress = float(step) / steps;
-
-		int posX = CSDL_Ext::lerp(from.x, dest.x, progress);
+		int posX = vstd::lerp(from.x, dest.x, progress);
 		int posY = calculateCatapultParabolaY(from, dest, posX);
 		Point pos(posX, posY);
 
@@ -93,16 +90,16 @@ void ProjectileCatapult::show(Canvas & canvas)
 
 		frameNum = (frameNum + 1) % animation->size(0);
 	}
-	++step;
+
+	float timePassed = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+	progress += timePassed * speed;
 }
 
 void ProjectileRay::show(Canvas & canvas)
 {
-	float progress = float(step) / steps;
-
 	Point curr {
-		CSDL_Ext::lerp(from.x, dest.x, progress),
-		CSDL_Ext::lerp(from.y, dest.y, progress),
+		vstd::lerp(from.x, dest.x, progress),
+		vstd::lerp(from.y, dest.y, progress),
 	};
 
 	Point length = curr - from;
@@ -143,7 +140,9 @@ void ProjectileRay::show(Canvas & canvas)
 			canvas.drawLine(Point(x1 + i, y1), Point(x2 + i, y2), beginColor, endColor);
 		}
 	}
-	++step;
+
+	float timePassed = GH.mainFPSmng->getElapsedMilliseconds() / 1000.f;
+	progress += timePassed * speed;
 }
 
 BattleProjectileController::BattleProjectileController(BattleInterface & owner):
@@ -232,17 +231,17 @@ void BattleProjectileController::showProjectiles(Canvas & canvas)
 	}
 
 	vstd::erase_if(projectiles, [&](const std::shared_ptr<ProjectileBase> & projectile){
-		return projectile->step > projectile->steps;
+		return projectile->progress > 1.0f;
 	});
 }
 
-bool BattleProjectileController::hasActiveProjectile(const CStack * stack) const
+bool BattleProjectileController::hasActiveProjectile(const CStack * stack, bool emittedOnly) const
 {
 	int stackID = stack ? stack->ID : -1;
 
 	for(auto const & instance : projectiles)
 	{
-		if(instance->shooterID == stackID)
+		if(instance->shooterID == stackID && (instance->playing || !emittedOnly))
 		{
 			return true;
 		}
@@ -250,15 +249,14 @@ bool BattleProjectileController::hasActiveProjectile(const CStack * stack) const
 	return false;
 }
 
-int BattleProjectileController::computeProjectileFlightTime( Point from, Point dest, double animSpeed)
+float BattleProjectileController::computeProjectileFlightTime( Point from, Point dest, double animSpeed)
 {
-	double distanceSquared = (dest.x - from.x) * (dest.x - from.x) + (dest.y - from.y) * (dest.y - from.y);
-	double distance = sqrt(distanceSquared);
-	int steps = std::round(distance / 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);
 
-	if (steps > 0)
-		return steps;
-	return 1;
+	return animSpeed / std::max( 1.f, distance);
 }
 
 int BattleProjectileController::computeProjectileFrameID( Point from, Point dest, const CStack * stack)
@@ -298,12 +296,11 @@ void BattleProjectileController::createCatapultProjectile(const CStack * shooter
 
 	catapultProjectile->animation = getProjectileImage(shooter);
 	catapultProjectile->frameNum  = 0;
-	catapultProjectile->step      = 0;
-	catapultProjectile->steps     = computeProjectileFlightTime(from, dest, AnimationControls::getCatapultSpeed());
+	catapultProjectile->progress  = 0;
+	catapultProjectile->speed     = computeProjectileFlightTime(from, dest, AnimationControls::getCatapultSpeed());
 	catapultProjectile->from      = from;
 	catapultProjectile->dest      = dest;
 	catapultProjectile->shooterID = shooter->ID;
-	catapultProjectile->step      = 0;
 	catapultProjectile->playing   = false;
 
 	projectiles.push_back(std::shared_ptr<ProjectileBase>(catapultProjectile));
@@ -336,11 +333,11 @@ void BattleProjectileController::createProjectile(const CStack * shooter, Point
 		missileProjectile->frameNum = computeProjectileFrameID(from, dest, shooter);
 	}
 
-	projectile->steps     = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed());
+	projectile->speed     = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed());
 	projectile->from      = from;
 	projectile->dest      = dest;
 	projectile->shooterID = shooter->ID;
-	projectile->step      = 0;
+	projectile->progress  = 0;
 	projectile->playing   = false;
 
 	projectiles.push_back(projectile);
@@ -364,8 +361,8 @@ void BattleProjectileController::createSpellProjectile(const CStack * shooter, P
 		projectile->from          = from;
 		projectile->dest          = dest;
 		projectile->shooterID     = shooter ? shooter->ID : -1;
-		projectile->step          = 0;
-		projectile->steps         = computeProjectileFlightTime(from, dest, AnimationControls::getSpellEffectSpeed());
+		projectile->progress      = 0;
+		projectile->speed         = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed());
 		projectile->playing       = false;
 
 		projectiles.push_back(std::shared_ptr<ProjectileBase>(projectile));

+ 6 - 6
client/battle/BattleProjectileController.h

@@ -33,10 +33,10 @@ struct ProjectileBase
 	Point from; // initial position on the screen
 	Point dest; // target position on the screen
 
-	int step;      // current step counter
-	int steps;     // total number of steps/frames to show
-	int shooterID; // ID of shooter stack
-	bool playing;  // if set to true, projectile animation is playing, e.g. flying to target
+	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
@@ -97,7 +97,7 @@ class BattleProjectileController
 	const CCreature & getShooter(const CStack * stack) const;
 
 	int computeProjectileFrameID( Point from, Point dest, const CStack * stack);
-	int computeProjectileFlightTime( Point from, Point dest, double speed);
+	float computeProjectileFlightTime( Point from, Point dest, double speed);
 
 public:
 	BattleProjectileController(BattleInterface & owner);
@@ -106,7 +106,7 @@ public:
 	void showProjectiles(Canvas & canvas);
 
 	/// returns true if stack has projectile that is yet to hit target
-	bool hasActiveProjectile(const CStack * stack) const;
+	bool hasActiveProjectile(const CStack * stack, bool emittedOnly) const;
 
 	/// starts rendering previously created projectile
 	void emitStackProjectile(const CStack * stack);

+ 6 - 1
client/battle/BattleRenderer.cpp

@@ -11,19 +11,24 @@
 #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.collectRenderableObjects(*this);
 	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()

+ 5 - 5
client/battle/BattleRenderer.h

@@ -19,11 +19,11 @@ enum class EBattleFieldLayer {
 	OBSTACLES     = 0,
 	CORPSES       = 0,
 	WALLS         = 1,
-	HEROES        = 1,
-	STACKS        = 1, // after corpses, obstacles
-	BATTLEMENTS   = 2, // after stacks
-	STACK_AMOUNTS = 2, // after stacks, obstacles, corpses
-	EFFECTS       = 3, // after obstacles, battlements
+	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

+ 19 - 16
client/battle/BattleSiegeController.cpp

@@ -106,13 +106,13 @@ std::string BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisua
 	}
 }
 
-void BattleSiegeController::showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what, const Point & offset)
+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], offset + Point(pos.x, pos.y));
+		canvas.draw(wallPieceImages[what], Point(pos.x, pos.y));
 }
 
 std::string BattleSiegeController::getBattleBackgroundName() const
@@ -146,7 +146,7 @@ BattleHex BattleSiegeController::getWallPiecePosition(EWallVisual::EWallVisual w
 		BattleHex::HEX_AFTER_ALL,  // BOTTOM_TOWER,
 		182,                       // BOTTOM_WALL,
 		130,                       // WALL_BELLOW_GATE,
-		78,                        // WALL_OVER_GATE,
+		62,                        // WALL_OVER_GATE,
 		12,                        // UPPER_WALL,
 		BattleHex::HEX_BEFORE_ALL, // UPPER_TOWER,
 		BattleHex::HEX_BEFORE_ALL, // GATE,               // 94
@@ -205,10 +205,10 @@ Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) con
 
 	if (posID != 0)
 	{
-		Point result = owner.pos.topLeft();
-		result.x += town->town->clientInfo.siegePositions[posID].x;
-		result.y += town->town->clientInfo.siegePositions[posID].y;
-		return result;
+		return {
+			town->town->clientInfo.siegePositions[posID].x,
+			town->town->clientInfo.siegePositions[posID].y
+		};
 	}
 
 	assert(0);
@@ -249,13 +249,13 @@ void BattleSiegeController::gateStateChanged(const EGateState state)
 		CCS->soundh->playSound(soundBase::DRAWBRG);
 }
 
-void BattleSiegeController::showAbsoluteObstacles(Canvas & canvas, const Point & offset)
+void BattleSiegeController::showAbsoluteObstacles(Canvas & canvas)
 {
 	if (getWallPieceExistance(EWallVisual::MOAT))
-		showWallPiece(canvas, EWallVisual::MOAT, offset);
+		showWallPiece(canvas, EWallVisual::MOAT);
 
 	if (getWallPieceExistance(EWallVisual::MOAT_BANK))
-		showWallPiece(canvas, EWallVisual::MOAT_BANK, offset);
+		showWallPiece(canvas, EWallVisual::MOAT_BANK);
 }
 
 BattleHex BattleSiegeController::getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const
@@ -301,11 +301,11 @@ void BattleSiegeController::collectRenderableObjects(BattleRenderer & renderer)
 				owner.stacksController->showStack(canvas, getTurretStack(wallPiece));
 			});
 			renderer.insert( EBattleFieldLayer::BATTLEMENTS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
-				showWallPiece(canvas, wallPiece, owner.pos.topLeft());
+				showWallPiece(canvas, wallPiece);
 			});
 		}
 		renderer.insert( EBattleFieldLayer::WALLS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){
-			showWallPiece(canvas, wallPiece, owner.pos.topLeft());
+			showWallPiece(canvas, wallPiece);
 		});
 
 
@@ -327,12 +327,14 @@ bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const
 
 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 CCatapultAnimation(owner, stack, attackInfo.destinationTile, nullptr, attackInfo.damageDealt));
+			owner.stacksController->addNewAnim(new CatapultAnimation(owner, stack, attackInfo.destinationTile, nullptr, attackInfo.damageDealt));
 		}
 	}
 	else
@@ -343,11 +345,12 @@ void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca)
 		for (auto attackInfo : ca.attackedParts)
 			positions.push_back(owner.stacksController->getStackPositionAtHex(attackInfo.destinationTile, nullptr) + Point(99, 120));
 
-
-		owner.stacksController->addNewAnim(new CPointEffectAnimation(owner, soundBase::WALLHIT, "SGEXPL.DEF", positions));
+		CCS->soundh->playSound( "WALLHIT" );
+		owner.stacksController->addNewAnim(new EffectAnimation(owner, "SGEXPL.DEF", positions));
 	}
 
-	owner.waitForAnims();
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.setAnimationCondition(EAnimationEvents::HIT, false);
 
 	for (auto attackInfo : ca.attackedParts)
 	{

+ 2 - 2
client/battle/BattleSiegeController.h

@@ -84,7 +84,7 @@ class BattleSiegeController
 	/// 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, const Point & offset);
+	void showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what);
 
 	BattleHex getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const;
 	const CStack * getTurretStack(EWallVisual::EWallVisual wallPiece) const;
@@ -97,7 +97,7 @@ public:
 	void stackIsCatapulting(const CatapultAttack & ca);
 
 	/// call-ins from other battle controllers
-	void showAbsoluteObstacles(Canvas & canvas, const Point & offset);
+	void showAbsoluteObstacles(Canvas & canvas);
 	void collectRenderableObjects(BattleRenderer & renderer);
 
 	/// queries from other battle controllers

+ 488 - 136
client/battle/BattleStacksController.cpp

@@ -13,11 +13,12 @@
 #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 "BattleControlPanel.h"
+#include "BattleWindow.h"
 #include "BattleRenderer.h"
 #include "CreatureAnimation.h"
 
@@ -27,6 +28,7 @@
 #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"
@@ -40,20 +42,26 @@ static void onAnimationFinished(const CStack *stack, std::weak_ptr<CreatureAnima
 	if(!animation)
 		return;
 
+	if (!stack->isFrozen() && animation->getType() == ECreatureAnimType::FROZEN)
+		animation->setType(ECreatureAnimType::HOLDING);
+
 	if (animation->isIdle())
 	{
 		const CCreature *creature = stack->getCreature();
 
-		if (animation->framesInGroup(CCreatureAnim::MOUSEON) > 0)
+		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(CCreatureAnim::MOUSEON);
+				animation->playOnce(ECreatureAnimType::MOUSEON);
 			else
-				animation->setType(CCreatureAnim::HOLDING);
+				animation->setType(ECreatureAnimType::HOLDING);
 		}
 		else
 		{
-			animation->setType(CCreatureAnim::HOLDING);
+			animation->setType(ECreatureAnimType::HOLDING);
 		}
 	}
 	// always reset callback
@@ -63,7 +71,6 @@ static void onAnimationFinished(const CStack *stack, std::weak_ptr<CreatureAnima
 BattleStacksController::BattleStacksController(BattleInterface & owner):
 	owner(owner),
 	activeStack(nullptr),
-	mouseHoveredStack(nullptr),
 	stackToActivate(nullptr),
 	selectedStack(nullptr),
 	stackCanCastSpell(false),
@@ -76,20 +83,26 @@ BattleStacksController::BattleStacksController(BattleInterface & owner):
 	amountNegative   = IImage::createFromFile("CMNUMWIN.BMP");
 	amountEffNeutral = IImage::createFromFile("CMNUMWIN.BMP");
 
-	static const ColorShifterMultiplyAndAddExcept shifterNormal  ({150,  50, 255, 255}, {0,0,0,0}, {255, 231, 132, 255});
-	static const ColorShifterMultiplyAndAddExcept shifterPositive({ 50, 255,  50, 255}, {0,0,0,0}, {255, 231, 132, 255});
-	static const ColorShifterMultiplyAndAddExcept shifterNegative({255,  50,  50, 255}, {0,0,0,0}, {255, 231, 132, 255});
-	static const ColorShifterMultiplyAndAddExcept shifterNeutral ({255, 255,  50, 255}, {0,0,0,0}, {255, 231, 132, 255});
+	static const auto shifterNormal   = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.6f, 0.2f, 1.0f );
+	static const auto shifterPositive = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.2f, 1.0f, 0.2f );
+	static const auto shifterNegative = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 0.2f, 0.2f );
+	static const auto shifterNeutral  = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 1.0f, 0.2f );
 
-	amountNormal->adjustPalette(&shifterNormal);
-	amountPositive->adjustPalette(&shifterPositive);
-	amountNegative->adjustPalette(&shifterNegative);
-	amountEffNeutral->adjustPalette(&shifterNeutral);
+	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);
+		stackAdded(s, true);
 	}
 }
 
@@ -98,7 +111,7 @@ BattleHex BattleStacksController::getStackCurrentPosition(const CStack * stack)
 	if ( !stackAnimation.at(stack->ID)->isMoving())
 		return stack->getPosition();
 
-	if (stack->hasBonusOfType(Bonus::FLYING))
+	if (stack->hasBonusOfType(Bonus::FLYING) && stackAnimation.at(stack->ID)->getType() == ECreatureAnimType::MOVING )
 		return BattleHex::HEX_AFTER_ALL;
 
 	for (auto & anim : currentAnimations)
@@ -107,10 +120,10 @@ BattleHex BattleStacksController::getStackCurrentPosition(const CStack * stack)
 		// 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 (CStackMoveAnimation *move = dynamic_cast<CStackMoveAnimation*>(anim))
+		if (StackMoveAnimation *move = dynamic_cast<StackMoveAnimation*>(anim))
 		{
 			if (move->stack == stack)
-				return move->currentHex;
+				return std::max(move->prevHex, move->nextHex);
 		}
 	}
 	return stack->getPosition();
@@ -126,7 +139,7 @@ void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer)
 			continue;
 
 		//FIXME: hack to ignore ghost stacks
-		if ((stackAnimation[stack->ID]->getType() == CCreatureAnim::DEAD || stackAnimation[stack->ID]->getType() == CCreatureAnim::HOLDING) && stack->isGhost())
+		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;
@@ -147,6 +160,11 @@ void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer)
 
 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())
@@ -158,22 +176,18 @@ void BattleStacksController::stackReset(const CStack * stack)
 	auto animation = iter->second;
 
 	if(stack->alive() && animation->isDeadOrDying())
-		animation->setType(CCreatureAnim::HOLDING);
-
-	static const ColorShifterMultiplyAndAdd shifterClone ({255, 255, 0, 255}, {0, 0, 255, 0});
-
-	if (stack->isClone())
 	{
-		animation->shiftColor(&shifterClone);
+		owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=]()
+		{
+			addNewAnim(new ResurrectionAnimation(owner, stack));
+		});
 	}
-
-	//TODO: handle more cases
 }
 
-void BattleStacksController::stackAdded(const CStack * stack)
+void BattleStacksController::stackAdded(const CStack * stack, bool instant)
 {
 	// Tower shooters have only their upper half visible
-	static const int turretCreatureAnimationHeight = 235;
+	static const int turretCreatureAnimationHeight = 225;
 
 	stackFacingRight[stack->ID] = stack->side == BattleSide::ATTACKER; // must be set before getting stack position
 
@@ -199,7 +213,21 @@ void BattleStacksController::stackAdded(const CStack * stack)
 	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(CCreatureAnim::HOLDING);
+	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)
@@ -212,31 +240,7 @@ void BattleStacksController::setActiveStack(const CStack *stack)
 	if (activeStack) // update UI
 		stackAnimation[activeStack->ID]->setBorderColor(AnimationControls::getGoldBorder());
 
-	owner.controlPanel->blockUI(activeStack == nullptr);
-}
-
-void BattleStacksController::setHoveredStack(const CStack *stack)
-{
-	if ( stack == mouseHoveredStack )
-		 return;
-
-	if (mouseHoveredStack)
-		stackAnimation[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)
-		{
-			stackAnimation[mouseHoveredStack->ID]->setBorderColor(AnimationControls::getBlueBorder());
-			if (stackAnimation[mouseHoveredStack->ID]->framesInGroup(CCreatureAnim::MOUSEON) > 0)
-				stackAnimation[mouseHoveredStack->ID]->playOnce(CCreatureAnim::MOUSEON);
-		}
-	}
-	else
-		mouseHoveredStack = nullptr;
+	owner.windowObject->blockUI(activeStack == nullptr);
 }
 
 bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const
@@ -249,39 +253,25 @@ bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const
 			currentActionTarget = target.at(0).hexValue;
 	}
 
-	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
+	//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 (!owner.battleActionsStarted) // do not perform any further checks since they are related to actions that will only occur after intro music
-		return true;
-
 	if(!stack->alive())
 		return false;
 
-	if(stack->getCount() == 0) //hide box when target is going to die anyway - do not display "0 creatures"
+	//hide box when target is going to die anyway - do not display "0 creatures"
+	if(stack->getCount() == 0)
 		return false;
 
-	for(auto anim : currentAnimations) //no matter what other conditions below are, hide box when creature is playing hit animation
+	// if stack has any ongoing animation - hide the box
+	for(auto anim : currentAnimations)
 	{
-		auto hitAnimation = dynamic_cast<CDefenceAnimation*>(anim);
-		if(hitAnimation && (hitAnimation->stack->ID == stack->ID))
+		auto stackAnimation = dynamic_cast<BattleStackAnimation*>(anim);
+		if(stackAnimation && (stackAnimation->stack->ID == stack->ID))
 			return false;
 	}
 
-	if(owner.curInt->curAction)
-	{
-		if(owner.curInt->curAction->stackNumber == stack->ID) //stack is currently taking action (is not a target of another creature's action etc)
-		{
-			if(owner.curInt->curAction->actionType == EActionType::WALK || owner.curInt->curAction->actionType == EActionType::SHOOT) //hide when stack walks or shoots
-				return false;
-
-			else if(owner.curInt->curAction->actionType == EActionType::WALK_AND_ATTACK && currentActionTarget != stack->getPosition()) //when attacking, hide until walk phase finished
-				return false;
-		}
-
-		if(owner.curInt->curAction->actionType == EActionType::SHOOT && currentActionTarget == stack->getPosition()) //hide if we are ranged attack target
-			return false;
-	}
 	return true;
 }
 
@@ -332,46 +322,65 @@ void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack *
 
 void BattleStacksController::showStack(Canvas & canvas, const CStack * stack)
 {
-	stackAnimation[stack->ID]->nextFrame(canvas, facingRight(stack)); // do actual blit
+	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::updateBattleAnimations()
+void BattleStacksController::update()
 {
-	for (auto & elem : currentAnimations)
-	{
-		if (!elem)
-			continue;
+	updateHoveredStacks();
+	updateBattleAnimations();
+}
 
-		if (elem->isInitialized())
-			elem->nextFrame();
-		else
+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())
-	{
-		//anims ended
-		owner.controlPanel->blockUI(activeStack == nullptr);
-		owner.animsAreDisplayed.setn(false);
-	}
-}
+		owner.setAnimationCondition(EAnimationEvents::ACTION, false);
 
-void BattleStacksController::addNewAnim(CBattleAnimation *anim)
-{
-	currentAnimations.push_back(anim);
-	owner.animsAreDisplayed.setn(true);
+	initializeBattleAnimations();
 }
 
-void BattleStacksController::stackActivated(const CStack *stack) //TODO: check it all before game state is changed due to abilities
+void BattleStacksController::addNewAnim(BattleAnimation *anim)
 {
-	stackToActivate = stack;
-	owner.waitForAnims();
-	if (stackToActivate) //during waiting stack may have gotten activated through show
-		owner.activateStack();
+	currentAnimations.push_back(anim);
+	owner.setAnimationCondition(EAnimationEvents::ACTION, true);
 }
 
 void BattleStacksController::stackRemoved(uint32_t stackID)
@@ -389,44 +398,275 @@ void BattleStacksController::stackRemoved(uint32_t stackID)
 
 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.cloneKilled) //FIXME: play dead animation for cloned creature before it vanishes
-			addNewAnim(new CDefenceAnimation(attackedInfo, owner));
+		if (!attackedInfo.attacker)
+			continue;
+
+		// In H3, attacked stack will not reverse on ranged attack
+		if (attackedInfo.indirectAttack)
+			continue;
 
-		if(attackedInfo.rebirth)
+		// 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.effectsController->displayEffect(EBattleEffect::RESURRECT, soundBase::RESURECT, attackedInfo.defender->getPosition()); //TODO: play reverse death animation
+			owner.executeOnAnimationCondition(EAnimationEvents::MOVEMENT, true, [=]()
+			{
+				addNewAnim(new ReverseAnimation(owner, attackedInfo.defender, attackedInfo.defender->getPosition()));
+			});
 		}
 	}
-	owner.waitForAnims();
+
+	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)
-			stackAnimation[attackedInfo.defender->ID]->setType(CCreatureAnim::HOLDING);
-		if (attackedInfo.cloneKilled)
-			stackRemoved(attackedInfo.defender->ID);
+		{
+			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)
 {
-	addNewAnim(new CMovementAnimation(owner, stack, destHex, distance));
-	owner.waitForAnims();
+	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);
 }
 
-void BattleStacksController::stackAttacking( const CStack *attacker, BattleHex dest, const CStack *attacked, bool shooting )
+bool BattleStacksController::shouldAttackFacingRight(const CStack * attacker, const CStack * defender)
 {
-	if (shooting)
+	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)
 	{
-		addNewAnim(new CShootingAnimation(owner, attacker, dest, attacked));
+		needsReverse = shouldRotate(attacker, attacker->position, info.tile);
 	}
 	else
 	{
-		addNewAnim(new CMeleeAttackAnimation(owner, attacker, dest, attacked));
+		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());
+		});
 	}
-	//waitForAnims();
+
+	//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
@@ -442,11 +682,11 @@ bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex
 	return false;
 }
 
-
 void BattleStacksController::endAction(const BattleAction* action)
 {
+	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+
 	//check if we should reverse stacks
-	//for some strange reason, it's not enough
 	TStacks stacks = owner.curInt->cb->battleGetStacks(CBattleCallback::MINE_AND_ENEMY);
 
 	for (const CStack *s : stacks)
@@ -455,29 +695,32 @@ void BattleStacksController::endAction(const BattleAction* action)
 
 		if (s && facingRight(s) != shouldFaceRight && s->alive() && stackAnimation[s->ID]->isIdle())
 		{
-			addNewAnim(new CReverseAnimation(owner, s, s->getPosition(), false));
+			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)
 {
-	const CStack *stack = owner.curInt->cb->battleGetStackByID(action->stackNumber);
-	setHoveredStack(nullptr);
-
-	auto actionTarget = action->getTarget(owner.curInt->cb.get());
-
-	if(action->actionType == EActionType::WALK
-		|| (action->actionType == EActionType::WALK_AND_ATTACK && actionTarget.at(0).hexValue != stack->getPosition()))
-	{
-		assert(stack);
-		owner.moveStarted = true;
-		if (stackAnimation[action->stackNumber]->framesInGroup(CCreatureAnim::MOVE_START))
-			addNewAnim(new CMovementStartAnimation(owner, stack));
+	removeExpiredColorFilters();
+}
 
-		//if(shouldRotate(stack, stack->getPosition(), actionTarget.at(0).hexValue))
-		//	addNewAnim(new CReverseAnimation(owner, stack, stack->getPosition(), true));
-	}
+void BattleStacksController::stackActivated(const CStack *stack)
+{
+	stackToActivate = stack;
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.activateStack();
 }
 
 void BattleStacksController::activateStack()
@@ -584,6 +827,115 @@ Point BattleStacksController::getStackPositionAtHex(BattleHex hexNum, const CSta
 		}
 	}
 	//returning
-	return ret + owner.pos.topLeft();
+	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 {};
 }

+ 45 - 12
client/battle/BattleStacksController.h

@@ -10,26 +10,38 @@
 #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 CBattleAnimation;
+class BattleAnimation;
 class CreatureAnimation;
-class CBattleAnimation;
+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
@@ -44,7 +56,10 @@ class BattleStacksController
 	std::shared_ptr<IImage> amountEffNeutral;
 
 	/// currently displayed animations <anim, initialized>
-	std::vector<CBattleAnimation *> currentAnimations;
+	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;
@@ -52,11 +67,11 @@ class BattleStacksController
 	/// <creatureID, if false reverse creature's animation> //TODO: move it to battle callback
 	std::map<int, bool> stackFacingRight;
 
-	/// number of active stack; nullptr - no one
+	/// currently active stack; nullptr - no one
 	const CStack *activeStack;
 
-	/// stack below mouse pointer, used for border animation
-	const CStack *mouseHoveredStack;
+	/// 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;
@@ -77,6 +92,19 @@ class BattleStacksController
 
 	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);
 
@@ -84,12 +112,13 @@ public:
 	bool facingRight(const CStack * stack) const;
 
 	void stackReset(const CStack * stack);
-	void stackAdded(const CStack * stack); //new stack appeared on battlefield
+	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 CStack *attacker, BattleHex dest, const CStack *attacked, bool shooting); //called when stack with id ID is attacking something on hex dest
+	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);
@@ -100,7 +129,6 @@ public:
 	void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all
 
 	void setActiveStack(const CStack *stack);
-	void setHoveredStack(const CStack *stack);
 	void setSelectedStack(const CStack *stack);
 
 	void showAliveStack(Canvas & canvas, const CStack * stack);
@@ -108,14 +136,19 @@ public:
 
 	void collectRenderableObjects(BattleRenderer & renderer);
 
-	void addNewAnim(CBattleAnimation *anim); //adds new anim to pendingAnims
-	void updateBattleAnimations();
+	/// 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 CBattleAnimation; // for exposing pendingAnims/creAnims/creDir to animations
+	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/CursorHandler.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> &);
+
+};
+

+ 119 - 91
client/battle/CreatureAnimation.cpp

@@ -14,11 +14,17 @@
 #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;
@@ -40,10 +46,8 @@ std::shared_ptr<CreatureAnimation> AnimationControls::getAnimation(const CCreatu
 	return std::make_shared<CreatureAnimation>(creature->animDefName, func);
 }
 
-float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, size_t group)
+float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType type)
 {
-	CCreatureAnim::EAnimType type = CCreatureAnim::EAnimType(group);
-
 	assert(creature->animation.walkAnimationTime != 0);
 	assert(creature->animation.attackAnimationTime != 0);
 	assert(anim->framesInGroup(type) != 0);
@@ -58,49 +62,50 @@ float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, c
 
 	switch (type)
 	{
-	case CCreatureAnim::MOVING:
+	case ECreatureAnimType::MOVING:
 		return static_cast<float>(speed * 2 * creature->animation.walkAnimationTime / anim->framesInGroup(type));
 
-	case CCreatureAnim::MOUSEON:
+	case ECreatureAnimType::MOUSEON:
 		return baseSpeed;
-	case CCreatureAnim::HOLDING:
+	case ECreatureAnimType::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:
+	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 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:
+	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 CCreatureAnim::TURN_L:
-	case CCreatureAnim::TURN_R:
+	case ECreatureAnimType::TURN_L:
+	case ECreatureAnimType::TURN_R:
 		return speed / 3;
 
-	case CCreatureAnim::MOVE_START:
-	case CCreatureAnim::MOVE_END:
+	case ECreatureAnimType::MOVE_START:
+	case ECreatureAnimType::MOVE_END:
 		return speed / 3;
 
-	case CCreatureAnim::DEAD:
-	case CCreatureAnim::DEAD_RANGED:
+	case ECreatureAnimType::DEAD:
+	case ECreatureAnimType::DEAD_RANGED:
 		return speed;
 
 	default:
@@ -110,12 +115,12 @@ float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, c
 
 float AnimationControls::getProjectileSpeed()
 {
-	return static_cast<float>(settings["battle"]["animationSpeed"].Float() * 100);
+	return static_cast<float>(settings["battle"]["animationSpeed"].Float() * 4000);
 }
 
 float AnimationControls::getCatapultSpeed()
 {
-	return static_cast<float>(settings["battle"]["animationSpeed"].Float() * 20);
+	return static_cast<float>(settings["battle"]["animationSpeed"].Float() * 1000);
 }
 
 float AnimationControls::getSpellEffectSpeed()
@@ -133,12 +138,22 @@ float AnimationControls::getFlightDistance(const CCreature * creature)
 	return static_cast<float>(creature->animation.flightAnimationDistance * 200);
 }
 
-CCreatureAnim::EAnimType CreatureAnimation::getType() const
+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(CCreatureAnim::EAnimType type)
+void CreatureAnimation::setType(ECreatureAnimType type)
 {
 	this->type = type;
 	currentFrame = 0;
@@ -147,21 +162,13 @@ void CreatureAnimation::setType(CCreatureAnim::EAnimType type)
 	play();
 }
 
-void CreatureAnimation::shiftColor(const ColorShifter* shifter)
-{
-	if(forward)
-		forward->shiftColor(shifter);
-
-	if(reverse)
-		reverse->shiftColor(shifter);
-}
-
 CreatureAnimation::CreatureAnimation(const std::string & name_, TSpeedController controller)
 	: name(name_),
 	  speed(0.1f),
+	  shadowAlpha(128),
 	  currentFrame(0),
 	  elapsedTime(0),
-	  type(CCreatureAnim::HOLDING),
+	  type(ECreatureAnimType::HOLDING),
 	  border(CSDL_Ext::makeColor(0, 0, 0, 0)),
 	  speedController(controller),
 	  once(false)
@@ -174,20 +181,37 @@ CreatureAnimation::CreatureAnimation(const std::string & name_, TSpeedController
 	reverse->preload();
 
 	// if necessary, add one frame into vcmi-only group DEAD
-	if(forward->size(CCreatureAnim::DEAD) == 0)
+	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(CCreatureAnim::DEATH, forward->size(CCreatureAnim::DEATH)-1, CCreatureAnim::DEAD);
-		reverse->duplicateImage(CCreatureAnim::DEATH, reverse->size(CCreatureAnim::DEATH)-1, CCreatureAnim::DEAD);
+		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(CCreatureAnim::DEAD_RANGED) == 0 && forward->size(CCreatureAnim::DEATH_RANGED) != 0)
+	if(forward->size(size_t(ECreatureAnimType::FROZEN)) == 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);
+		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, type, true);
+	auto first = forward->getImage(0, size_t(type), true);
 
 	if(!first)
 	{
@@ -228,7 +252,7 @@ bool CreatureAnimation::incrementFrame(float timePassed)
 			currentFrame -= framesNumber;
 
 		if(once)
-			setType(CCreatureAnim::HOLDING);
+			setType(ECreatureAnimType::HOLDING);
 
 		endAnimation();
 		return true;
@@ -256,7 +280,7 @@ float CreatureAnimation::getCurrentFrame() const
 	return currentFrame;
 }
 
-void CreatureAnimation::playOnce( CCreatureAnim::EAnimType type )
+void CreatureAnimation::playOnce( ECreatureAnimType type )
 {
 	setType(type);
 	once = true;
@@ -269,11 +293,6 @@ inline int getBorderStrength(float time)
 	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));
@@ -294,80 +313,89 @@ static SDL_Color addColors(const SDL_Color & base, const SDL_Color & over)
 			);
 }
 
-void CreatureAnimation::genBorderPalette(IImage::BorderPallete & target)
+void CreatureAnimation::genSpecialPalette(IImage::SpecialPalette & 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));
+	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, bool facingRight)
+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, type);
+		image = forward->getImage(frame, size_t(type));
 	else
-		image = reverse->getImage(frame, type);
+		image = reverse->getImage(frame, size_t(type));
 
 	if(image)
 	{
-		IImage::BorderPallete borderPallete;
-		genBorderPalette(borderPallete);
+		IImage::SpecialPalette SpecialPalette;
+		genSpecialPalette(SpecialPalette);
 
-		image->setBorderPallete(borderPallete);
+		image->setSpecialPallete(SpecialPalette);
+		image->adjustPalette(shifter);
 
 		canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h));
+
 	}
 }
 
-int CreatureAnimation::framesInGroup(CCreatureAnim::EAnimType group) const
+int CreatureAnimation::framesInGroup(ECreatureAnimType group) const
 {
-	return static_cast<int>(forward->size(group));
+	return static_cast<int>(forward->size(size_t(group)));
 }
 
 bool CreatureAnimation::isDead() const
 {
-	return getType() == CCreatureAnim::DEAD
-		|| getType() == CCreatureAnim::DEAD_RANGED;
+	return getType() == ECreatureAnimType::DEAD
+		|| getType() == ECreatureAnimType::DEAD_RANGED;
 }
 
 bool CreatureAnimation::isDying() const
 {
-	return getType() == CCreatureAnim::DEATH
-		|| getType() == CCreatureAnim::DEATH_RANGED;
+	return getType() == ECreatureAnimType::DEATH
+		|| getType() == ECreatureAnimType::DEATH_RANGED;
 }
 
 bool CreatureAnimation::isDeadOrDying() const
 {
-	return getType() == CCreatureAnim::DEAD
-		|| getType() == CCreatureAnim::DEATH
-		|| getType() == CCreatureAnim::DEAD_RANGED
-		|| getType() == CCreatureAnim::DEATH_RANGED;
+	return getType() == ECreatureAnimType::DEAD
+		|| getType() == ECreatureAnimType::DEATH
+		|| getType() == ECreatureAnimType::DEAD_RANGED
+		|| getType() == ECreatureAnimType::DEATH_RANGED;
 }
 
 bool CreatureAnimation::isIdle() const
 {
-	return getType() == CCreatureAnim::HOLDING
-	    || getType() == CCreatureAnim::MOUSEON;
+	return getType() == ECreatureAnimType::HOLDING
+		|| getType() == ECreatureAnimType::MOUSEON;
 }
 
 bool CreatureAnimation::isMoving() const
 {
-	return getType() == CCreatureAnim::MOVE_START
-	    || getType() == CCreatureAnim::MOVING
-		|| getType() == CCreatureAnim::MOVE_END
-		|| getType() == CCreatureAnim::TURN_L
-		|| getType() == CCreatureAnim::TURN_R;
+	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() == CCreatureAnim::SHOOT_UP
-	    || getType() == CCreatureAnim::SHOOT_FRONT
-	    || getType() == CCreatureAnim::SHOOT_DOWN;
+	return getType() == ECreatureAnimType::SHOOT_UP
+		|| getType() == ECreatureAnimType::SHOOT_FRONT
+		|| getType() == ECreatureAnimType::SHOOT_DOWN;
 }
 
 void CreatureAnimation::pause()
@@ -379,6 +407,6 @@ 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);
+	if(speedController(this, type) != 0)
+		speed = 1 / speedController(this, type);
 }

+ 20 - 15
client/battle/CreatureAnimation.h

@@ -29,13 +29,12 @@ namespace AnimationControls
 	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, size_t groupID);
+	float getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType groupID);
 
-	/// returns how far projectile should move each frame
-	/// TODO: make it time-based
+	/// returns how far projectile should move per second
 	float getProjectileSpeed();
 
-	/// returns speed of catapult projectile
+	/// 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)
@@ -46,6 +45,12 @@ namespace AnimationControls
 
 	/// 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
@@ -53,7 +58,7 @@ namespace AnimationControls
 class CreatureAnimation : public CIntObject
 {
 public:
-	typedef std::function<float(CreatureAnimation *, size_t)> TSpeedController;
+	typedef std::function<float(CreatureAnimation *, ECreatureAnimType)> TSpeedController;
 
 private:
 	std::string name;
@@ -78,7 +83,10 @@ private:
 	float elapsedTime;
 
 	///type of animation being displayed
-	CCreatureAnim::EAnimType type;
+	ECreatureAnimType type;
+
+	/// current value of shadow transparency
+	uint8_t shadowAlpha;
 
 	/// border color, disabled if alpha = 0
 	SDL_Color border;
@@ -90,7 +98,7 @@ private:
 
 	void endAnimation();
 
-	void genBorderPalette(IImage::BorderPallete & target);
+	void genSpecialPalette(IImage::SpecialPalette & target);
 public:
 
 	/// function(s) that will be called when animation ends, after reset to 1st frame
@@ -107,29 +115,26 @@ public:
 	CreatureAnimation(const std::string & name_, TSpeedController speedController);
 
 	/// sets type of animation and resets framecount
-	void setType(CCreatureAnim::EAnimType type);
+	void setType(ECreatureAnimType type);
 
 	/// returns currently rendered type of animation
-	CCreatureAnim::EAnimType getType() const;
+	ECreatureAnimType getType() const;
 
-	void nextFrame(Canvas & canvas, bool facingRight);
+	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);
 
-	/// apply color tint effect
-	void shiftColor(const ColorShifter * shifter);
-
 	/// Gets the current frame ID within current group.
 	float getCurrentFrame() const;
 
 	/// plays once given type of animation, then resets to idle
-	void playOnce(CCreatureAnim::EAnimType type);
+	void playOnce(ECreatureAnimType type);
 
 	/// returns number of frames in selected animation type
-	int framesInGroup(CCreatureAnim::EAnimType type) const;
+	int framesInGroup(ECreatureAnimType group) const;
 
 	void pause();
 	void play();

+ 31 - 47
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,8 +94,8 @@ 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, const Rect *src=nullptr, ui8 alpha=255) const override;
-	void draw(SDL_Surface * where, const SDL_Rect * dest, const 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;
@@ -105,10 +107,11 @@ public:
 	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;
 
@@ -212,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));
 
@@ -654,17 +642,16 @@ SDLImage::SDLImage(std::string filename)
 	}
 }
 
-void SDLImage::draw(SDL_Surface *where, int posX, int posY, const 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, const SDL_Rect* dest, const SDL_Rect* src, ui8 alpha) const
+void SDLImage::draw(SDL_Surface* where, const SDL_Rect* dest, const SDL_Rect* src) const
 {
 	if (!surf)
 		return;
@@ -804,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;
@@ -814,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]);
 	}
 }
 
@@ -827,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);
 	}
 }
 
@@ -1094,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)

+ 8 - 10
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, const Rect * src = nullptr, ui8 alpha = 255) const=0;
-	virtual void draw(SDL_Surface * where, const SDL_Rect * dest, const 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;
 
@@ -62,11 +62,12 @@ public:
 
 	//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;
@@ -121,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);
 

+ 0 - 271
client/gui/CCursorHandler.cpp

@@ -1,271 +0,0 @@
-/*
- * CCursorHandler.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 "CCursorHandler.h"
-
-#include <SDL.h>
-
-#include "SDL_Extensions.h"
-#include "CGuiHandler.h"
-#include "../widgets/Images.h"
-
-#include "../CMT.h"
-
-void CCursorHandler::clearBuffer()
-{
-	Uint32 fillColor = SDL_MapRGBA(buffer->format, 0, 0, 0, 0);
-	CSDL_Ext::fillRect(buffer, nullptr, fillColor);
-}
-
-void CCursorHandler::updateBuffer(CIntObject * payload)
-{
-	payload->moveTo(Point(0,0));
-	payload->showAll(buffer);
-
-	needUpdate = true;
-}
-
-void CCursorHandler::replaceBuffer(CIntObject * payload)
-{
-	clearBuffer();
-	updateBuffer(payload);
-}
-
-void CCursorHandler::initCursor()
-{
-	cursorLayer = SDL_CreateTexture(mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 40, 40);
-	SDL_SetTextureBlendMode(cursorLayer, SDL_BLENDMODE_BLEND);
-
-	xpos = ypos = 0;
-	type = ECursor::DEFAULT;
-	dndObject = nullptr;
-
-	cursors =
-	{
-		make_unique<CAnimImage>("CRADVNTR", 0),
-		make_unique<CAnimImage>("CRCOMBAT", 0),
-		make_unique<CAnimImage>("CRDEFLT",  0),
-		make_unique<CAnimImage>("CRSPELL",  0)
-	};
-
-	currentCursor = cursors.at(int(ECursor::DEFAULT)).get();
-
-	buffer = CSDL_Ext::newSurface(40,40);
-
-	SDL_SetSurfaceBlendMode(buffer, SDL_BLENDMODE_NONE);
-	SDL_ShowCursor(SDL_DISABLE);
-
-	changeGraphic(ECursor::ADVENTURE, 0);
-}
-
-void CCursorHandler::changeGraphic(ECursor::ECursorTypes type, int index)
-{
-	assert(dndObject == nullptr);
-
-	if(type != this->type)
-	{
-		this->type = type;
-		this->frame = index;
-		currentCursor = cursors.at(int(type)).get();
-		currentCursor->setFrame(index);
-	}
-	else if(index != this->frame)
-	{
-		this->frame = index;
-		currentCursor->setFrame(index);
-	}
-
-	replaceBuffer(currentCursor);
-}
-
-void CCursorHandler::dragAndDropCursor(std::unique_ptr<CAnimImage> object)
-{
-	dndObject = std::move(object);
-	if(dndObject)
-		replaceBuffer(dndObject.get());
-	else
-		replaceBuffer(currentCursor);
-}
-
-void CCursorHandler::cursorMove(const int & x, const int & y)
-{
-	xpos = x;
-	ypos = y;
-}
-
-void CCursorHandler::shiftPos( int &x, int &y )
-{
-	if(( type == ECursor::COMBAT && frame != ECursor::COMBAT_POINTER) || type == ECursor::SPELLBOOK)
-	{
-		x-=16;
-		y-=16;
-
-		// Properly align the melee attack cursors.
-		if (type == ECursor::COMBAT)
-		{
-			switch (frame)
-			{
-			case 7: // Bottom left
-				x -= 6;
-				y += 16;
-				break;
-			case 8: // Left
-				x -= 16;
-				y += 10;
-				break;
-			case 9: // Top left
-				x -= 6;
-				y -= 6;
-				break;
-			case 10: // Top right
-				x += 16;
-				y -= 6;
-				break;
-			case 11: // Right
-				x += 16;
-				y += 11;
-				break;
-			case 12: // Bottom right
-				x += 16;
-				y += 16;
-				break;
-			case 13: // Below
-				x += 9;
-				y += 16;
-				break;
-			case 14: // Above
-				x += 9;
-				y -= 15;
-				break;
-			}
-		}
-	}
-	else if(type == ECursor::ADVENTURE)
-	{
-		if (frame == 0); //to exclude
-		else if(frame == 2)
-		{
-			x -= 12;
-			y -= 10;
-		}
-		else if(frame == 3)
-		{
-			x -= 12;
-			y -= 12;
-		}
-		else if(frame < 27)
-		{
-			int hlpNum = (frame - 4)%6;
-			if(hlpNum == 0)
-			{
-				x -= 15;
-				y -= 13;
-			}
-			else if(hlpNum == 1)
-			{
-				x -= 13;
-				y -= 13;
-			}
-			else if(hlpNum == 2)
-			{
-				x -= 20;
-				y -= 20;
-			}
-			else if(hlpNum == 3)
-			{
-				x -= 13;
-				y -= 16;
-			}
-			else if(hlpNum == 4)
-			{
-				x -= 8;
-				y -= 9;
-			}
-			else if(hlpNum == 5)
-			{
-				x -= 14;
-				y -= 16;
-			}
-		}
-		else if(frame == 41)
-		{
-			x -= 14;
-			y -= 16;
-		}
-		else if(frame < 31 || frame == 42)
-		{
-			x -= 20;
-			y -= 20;
-		}
-	}
-}
-
-void CCursorHandler::centerCursor()
-{
-	this->xpos = static_cast<int>((screen->w / 2.) - (currentCursor->pos.w / 2.));
-	this->ypos = static_cast<int>((screen->h / 2.) - (currentCursor->pos.h / 2.));
-	SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE);
-	SDL_WarpMouse(this->xpos, this->ypos);
-	SDL_EventState(SDL_MOUSEMOTION, SDL_ENABLE);
-}
-
-void CCursorHandler::render()
-{
-	if(!showing)
-		return;
-
-	//the must update texture in the main (renderer) thread, but changes to cursor type may come from other threads
-	updateTexture();
-
-	int x = xpos;
-	int y = ypos;
-	shiftPos(x, y);
-
-	if(dndObject)
-	{
-		x -= dndObject->pos.w/2;
-		y -= dndObject->pos.h/2;
-	}
-
-	SDL_Rect destRect;
-	destRect.x = x;
-	destRect.y = y;
-	destRect.w = 40;
-	destRect.h = 40;
-
-	SDL_RenderCopy(mainRenderer, cursorLayer, nullptr, &destRect);
-}
-
-void CCursorHandler::updateTexture()
-{
-	if(needUpdate)
-	{
-		SDL_UpdateTexture(cursorLayer, nullptr, buffer->pixels, buffer->pitch);
-		needUpdate = false;
-	}
-}
-
-CCursorHandler::CCursorHandler()
-	: needUpdate(true),
-	buffer(nullptr),
-	cursorLayer(nullptr),
-	showing(false)
-{
-
-}
-
-CCursorHandler::~CCursorHandler()
-{
-	if(buffer)
-		SDL_FreeSurface(buffer);
-
-	if(cursorLayer)
-		SDL_DestroyTexture(cursorLayer);
-}

+ 0 - 82
client/gui/CCursorHandler.h

@@ -1,82 +0,0 @@
-/*
- * CCursorHandler.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
-class CIntObject;
-class CAnimImage;
-struct SDL_Surface;
-struct SDL_Texture;
-
-namespace ECursor
-{
-	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};
-}
-
-/// handles mouse cursor
-class CCursorHandler final
-{
-	bool needUpdate;
-	SDL_Texture * cursorLayer;
-
-	SDL_Surface * buffer;
-	CAnimImage * currentCursor;
-
-	std::unique_ptr<CAnimImage> dndObject; //if set, overrides currentCursor
-
-	std::array<std::unique_ptr<CAnimImage>, 4> cursors;
-
-	bool showing;
-
-	void clearBuffer();
-	void updateBuffer(CIntObject * payload);
-	void replaceBuffer(CIntObject * payload);
-	void shiftPos( int &x, int &y );
-
-	void updateTexture();
-public:
-	/// position of cursor
-	int xpos, ypos;
-
-	/// Current cursor
-	ECursor::ECursorTypes type;
-	size_t frame;
-
-	/// inits cursorHandler - run only once, it's not memleak-proof (rev 1333)
-	void initCursor();
-
-	/// 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);
-
-	/**
-	 * Replaces the cursor with a custom image.
-	 *
-	 * @param image Image to replace cursor with or nullptr to use the normal
-	 * cursor. CursorHandler takes ownership of object
-	 */
-	void dragAndDropCursor (std::unique_ptr<CAnimImage> image);
-
-	void render();
-
-	void hide() { showing=false; };
-	void show() { showing=true; };
-
-	/// change cursor's positions to (x, y)
-	void cursorMove(const int & x, const int & y);
-	/// Move cursor to screen center
-	void centerCursor();
-
-	CCursorHandler();
-	~CCursorHandler();
-};

+ 7 - 7
client/gui/CGuiHandler.cpp

@@ -14,7 +14,7 @@
 #include <SDL.h>
 
 #include "CIntObject.h"
-#include "CCursorHandler.h"
+#include "CursorHandler.h"
 
 #include "../CGameInfo.h"
 #include "../../lib/CThreadHelper.h"
@@ -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();
 

+ 2 - 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()

+ 31 - 13
client/gui/Canvas.cpp

@@ -17,24 +17,42 @@
 #include "../Graphics.h"
 
 Canvas::Canvas(SDL_Surface * surface):
-	surface(surface)
+	surface(surface),
+	renderOffset(0,0)
 {
 	surface->refcount++;
 }
 
 Canvas::Canvas(Canvas & other):
-	surface(other.surface)
+	surface(other.surface),
+	renderOffset(other.renderOffset)
 {
 	surface->refcount++;
 }
 
-Canvas::Canvas(const Point & size)
+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);
 }
 
@@ -42,33 +60,33 @@ void Canvas::draw(std::shared_ptr<IImage> image, const Point & pos)
 {
 	assert(image);
 	if (image)
-		image->draw(surface, pos.x, pos.y);
+		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, pos.x, pos.y, &sourceRect);
+		image->draw(surface, renderOffset.x + pos.x, renderOffset.y + pos.y, &sourceRect);
 }
 
 void Canvas::draw(Canvas & image, const Point & pos)
 {
-	blitAt(image.surface, pos.x, pos.y, surface);
+	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, from.x, from.y, dest.x, dest.y, colorFrom, 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, position);
-	case ETextAlignment::CENTER:       return graphics->fonts[font]->renderTextCenter(surface, text, colorDest, position);
-	case ETextAlignment::BOTTOMRIGHT:  return graphics->fonts[font]->renderTextRight (surface, text, colorDest, position);
+	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);
 	}
 }
 
@@ -76,9 +94,9 @@ void Canvas::drawText(const Point & position, const EFonts & font, const SDL_Col
 {
 	switch (alignment)
 	{
-	case ETextAlignment::TOPLEFT:      return graphics->fonts[font]->renderTextLinesLeft  (surface, text, colorDest, position);
-	case ETextAlignment::CENTER:       return graphics->fonts[font]->renderTextLinesCenter(surface, text, colorDest, position);
-	case ETextAlignment::BOTTOMRIGHT:  return graphics->fonts[font]->renderTextLinesRight (surface, text, colorDest, position);
+	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);
 	}
 }
 

+ 10 - 0
client/gui/Canvas.h

@@ -19,8 +19,15 @@ 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:
 
@@ -30,6 +37,9 @@ public:
 	/// 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);
 

+ 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;
+};

+ 402 - 0
client/gui/CursorHandler.cpp

@@ -0,0 +1,402 @@
+/*
+ * CCursorHandler.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 "CursorHandler.h"
+
+#include <SDL.h>
+
+#include "SDL_Extensions.h"
+#include "CGuiHandler.h"
+#include "CAnimation.h"
+#include "../../lib/CConfigHandler.h"
+
+//#include "../CMT.h"
+
+std::unique_ptr<ICursor> CursorHandler::createCursor()
+{
+	if (settings["video"]["softwareCursor"].Bool())
+		return std::make_unique<CursorSoftware>();
+	else
+		return std::make_unique<CursorHardware>();
+}
+
+CursorHandler::CursorHandler()
+	: cursor(createCursor())
+	, frameTime(0.f)
+	, showing(false)
+	, pos(0,0)
+{
+
+	type = Cursor::Type::DEFAULT;
+	dndObject = nullptr;
+
+	cursors =
+	{
+		std::make_unique<CAnimation>("CRADVNTR"),
+		std::make_unique<CAnimation>("CRCOMBAT"),
+		std::make_unique<CAnimation>("CRDEFLT"),
+		std::make_unique<CAnimation>("CRSPELL")
+	};
+
+	for (auto & cursor : cursors)
+		cursor->preload();
+
+	set(Cursor::Map::POINTER);
+}
+
+Point CursorHandler::position() const
+{
+	return pos;
+}
+
+void CursorHandler::changeGraphic(Cursor::Type type, size_t index)
+{
+	assert(dndObject == nullptr);
+
+	if (type == this->type && index == this->frame)
+		return;
+
+	this->type = type;
+	this->frame = index;
+
+	cursor->setImage(getCurrentImage(), getPivotOffset());
+}
+
+void CursorHandler::set(Cursor::Default index)
+{
+	changeGraphic(Cursor::Type::DEFAULT, static_cast<size_t>(index));
+}
+
+void CursorHandler::set(Cursor::Map index)
+{
+	changeGraphic(Cursor::Type::ADVENTURE, static_cast<size_t>(index));
+}
+
+void CursorHandler::set(Cursor::Combat index)
+{
+	changeGraphic(Cursor::Type::COMBAT, static_cast<size_t>(index));
+}
+
+void CursorHandler::set(Cursor::Spellcast index)
+{
+	//Note: this is animated cursor, ignore specified frame and only change type
+	changeGraphic(Cursor::Type::SPELLBOOK, frame);
+}
+
+void CursorHandler::dragAndDropCursor(std::shared_ptr<IImage> image)
+{
+	dndObject = image;
+	cursor->setImage(getCurrentImage(), getPivotOffset());
+}
+
+void CursorHandler::dragAndDropCursor (std::string path, size_t index)
+{
+	CAnimation anim(path);
+	anim.load(index);
+	dragAndDropCursor(anim.getImage(index));
+}
+
+void CursorHandler::cursorMove(const int & x, const int & y)
+{
+	pos.x = x;
+	pos.y = y;
+
+	cursor->setCursorPosition(pos);
+}
+
+Point CursorHandler::getPivotOffsetDefault(size_t index)
+{
+	return {0, 0};
+}
+
+Point CursorHandler::getPivotOffsetMap(size_t index)
+{
+	static const std::array<Point, 43> offsets = {{
+		{  0,  0}, // POINTER          =  0,
+		{  0,  0}, // HOURGLASS        =  1,
+		{ 12, 10}, // HERO             =  2,
+		{ 12, 12}, // TOWN             =  3,
+
+		{ 15, 13}, // T1_MOVE          =  4,
+		{ 13, 13}, // T1_ATTACK        =  5,
+		{ 16, 32}, // T1_SAIL          =  6,
+		{ 13, 20}, // T1_DISEMBARK     =  7,
+		{  8,  9}, // T1_EXCHANGE      =  8,
+		{ 14, 16}, // T1_VISIT         =  9,
+
+		{ 15, 13}, // T2_MOVE          = 10,
+		{ 13, 13}, // T2_ATTACK        = 11,
+		{ 16, 32}, // T2_SAIL          = 12,
+		{ 13, 20}, // T2_DISEMBARK     = 13,
+		{  8,  9}, // T2_EXCHANGE      = 14,
+		{ 14, 16}, // T2_VISIT         = 15,
+
+		{ 15, 13}, // T3_MOVE          = 16,
+		{ 13, 13}, // T3_ATTACK        = 17,
+		{ 16, 32}, // T3_SAIL          = 18,
+		{ 13, 20}, // T3_DISEMBARK     = 19,
+		{  8,  9}, // T3_EXCHANGE      = 20,
+		{ 14, 16}, // T3_VISIT         = 21,
+
+		{ 15, 13}, // T4_MOVE          = 22,
+		{ 13, 13}, // T4_ATTACK        = 23,
+		{ 16, 32}, // T4_SAIL          = 24,
+		{ 13, 20}, // T4_DISEMBARK     = 25,
+		{  8,  9}, // T4_EXCHANGE      = 26,
+		{ 14, 16}, // T4_VISIT         = 27,
+
+		{ 16, 32}, // T1_SAIL_VISIT    = 28,
+		{ 16, 32}, // T2_SAIL_VISIT    = 29,
+		{ 16, 32}, // T3_SAIL_VISIT    = 30,
+		{ 16, 32}, // T4_SAIL_VISIT    = 31,
+
+		{  6,  1}, // SCROLL_NORTH     = 32,
+		{ 16,  2}, // SCROLL_NORTHEAST = 33,
+		{ 21,  6}, // SCROLL_EAST      = 34,
+		{ 16, 16}, // SCROLL_SOUTHEAST = 35,
+		{  6, 21}, // SCROLL_SOUTH     = 36,
+		{  1, 16}, // SCROLL_SOUTHWEST = 37,
+		{  1,  5}, // SCROLL_WEST      = 38,
+		{  2,  1}, // SCROLL_NORTHWEST = 39,
+
+		{  0,  0}, // POINTER_COPY     = 40,
+		{ 14, 16}, // TELEPORT         = 41,
+		{ 20, 20}, // SCUTTLE_BOAT     = 42
+	}};
+
+	assert(offsets.size() == size_t(Cursor::Map::COUNT)); //Invalid number of pivot offsets for cursor
+	assert(index < offsets.size());
+	return offsets[index];
+}
+
+Point CursorHandler::getPivotOffsetCombat(size_t index)
+{
+	static const std::array<Point, 20> offsets = {{
+		{ 12, 12 }, // BLOCKED        = 0,
+		{ 10, 14 }, // MOVE           = 1,
+		{ 14, 14 }, // FLY            = 2,
+		{ 12, 12 }, // SHOOT          = 3,
+		{ 12, 12 }, // HERO           = 4,
+		{  8, 12 }, // QUERY          = 5,
+		{  0,  0 }, // POINTER        = 6,
+		{ 21,  0 }, // HIT_NORTHEAST  = 7,
+		{ 31,  5 }, // HIT_EAST       = 8,
+		{ 21, 21 }, // HIT_SOUTHEAST  = 9,
+		{  0, 21 }, // HIT_SOUTHWEST  = 10,
+		{  0,  5 }, // HIT_WEST       = 11,
+		{  0,  0 }, // HIT_NORTHWEST  = 12,
+		{  6,  0 }, // HIT_NORTH      = 13,
+		{  6, 31 }, // HIT_SOUTH      = 14,
+		{ 14,  0 }, // SHOOT_PENALTY  = 15,
+		{ 12, 12 }, // SHOOT_CATAPULT = 16,
+		{ 12, 12 }, // HEAL           = 17,
+		{ 12, 12 }, // SACRIFICE      = 18,
+		{ 14, 20 }, // TELEPORT       = 19
+	}};
+
+	assert(offsets.size() == size_t(Cursor::Combat::COUNT)); //Invalid number of pivot offsets for cursor
+	assert(index < offsets.size());
+	return offsets[index];
+}
+
+Point CursorHandler::getPivotOffsetSpellcast()
+{
+	return { 18, 28};
+}
+
+Point CursorHandler::getPivotOffset()
+{
+	if (dndObject)
+		return dndObject->dimensions() / 2;
+
+	switch (type) {
+	case Cursor::Type::ADVENTURE: return getPivotOffsetMap(frame);
+	case Cursor::Type::COMBAT:    return getPivotOffsetCombat(frame);
+	case Cursor::Type::DEFAULT:   return getPivotOffsetDefault(frame);
+	case Cursor::Type::SPELLBOOK: return getPivotOffsetSpellcast();
+	};
+
+	assert(0);
+	return {0, 0};
+}
+
+std::shared_ptr<IImage> CursorHandler::getCurrentImage()
+{
+	if (dndObject)
+		return dndObject;
+
+	return cursors[static_cast<size_t>(type)]->getImage(frame);
+}
+
+void CursorHandler::centerCursor()
+{
+	Point screenSize {screen->w, screen->h};
+	pos = screenSize / 2 - getPivotOffset();
+
+	SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE);
+	SDL_WarpMouse(pos.x, pos.y);
+	SDL_EventState(SDL_MOUSEMOTION, SDL_ENABLE);
+
+	cursor->setCursorPosition(pos);
+}
+
+void CursorHandler::updateSpellcastCursor()
+{
+	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);
+}
+
+void CursorHandler::render()
+{
+	if(!showing)
+		return;
+
+	if (type == Cursor::Type::SPELLBOOK)
+		updateSpellcastCursor();
+
+	cursor->render();
+}
+
+void CursorSoftware::render()
+{
+	//texture must be updated in the main (renderer) thread, but changes to cursor type may come from other threads
+	if (needUpdate)
+		updateTexture();
+
+	Point renderPos = pos - pivot;
+
+	SDL_Rect destRect;
+	destRect.x = renderPos.x;
+	destRect.y = renderPos.y;
+	destRect.w = 40;
+	destRect.h = 40;
+
+	SDL_RenderCopy(mainRenderer, cursorTexture, nullptr, &destRect);
+}
+
+void CursorSoftware::createTexture(const Point & dimensions)
+{
+	if(cursorTexture)
+		SDL_DestroyTexture(cursorTexture);
+
+	if (cursorSurface)
+		SDL_FreeSurface(cursorSurface);
+
+	cursorSurface = CSDL_Ext::newSurface(dimensions.x, dimensions.y);
+	cursorTexture = SDL_CreateTexture(mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, dimensions.x, dimensions.y);
+
+	SDL_SetSurfaceBlendMode(cursorSurface, SDL_BLENDMODE_NONE);
+	SDL_SetTextureBlendMode(cursorTexture, SDL_BLENDMODE_BLEND);
+}
+
+void CursorSoftware::updateTexture()
+{
+	Point dimensions(-1, -1);
+
+	if (!cursorSurface ||  Point(cursorSurface->w, cursorSurface->h) != cursorImage->dimensions())
+		createTexture(cursorImage->dimensions());
+
+	Uint32 fillColor = SDL_MapRGBA(cursorSurface->format, 0, 0, 0, 0);
+	CSDL_Ext::fillRect(cursorSurface, nullptr, fillColor);
+
+	cursorImage->draw(cursorSurface);
+	SDL_UpdateTexture(cursorTexture, NULL, cursorSurface->pixels, cursorSurface->pitch);
+	needUpdate = false;
+}
+
+void CursorSoftware::setImage(std::shared_ptr<IImage> image, const Point & pivotOffset)
+{
+	assert(image != nullptr);
+	cursorImage = image;
+	pivot = pivotOffset;
+	needUpdate = true;
+}
+
+void CursorSoftware::setCursorPosition( const Point & newPos )
+{
+	pos = newPos;
+}
+
+CursorSoftware::CursorSoftware():
+	cursorTexture(nullptr),
+	cursorSurface(nullptr),
+	needUpdate(false),
+	pivot(0,0)
+{
+	SDL_ShowCursor(SDL_DISABLE);
+}
+
+CursorSoftware::~CursorSoftware()
+{
+	if(cursorTexture)
+		SDL_DestroyTexture(cursorTexture);
+
+	if (cursorSurface)
+		SDL_FreeSurface(cursorSurface);
+
+}
+
+CursorHardware::CursorHardware():
+	cursor(nullptr)
+{
+}
+
+CursorHardware::~CursorHardware()
+{
+	if(cursor)
+		SDL_FreeCursor(cursor);
+}
+
+void CursorHardware::setImage(std::shared_ptr<IImage> image, const Point & pivotOffset)
+{
+	auto cursorSurface = CSDL_Ext::newSurface(image->dimensions().x, image->dimensions().y);
+
+	Uint32 fillColor = SDL_MapRGBA(cursorSurface->format, 0, 0, 0, 0);
+	CSDL_Ext::fillRect(cursorSurface, nullptr, fillColor);
+
+	image->draw(cursorSurface);
+
+	auto oldCursor = cursor;
+	cursor = SDL_CreateColorCursor(cursorSurface, pivotOffset.x, pivotOffset.y);
+
+	if (!cursor)
+		logGlobal->error("Failed to set cursor! SDL says %s", SDL_GetError());
+
+	SDL_FreeSurface(cursorSurface);
+	SDL_SetCursor(cursor);
+
+	if (oldCursor)
+		SDL_FreeCursor(oldCursor);
+}
+
+void CursorHardware::setCursorPosition( const Point & newPos )
+{
+	//no-op
+}
+
+void CursorHardware::render()
+{
+	//no-op
+}

+ 233 - 0
client/gui/CursorHandler.h

@@ -0,0 +1,233 @@
+/*
+ * CCursorHandler.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
+
+class CAnimation;
+class IImage;
+struct SDL_Surface;
+struct SDL_Texture;
+struct SDL_Cursor;
+
+#include "Geometries.h"
+
+namespace Cursor
+{
+	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,
+
+		COUNT
+	};
+
+	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,
+
+		COUNT
+	};
+
+	enum class Spellcast {
+		SPELL = 0,
+	};
+}
+
+class ICursor
+{
+public:
+	virtual ~ICursor() = default;
+
+	virtual void setImage(std::shared_ptr<IImage> image, const Point & pivotOffset) = 0;
+	virtual void setCursorPosition( const Point & newPos ) = 0;
+	virtual void render() = 0;
+};
+
+class CursorHardware : public ICursor
+{
+	std::shared_ptr<IImage> cursorImage;
+
+	SDL_Cursor * cursor;
+
+public:
+	CursorHardware();
+	~CursorHardware();
+
+	void setImage(std::shared_ptr<IImage> image, const Point & pivotOffset) override;
+	void setCursorPosition( const Point & newPos ) override;
+	void render() override;
+};
+
+class CursorSoftware : public ICursor
+{
+	std::shared_ptr<IImage> cursorImage;
+
+	SDL_Texture * cursorTexture;
+	SDL_Surface * cursorSurface;
+
+	Point pos;
+	Point pivot;
+	bool needUpdate;
+
+	void createTexture(const Point & dimensions);
+	void updateTexture();
+public:
+	CursorSoftware();
+	~CursorSoftware();
+
+	void setImage(std::shared_ptr<IImage> image, const Point & pivotOffset) override;
+	void setCursorPosition( const Point & newPos ) override;
+	void render() override;
+};
+
+/// handles mouse cursor
+class CursorHandler final
+{
+	std::shared_ptr<IImage> dndObject; //if set, overrides currentCursor
+
+	std::array<std::unique_ptr<CAnimation>, 4> cursors;
+
+	bool showing;
+
+	/// Current cursor
+	Cursor::Type type;
+	size_t frame;
+	float frameTime;
+	Point pos;
+
+	void changeGraphic(Cursor::Type type, size_t index);
+
+	Point getPivotOffsetDefault(size_t index);
+	Point getPivotOffsetMap(size_t index);
+	Point getPivotOffsetCombat(size_t index);
+	Point getPivotOffsetSpellcast();
+	Point getPivotOffset();
+
+	void updateSpellcastCursor();
+
+	std::shared_ptr<IImage> getCurrentImage();
+
+	std::unique_ptr<ICursor> cursor;
+
+	static std::unique_ptr<ICursor> createCursor();
+public:
+	CursorHandler();
+	~CursorHandler();
+
+	/// Replaces the cursor with a custom image.
+	/// @param image Image to replace cursor with or nullptr to use the normal cursor.
+	void dragAndDropCursor(std::shared_ptr<IImage> image);
+
+	void dragAndDropCursor(std::string path, size_t index);
+
+	/// 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; };
+	void show() { showing=true; };
+
+	/// change cursor's positions to (x, y)
+	void cursorMove(const int & x, const int & y);
+	/// Move cursor to screen center
+	void centerCursor();
+
+};

+ 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);
 }
+

+ 40 - 53
client/gui/Geometries.h

@@ -9,13 +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
 {
@@ -26,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
@@ -73,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;
@@ -96,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;
 	}
@@ -121,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 Point &p) const
+	{
+		return Rect(x+p.x,y+p.y,w,h);
 	}
-	Rect& operator=(const Rect &p) //assignment operator
+
+	Rect& operator=(const Rect &p)
 	{
 		x = p.x;
 		y = p.y;
@@ -182,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;

+ 74 - 38
client/gui/InterfaceObjectConfigurable.cpp

@@ -13,6 +13,7 @@
 #include "InterfaceObjectConfigurable.h"
 
 #include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
 #include "../gui/CAnimation.h"
 #include "../gui/CGuiHandler.h"
 #include "../widgets/CComponent.h"
@@ -25,11 +26,20 @@
 
 #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)
 {
-	init(config);
+	build(config);
 }
 
 InterfaceObjectConfigurable::InterfaceObjectConfigurable(int used, Point offset):
@@ -57,19 +67,32 @@ void InterfaceObjectConfigurable::addCallback(const std::string & callbackName,
 	callbacks[callbackName] = callback;
 }
 
-void InterfaceObjectConfigurable::init(const JsonNode &config)
+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");
-	for(auto & item : config["variables"].Struct())
+	auto * items = &config;
+	
+	if(config.getType() == JsonNode::JsonType::DATA_STRUCT)
 	{
-		logGlobal->debug("Read variable named %s", item.first);
-		variables[item.first] = item.second;
+		for(auto & item : config["variables"].Struct())
+		{
+			logGlobal->debug("Read variable named %s", item.first);
+			variables[item.first] = item.second;
+		}
+		
+		items = &config["items"];
 	}
 	
-	int unnamedObjectId = 0;
 	const std::string unnamedObjectPrefix = "__widget_";
-	for(const auto & item : config["items"].Vector())
+	for(const auto & item : items->Vector())
 	{
 		std::string name = item["name"].isNull()
 						? unnamedObjectPrefix + std::to_string(unnamedObjectId++)
@@ -84,27 +107,9 @@ std::string InterfaceObjectConfigurable::readText(const JsonNode & config) const
 	if(config.isNull())
 		return "";
 	
-	if(config.isNumber())
-	{
-		logGlobal->debug("Reading text from generaltext handler id:%d", config.Integer());
-		return CGI->generaltexth->allTexts[config.Integer()];
-	}
-	
-	const std::string delimiter = "/";
 	std::string s = config.String();
 	logGlobal->debug("Reading text from translations by key: %s", s);
-	JsonNode translated = CGI->generaltexth->localizedTexts;
-	for(size_t p = s.find(delimiter); p != std::string::npos; p = s.find(delimiter))
-	{
-		translated = translated[s.substr(0, p)];
-		s.erase(0, p + delimiter.length());
-	}
-	if(s == config.String())
-	{
-		logGlobal->warn("Reading non-translated text: %s", s);
-		return s;
-	}
-	return translated[s].String();
+	return CGI->generaltexth->translate(s);
 }
 
 Point InterfaceObjectConfigurable::readPosition(const JsonNode & config) const
@@ -189,12 +194,6 @@ std::pair<std::string, std::string> InterfaceObjectConfigurable::readHintText(co
 	std::pair<std::string, std::string> result;
 	if(!config.isNull())
 	{
-		if(config.isNumber())
-		{
-			logGlobal->debug("Reading hint text (zelp) from generaltext handler id:%d", config.Integer());
-			return CGI->generaltexth->zelp[config.Integer()];
-		}
-		
 		if(config.getType() == JsonNode::JsonType::DATA_STRUCT)
 		{
 			result.first = readText(config["hover"]);
@@ -203,13 +202,31 @@ std::pair<std::string, std::string> InterfaceObjectConfigurable::readHintText(co
 		}
 		if(config.getType() == JsonNode::JsonType::DATA_STRING)
 		{
-			logGlobal->debug("Reading non-translated hint: %s", config.String());
-			result.first = result.second = config.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");
@@ -218,6 +235,9 @@ std::shared_ptr<CPicture> InterfaceObjectConfigurable::buildPicture(const JsonNo
 	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;
 }
 
@@ -260,8 +280,8 @@ std::shared_ptr<CToggleButton> InterfaceObjectConfigurable::buildToggleButton(co
 	logGlobal->debug("Building widget CToggleButton");
 	auto position = readPosition(config["position"]);
 	auto image = config["image"].String();
-	auto zelp = readHintText(config["zelp"]);
-	auto button = std::make_shared<CToggleButton>(position, image, zelp);
+	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())
@@ -280,8 +300,8 @@ std::shared_ptr<CButton> InterfaceObjectConfigurable::buildButton(const JsonNode
 	logGlobal->debug("Building widget CButton");
 	auto position = readPosition(config["position"]);
 	auto image = config["image"].String();
-	auto zelp = readHintText(config["zelp"]);
-	auto button = std::make_shared<CButton>(position, image, zelp);
+	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())
@@ -289,8 +309,24 @@ std::shared_ptr<CButton> InterfaceObjectConfigurable::buildButton(const JsonNode
 			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;
 }
 

+ 5 - 1
client/gui/InterfaceObjectConfigurable.h

@@ -39,7 +39,7 @@ protected:
 	void registerBuilder(const std::string &, BuilderFunction);
 	
 	//must be called after adding callbacks
-	void init(const JsonNode & config);
+	void build(const JsonNode & config);
 	
 	void addCallback(const std::string & callbackName, std::function<void(int)> callback);
 	JsonNode variables;
@@ -52,6 +52,8 @@ protected:
 			return nullptr;
 		return std::dynamic_pointer_cast<T>(iter->second);
 	}
+	
+	void deleteWidget(const std::string & name);
 		
 	//basic serializers
 	Point readPosition(const JsonNode &) const;
@@ -61,6 +63,7 @@ protected:
 	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;
@@ -79,6 +82,7 @@ protected:
 	
 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;

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff