浏览代码

Introduced animation phases for beter ordering of visuals in battles

Ivan Savenko 2 年之前
父节点
当前提交
0020d76d1d

+ 5 - 8
client/CPlayerInterface.cpp

@@ -1017,7 +1017,7 @@ void CPlayerInterface::battleAttack(const BattleAttack * ba)
 
 	battleInt->effectsController->displayCustomEffects(ba->customEffects);
 
-	battleInt->waitForAnims();
+	battleInt->waitForAnimationCondition(EAnimationEvents::ACTION, false);
 
 	auto actionTarget = curAction->getTarget(cb.get());
 
@@ -1035,6 +1035,7 @@ void CPlayerInterface::battleAttack(const BattleAttack * ba)
 			{
 				const CStack * attacked = cb->battleGetStackByID(elem.stackAttacked);
 				battleInt->stackAttacking(attacker, attacked->getPosition(), attacked, true);
+				break;
 			}
 		}
 	}
@@ -1042,8 +1043,6 @@ void CPlayerInterface::battleAttack(const BattleAttack * ba)
 	{
 		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)
 		{
@@ -1058,17 +1057,15 @@ void CPlayerInterface::battleAttack(const BattleAttack * ba)
 
 		if(!ba->bsa.empty())
 		{
-			const CStack * attacked = cb->battleGetStackByID(ba->bsa.begin()->stackAttacked);
-			battleInt->stackAttacking(attacker, ba->counter() ? BattleHex(attackTarget + shift) : attackTarget, attacked, false);
+			const CStack * defender = cb->battleGetStackByID(ba->bsa.begin()->stackAttacked);
+			battleInt->stackAttacking(attacker, BattleHex(attackTarget + shift), defender, false);
 		}
 	}
 
-	//battleInt->waitForAnims(); //FIXME: freeze
+	//battleInt->waitForAnimationCondition(EAnimationEvents::ACTION, false); // 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;

+ 37 - 77
client/battle/BattleAnimationClasses.cpp

@@ -152,36 +152,18 @@ CDefenceAnimation::CDefenceAnimation(StackAttackedInfo _attackedInfo, BattleInte
 	: CBattleStackAnimation(owner, _attackedInfo.defender),
 	  attacker(_attackedInfo.attacker),
 	  rangedAttack(_attackedInfo.indirectAttack),
-	  killed(_attackedInfo.killed),
-	  timeToWait(0)
+	  killed(_attackedInfo.killed)
 {
 	logAnim->debug("Created defence anim for %s", _attackedInfo.defender->getName());
 }
 
 bool CDefenceAnimation::init()
 {
-	if(rangedAttack && attacker != nullptr && owner.projectilesController->hasActiveProjectile(attacker)) //delay hit animation
-	{
-		return false;
-	}
-
-	// synchronize animation with attacker, unless defending or attacked by shooter:
-	// wait for 1/2 of attack animation
-	if (!rangedAttack && getMyAnimType() != ECreatureAnimType::DEFENCE)
-	{
-		float frameLength = AnimationControls::getCreatureAnimationSpeed(
-								  stack->getCreature(), stackAnimation(stack).get(), getMyAnimType());
-
-		timeToWait = myAnim->framesInGroup(getMyAnimType()) * frameLength / 2;
+	logAnim->info("CDefenceAnimation::init: stack %d", stack->ID);
 
-		//FIXME: perhaps this should be pause instead?
-		myAnim->setType(ECreatureAnimType::HOLDING);
-	}
-	else
-	{
-		timeToWait = 0;
-		startAnimation();
-	}
+	CCS->soundh->playSound(getMySound());
+	myAnim->setType(getMyAnimType());
+	myAnim->onAnimationReset += [&](){ delete this; };
 
 	return true; //initialized successfuly
 }
@@ -212,25 +194,6 @@ ECreatureAnimType::Type CDefenceAnimation::getMyAnimType()
 		return ECreatureAnimType::HITTED;
 }
 
-void CDefenceAnimation::startAnimation()
-{
-	CCS->soundh->playSound(getMySound());
-	myAnim->setType(getMyAnimType());
-	myAnim->onAnimationReset += [&](){ delete this; };
-}
-
-void CDefenceAnimation::nextFrame()
-{
-	if (timeToWait > 0)
-	{
-		timeToWait -= float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000;
-		if (timeToWait <= 0)
-			startAnimation();
-	}
-
-	CBattleAnimation::nextFrame();
-}
-
 CDefenceAnimation::~CDefenceAnimation()
 {
 	if(killed)
@@ -274,19 +237,7 @@ bool CMeleeAttackAnimation::init()
 		return false;
 	}
 
-	bool toReverse = owner.getCurrentPlayerInterface()->cb->isToReverse(attackingStackPosBeforeReturn, attackedStack->getPosition(), stackFacingRight(stack), attackedStack->doubleWide(), stackFacingRight(attackedStack));
-
-	if(toReverse)
-	{
-		owner.stacksController->addNewAnim(new CReverseAnimation(owner, stack, attackingStackPosBeforeReturn));
-		return false;
-	}
-
-	// opponent must face attacker ( = different directions) before he can be attacked
-	if(attackingStack && attackedStack &&
-		stackFacingRight(attackingStack) == stackFacingRight(attackedStack))
-		return false;
-
+	logAnim->info("CMeleeAttackAnimation::init: stack %d -> stack %d", stack->ID, attackedStack->ID);
 	//reversed
 
 	shooting = false;
@@ -354,6 +305,20 @@ bool CMeleeAttackAnimation::init()
 	return true;
 }
 
+void CMeleeAttackAnimation::nextFrame()
+{
+	size_t currentFrame = stackAnimation(attackingStack)->getCurrentFrame();
+	size_t totalFrames = stackAnimation(attackingStack)->framesInGroup(group);
+
+	if ( currentFrame * 2 >= totalFrames )
+	{
+		if(owner.getAnimationCondition(EAnimationEvents::HIT) == false)
+			owner.setAnimationCondition(EAnimationEvents::HIT, true);
+	}
+
+	CAttackAnimation::nextFrame();
+}
+
 CMeleeAttackAnimation::CMeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked)
 	: CAttackAnimation(owner, attacker, _dest, _attacked)
 {
@@ -386,6 +351,8 @@ bool CMovementAnimation::init()
 		return false;
 	}
 
+	logAnim->info("CMovementAnimation::init: stack %d moves %d -> %d", stack->ID, oldPos, currentHex);
+
 	//reverse unit if necessary
 	if(owner.stacksController->shouldRotate(stack, oldPos, currentHex))
 	{
@@ -500,6 +467,8 @@ bool CMovementEndAnimation::init()
 		return false;
 	}
 
+	logAnim->info("CMovementEndAnimation::init: stack %d", stack->ID);
+
 	CCS->soundh->playSound(battle_sound(stack->getCreature(), endMoving));
 
 	if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_END))
@@ -508,6 +477,7 @@ bool CMovementEndAnimation::init()
 		return false;
 	}
 
+
 	myAnim->setType(ECreatureAnimType::MOVE_END);
 	myAnim->onAnimationReset += [&](){ delete this; };
 
@@ -539,6 +509,7 @@ bool CMovementStartAnimation::init()
 		return false;
 	}
 
+	logAnim->info("CMovementStartAnimation::init: stack %d", stack->ID);
 	CCS->soundh->playSound(battle_sound(stack->getCreature(), startMoving));
 
 	if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_START))
@@ -566,9 +537,10 @@ bool CReverseAnimation::init()
 		return false; //there is no such creature
 	}
 
+	logAnim->info("CReverseAnimation::init: stack %d", stack->ID);
 	if(myAnim->framesInGroup(ECreatureAnimType::TURN_L))
 	{
-		myAnim->setType(ECreatureAnimType::TURN_L);
+		myAnim->playOnce(ECreatureAnimType::TURN_L);
 		myAnim->onAnimationReset += std::bind(&CReverseAnimation::setupSecondPart, this);
 	}
 	else
@@ -578,12 +550,6 @@ bool CReverseAnimation::init()
 	return true;
 }
 
-CReverseAnimation::~CReverseAnimation()
-{
-	if( stack && stack->alive() )//don't do that if stack is dead
-		myAnim->setType(ECreatureAnimType::HOLDING);
-}
-
 void CBattleStackAnimation::rotateStack(BattleHex hex)
 {
 	setStackFacingRight(stack, !stackFacingRight(stack));
@@ -603,7 +569,7 @@ void CReverseAnimation::setupSecondPart()
 
 	if(myAnim->framesInGroup(ECreatureAnimType::TURN_R))
 	{
-		myAnim->setType(ECreatureAnimType::TURN_R);
+		myAnim->playOnce(ECreatureAnimType::TURN_R);
 		myAnim->onAnimationReset += [&](){ delete this; };
 	}
 	else
@@ -618,6 +584,7 @@ bool CResurrectionAnimation::init()
 		return false;
 	}
 
+	logAnim->info("CResurrectionAnimation::init: stack %d", stack->ID);
 	myAnim->playOnce(ECreatureAnimType::RESURRECTION);
 	myAnim->onAnimationReset += [&](){ delete this; };
 
@@ -630,16 +597,10 @@ CResurrectionAnimation::CResurrectionAnimation(BattleInterface & owner, const CS
 
 }
 
-CResurrectionAnimation::~CResurrectionAnimation()
-{
-
-}
-
 CRangedAttackAnimation::CRangedAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest_, const CStack * defender)
 	: CAttackAnimation(owner, attacker, dest_, defender),
 	  projectileEmitted(false)
 {
-	logAnim->info("Ranged attack animation created");
 }
 
 bool CRangedAttackAnimation::init()
@@ -655,7 +616,8 @@ bool CRangedAttackAnimation::init()
 		return false;
 	}
 
-	logAnim->info("Ranged attack animation initialized");
+	logAnim->info("CRangedAttackAnimation::init: stack %d", stack->ID);
+
 	setAnimationGroup();
 	initializeProjectile();
 	shooting = true;
@@ -726,18 +688,17 @@ void CRangedAttackAnimation::nextFrame()
 		if (owner.projectilesController->hasActiveProjectile(attackingStack))
 			stackAnimation(attackingStack)->pause();
 		else
+		{
 			stackAnimation(attackingStack)->play();
+			if(owner.getAnimationCondition(EAnimationEvents::HIT) == false)
+				owner.setAnimationCondition(EAnimationEvents::HIT, true);
+		}
 	}
 
 	CAttackAnimation::nextFrame();
 
 	if (!projectileEmitted)
 	{
-		logAnim->info("Ranged attack executing, %d / %d / %d",
-					  stackAnimation(attackingStack)->getCurrentFrame(),
-					  getAttackClimaxFrame(),
-					  stackAnimation(attackingStack)->framesInGroup(group));
-
 		// emit projectile once animation playback reached "climax" frame
 		if ( stackAnimation(attackingStack)->getCurrentFrame() >= getAttackClimaxFrame() )
 		{
@@ -750,7 +711,6 @@ void CRangedAttackAnimation::nextFrame()
 
 CRangedAttackAnimation::~CRangedAttackAnimation()
 {
-	logAnim->info("Ranged attack animation is over");
 	//FIXME: this assert triggers under some unclear, rare conditions. Possibly - if game window is inactive and/or in foreground/minimized?
 	assert(!owner.projectilesController->hasActiveProjectile(attackingStack));
 	assert(projectileEmitted);
@@ -830,7 +790,6 @@ void CCatapultAnimation::createProjectile(const Point & from, const Point & dest
 	owner.projectilesController->createCatapultProjectile(attackingStack, from, dest);
 }
 
-
 CCastAnimation::CCastAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest_, const CStack * defender, const CSpell * spell)
 	: CRangedAttackAnimation(owner, attacker, dest_, defender),
 	  spell(spell)
@@ -908,6 +867,7 @@ CPointEffectAnimation::CPointEffectAnimation(BattleInterface & owner, soundBase:
 	soundFinished(false),
 	effectFinished(false)
 {
+	logAnim->info("CPointEffectAnimation::init: effect %s", animationName);
 }
 
 CPointEffectAnimation::CPointEffectAnimation(BattleInterface & owner, soundBase::soundID sound, std::string animationName, std::vector<BattleHex> hex, int effects):

+ 3 - 9
client/battle/BattleAnimationClasses.h

@@ -96,16 +96,12 @@ class CDefenceAnimation : public CBattleStackAnimation
 	ECreatureAnimType::Type 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();
@@ -128,6 +124,7 @@ class CMeleeAttackAnimation : public CAttackAnimation
 {
 public:
 	bool init() override;
+	void nextFrame() override;
 
 	CMeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked);
 };
@@ -187,13 +184,11 @@ public:
 /// Class responsible for animation of stack chaning direction (left <-> right)
 class CReverseAnimation : public CStackMoveAnimation
 {
+	void setupSecondPart();
 public:
 	bool init() override;
 
-	void setupSecondPart();
-
 	CReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest);
-	~CReverseAnimation();
 };
 
 /// Resurrects stack from dead state
@@ -202,8 +197,7 @@ class CResurrectionAnimation : public CBattleStackAnimation
 public:
 	bool init() override;
 
-	CResurrectionAnimation(BattleInterface & owner, const CStack * stack);
-	~CResurrectionAnimation();
+	CResurrectionAnimation(BattleInterface & owner, const CStack * _stack);
 };
 
 class CRangedAttackAnimation : public CAttackAnimation

+ 33 - 23
client/battle/BattleConstants.h

@@ -9,6 +9,16 @@
  */
 #pragma once
 
+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
+	ATTACK      = 3, // attack and defense animations are playing
+	HIT         = 4, // hit & death animations are playing
+	PROJECTILES = 5, // there are any flying projectiles
+	COUNT
+};
+
 namespace EBattleEffect
 {
 	enum EBattleEffect
@@ -38,7 +48,7 @@ enum Type
 	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
+	CAST_SPELL = 4  // spellcasting
 };
 }
 
@@ -46,28 +56,28 @@ namespace ECreatureAnimType
 {
 enum Type // list of creature animations, numbers were taken from def files
 {
-	MOVING          =0,
-	MOUSEON         =1,
-	HOLDING         =2,
-	HITTED          =3,
-	DEFENCE         =4,
-	DEATH           =5,
-	DEATH_RANGED    =6,
-	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,
-	SHOOT_FRONT     =15,
-	SHOOT_DOWN      =16,
-	CAST_UP         =17,
-	CAST_FRONT      =18,
-	CAST_DOWN       =19,
-	MOVE_START      =20,
-	MOVE_END        =21,
+	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,  // 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,
+	SHOOT_FRONT     = 15,
+	SHOOT_DOWN      = 16,
+	CAST_UP         = 17,
+	CAST_FRONT      = 18,
+	CAST_DOWN       = 19,
+	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

+ 7 - 1
client/battle/BattleEffectsController.cpp

@@ -62,6 +62,8 @@ void BattleEffectsController::displayCustomEffects(const std::vector<CustomEffec
 
 void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bte)
 {
+	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+
 	const CStack * stack = owner.curInt->cb->battleGetStackByID(bte.stackID);
 	if(!stack)
 	{
@@ -95,11 +97,13 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt
 		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)
@@ -121,6 +125,8 @@ void BattleEffectsController::startAction(const BattleAction* action)
 			displayEffect(EBattleEffect::REGENERATION, soundBase::REGENER, actionTarget.at(0).hexValue);
 			break;
 	}
+
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 }
 
 void BattleEffectsController::collectRenderableObjects(BattleRenderer & renderer)

+ 62 - 22
client/battle/BattleInterface.cpp

@@ -42,19 +42,30 @@
 #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), moveSoundHander(-1), bresult(nullptr), battleActionsStarted(false)
+		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)
+	, moveSoundHander(-1)
+	, bresult(nullptr)
+	, battleActionsStarted(false)
 {
 	OBJ_CONSTRUCTION;
 
+	for ( auto & event : animationEvents)
+		event.setn(false);
+
 	if(spectatorInt)
 	{
 		curInt = spectatorInt;
@@ -65,7 +76,6 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *
 		curInt = defenderInt;
 	}
 
-	animsAreDisplayed.setn(false);
 	pos = myRect;
 	strongInterest = true;
 	givenCommand.setn(nullptr);
@@ -204,7 +214,9 @@ BattleInterface::~BattleInterface()
 		const auto & terrain = *(LOCPLINT->cb->getTile(adventureInt->selection->visitablePos())->terType);
 		CCS->musich->playMusicFromSet("terrain", terrain.name, true, false);
 	}
-	animsAreDisplayed.setn(false);
+	assert(getAnimationCondition(EAnimationEvents::ACTION) == false);
+
+	setAnimationCondition(EAnimationEvents::ACTION, false);
 }
 
 void BattleInterface::setPrintCellBorders(bool set)
@@ -367,7 +379,7 @@ void BattleInterface::stackAttacking( const CStack *attacker, BattleHex dest, co
 
 void BattleInterface::newRoundFirst( int round )
 {
-	waitForAnims();
+	waitForAnimationCondition(EAnimationEvents::ACTION, false);
 }
 
 void BattleInterface::newRound(int number)
@@ -455,10 +467,7 @@ void BattleInterface::gateStateChanged(const EGateState state)
 void BattleInterface::battleFinished(const BattleResult& br)
 {
 	bresult = &br;
-	{
-		auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim);
-		animsAreDisplayed.waitUntil(false);
-	}
+	waitForAnimationCondition(EAnimationEvents::ACTION, false);
 	stacksController->setActiveStack(nullptr);
 	displayBattleFinished();
 }
@@ -519,7 +528,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 		}
 	}
 
-	waitForAnims(); //wait for projectile animation
+	waitForAnimationCondition(EAnimationEvents::ACTION, false);//wait for projectile animation
 
 	displaySpellHit(spellID, sc->tile);
 
@@ -539,7 +548,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 			effectsController->displayEffect(EBattleEffect::EBattleEffect(elem.effect), stack->getPosition());
 	}
 
-	waitForAnims();
+	waitForAnimationCondition(EAnimationEvents::ACTION, false);
 	//mana absorption
 	if (sc->manaGained > 0)
 	{
@@ -548,7 +557,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 		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));
 	}
-	waitForAnims();
+	waitForAnimationCondition(EAnimationEvents::ACTION, false);
 }
 
 void BattleInterface::battleStacksEffectsSet(const SetStackEffect & sse)
@@ -776,12 +785,6 @@ 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);
@@ -800,7 +803,7 @@ void BattleInterface::tacticNextStack(const CStack * current)
 		current = stacksController->getActiveStack();
 
 	//no switching stacks when the current one is moving
-	waitForAnims();
+	waitForAnimationCondition(EAnimationEvents::ACTION, false);
 
 	TStacks stacksOfMine = tacticianInterface->cb->battleGetStacks(CBattleCallback::ONLY_MINE);
 	vstd::erase_if (stacksOfMine, &immobile);
@@ -959,3 +962,40 @@ void BattleInterface::castThisSpell(SpellID spellID)
 {
 	actionsController->castThisSpell(spellID);
 }
+
+void BattleInterface::setAnimationCondition( EAnimationEvents event, bool state)
+{
+	logAnim->info("setAnimationCondition: %d -> %s", static_cast<int>(event), state ? "ON" : "OFF");
+
+	size_t index = static_cast<size_t>(event);
+	animationEvents[index].setn(state);
+
+	for (auto it = awaitingEvents.begin(); it != awaitingEvents.end();)
+	{
+		if (it->event == event && it->eventState == state)
+		{
+			it->action();
+			it = awaitingEvents.erase(it);
+		}
+		else
+			++it;
+	}
+}
+
+bool BattleInterface::getAnimationCondition( EAnimationEvents event)
+{
+	size_t index = static_cast<size_t>(event);
+	return animationEvents[index].get();
+}
+
+void BattleInterface::waitForAnimationCondition( EAnimationEvents event, bool state)
+{
+	auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim);
+	size_t index = static_cast<size_t>(event);
+	animationEvents[index].waitUntil(state);
+}
+
+void BattleInterface::executeOnAnimationCondition( EAnimationEvents event, bool state, const AwaitingAnimationAction & action)
+{
+	awaitingEvents.push_back({action, event, state});
+}

+ 27 - 3
client/battle/BattleInterface.h

@@ -12,6 +12,7 @@
 #include "BattleConstants.h"
 #include "../gui/CIntObject.h"
 #include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation
+#include "../../lib/CondSh.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -21,7 +22,6 @@ class CStack;
 struct BattleResult;
 struct BattleSpellCast;
 struct CObstacleInstance;
-template <typename T> struct CondSh;
 struct SetStackEffect;
 class BattleAction;
 class CGTownInstance;
@@ -95,6 +95,20 @@ private:
 	bool battleActionsStarted; //used for delaying battle actions until intro sound stops
 	int battleIntroSoundChannel; //required as variable for disabling it via ESC key
 
+	using AwaitingAnimationAction = std::function<void()>;
+
+	struct AwaitingAnimationEvents {
+		AwaitingAnimationAction action;
+		EAnimationEvents event;
+		bool eventState;
+	};
+
+	/// Conditional variables that are set depending on ongoing animations on the battlefield
+	std::array< CondSh<bool>, static_cast<size_t>(EAnimationEvents::COUNT)> animationEvents;
+
+	/// List of events that are waiting to be triggered
+	std::vector<AwaitingAnimationEvents> awaitingEvents;
+
 	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();
@@ -116,7 +130,6 @@ 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
 	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)
@@ -136,7 +149,18 @@ public:
 
 	void tacticNextStack(const CStack *current);
 	void tacticPhaseEnd();
-	void waitForAnims();
+
+	/// sets condition to targeted state and executes any awaiting actions
+	void setAnimationCondition( EAnimationEvents event, bool state);
+
+	/// returns current state of condition
+	bool getAnimationCondition( EAnimationEvents event);
+
+	/// locks execution until selected condition reached targeted state
+	void waitForAnimationCondition( EAnimationEvents event, bool state);
+
+	/// adds action that will be executed one selected condition reached targeted state
+	void executeOnAnimationCondition( EAnimationEvents event, bool state, const AwaitingAnimationAction & action);
 
 	//napisz tu klase odpowiadajaca za wyswietlanie bitwy i obsluge uzytkownika, polecenia ma przekazywac callbackiem
 	void activate() override;

+ 1 - 1
client/battle/BattleObstacleController.cpp

@@ -107,7 +107,7 @@ void BattleObstacleController::obstaclePlaced(const std::vector<std::shared_ptr<
 		owner.stacksController->addNewAnim(new CPointEffectAnimation(owner, soundBase::invalid, spellObstacle->appearAnimation, whereTo, oi->pos, CPointEffectAnimation::WAIT_FOR_SOUND));
 
 		//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);

+ 0 - 1
client/battle/BattleProjectileController.cpp

@@ -48,7 +48,6 @@ 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);
 

+ 2 - 1
client/battle/BattleSiegeController.cpp

@@ -347,7 +347,8 @@ void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca)
 		owner.stacksController->addNewAnim(new CPointEffectAnimation(owner, soundBase::WALLHIT, "SGEXPL.DEF", positions));
 	}
 
-	owner.waitForAnims();
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.setAnimationCondition(EAnimationEvents::HIT, false);
 
 	for (auto attackInfo : ca.attackedParts)
 	{

+ 66 - 31
client/battle/BattleStacksController.cpp

@@ -148,8 +148,8 @@ void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer)
 void BattleStacksController::stackReset(const CStack * stack)
 {
 	//FIXME: there should be no more ongoing animations. If not - then some other method created animations but did not wait for them to end
-	assert(!owner.animsAreDisplayed.get());
-	owner.waitForAnims();
+	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 
 	auto iter = stackAnimation.find(stack->ID);
 
@@ -173,7 +173,7 @@ void BattleStacksController::stackReset(const CStack * stack)
 		animation->shiftColor(&shifterClone);
 	}
 
-	owner.waitForAnims();
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 }
 
 void BattleStacksController::stackAdded(const CStack * stack)
@@ -355,6 +355,7 @@ void BattleStacksController::updateBattleAnimations()
 			elem->tryInitialize();
 	}
 
+
 	bool hadAnimations = !currentAnimations.empty();
 	vstd::erase(currentAnimations, nullptr);
 
@@ -362,20 +363,20 @@ void BattleStacksController::updateBattleAnimations()
 	{
 		//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);
+	owner.setAnimationCondition(EAnimationEvents::ACTION, true);
 }
 
 void BattleStacksController::stackActivated(const CStack *stack) //TODO: check it all before game state is changed due to abilities
 {
 	stackToActivate = stack;
-	owner.waitForAnims();
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 	if (stackToActivate) //during waiting stack may have gotten activated through show
 		owner.activateStack();
 }
@@ -412,17 +413,34 @@ void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> at
 			addNewAnim(new CReverseAnimation(owner, attackedInfo.defender, attackedInfo.defender->getPosition()));
 	}
 
+	// raise flag that movement phase started, starting any queued movements
+	owner.setAnimationCondition(EAnimationEvents::MOVEMENT, true);
+
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.setAnimationCondition(EAnimationEvents::MOVEMENT, false);
+
 	for(auto & attackedInfo : attackedInfos)
 	{
-		addNewAnim(new CDefenceAnimation(attackedInfo, owner));
+		bool useDefenceAnim = attackedInfo.defender->defendingAnim && !attackedInfo.indirectAttack && !attackedInfo.killed;
 
-		if (attackedInfo.battleEffect != EBattleEffect::INVALID)
-			owner.effectsController->displayEffect(EBattleEffect::EBattleEffect(attackedInfo.battleEffect), attackedInfo.defender->getPosition());
+		EAnimationEvents usedEvent = useDefenceAnim ? EAnimationEvents::ATTACK : EAnimationEvents::HIT;
 
-		if (attackedInfo.spellEffect != SpellID::NONE)
-			owner.displaySpellEffect(attackedInfo.spellEffect, attackedInfo.defender->getPosition());
+		owner.executeOnAnimationCondition(usedEvent, true, [=]()
+		{
+			addNewAnim(new CDefenceAnimation(attackedInfo, owner));
+
+			if (attackedInfo.battleEffect != EBattleEffect::INVALID)
+				owner.effectsController->displayEffect(EBattleEffect::EBattleEffect(attackedInfo.battleEffect), attackedInfo.defender->getPosition());
+
+			if (attackedInfo.spellEffect != SpellID::NONE)
+				owner.displaySpellEffect(attackedInfo.spellEffect, attackedInfo.defender->getPosition());
+		});
 	}
-	owner.waitForAnims();
+
+	owner.setAnimationCondition(EAnimationEvents::ATTACK, true);
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.setAnimationCondition(EAnimationEvents::ATTACK, false);
+	owner.setAnimationCondition(EAnimationEvents::HIT, false);
 
 	for (auto & attackedInfo : attackedInfos)
 	{
@@ -435,7 +453,7 @@ void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> at
 		if (attackedInfo.cloneKilled)
 			stackRemoved(attackedInfo.defender->ID);
 	}
-	owner.waitForAnims();
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 }
 
 void BattleStacksController::stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance)
@@ -443,23 +461,27 @@ void BattleStacksController::stackMoved(const CStack *stack, std::vector<BattleH
 	assert(destHex.size() > 0);
 
 	//FIXME: there should be no more ongoing animations. If not - then some other method created animations but did not wait for them to end
-	assert(!owner.animsAreDisplayed.get());
+	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
 
 	if(shouldRotate(stack, stack->getPosition(), destHex[0]))
 		addNewAnim(new CReverseAnimation(owner, stack, destHex[0]));
 
 	addNewAnim(new CMovementStartAnimation(owner, stack));
-	owner.waitForAnims();
+
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 
 	addNewAnim(new CMovementAnimation(owner, stack, destHex, distance));
-	owner.waitForAnims();
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 
 	addNewAnim(new CMovementEndAnimation(owner, stack, destHex.back()));
-	owner.waitForAnims();
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 }
 
 void BattleStacksController::stackAttacking( const CStack *attacker, BattleHex dest, const CStack *defender, bool shooting )
 {
+	//FIXME: there should be no more ongoing animations. If not - then some other method created animations but did not wait for them to end
+	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+
 	bool needsReverse =
 			owner.curInt->cb->isToReverse(
 				attacker->getPosition(),
@@ -469,21 +491,26 @@ void BattleStacksController::stackAttacking( const CStack *attacker, BattleHex d
 				facingRight(defender));
 
 	if (needsReverse)
-		addNewAnim(new CReverseAnimation(owner, attacker, attacker->getPosition()));
-
-	owner.waitForAnims();
-
-	if (shooting)
 	{
-		addNewAnim(new CShootingAnimation(owner, attacker, dest, defender));
+		owner.executeOnAnimationCondition(EAnimationEvents::MOVEMENT, true, [=]()
+		{
+			addNewAnim(new CReverseAnimation(owner, attacker, attacker->getPosition()));
+		});
 	}
-	else
+
+	owner.executeOnAnimationCondition(EAnimationEvents::ATTACK, true, [=]()
 	{
-		addNewAnim(new CMeleeAttackAnimation(owner, attacker, dest, defender));
-	}
+		if (shooting)
+		{
+			addNewAnim(new CShootingAnimation(owner, attacker, dest, defender));
+		}
+		else
+		{
+			addNewAnim(new CMeleeAttackAnimation(owner, attacker, dest, defender));
+		}
+	});
 
-	// do not wait - waiting will be done at stacksAreAttacked
-	// waitForAnims();
+	//waiting will be done in stacksAreAttacked
 }
 
 bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const
@@ -503,8 +530,8 @@ bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex
 void BattleStacksController::endAction(const BattleAction* action)
 {
 	//FIXME: there should be no more ongoing animations. If not - then some other method created animations but did not wait for them to end
-	assert(!owner.animsAreDisplayed.get());
-	owner.waitForAnims();
+	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 
 	//check if we should reverse stacks
 	TStacks stacks = owner.curInt->cb->battleGetStacks(CBattleCallback::MINE_AND_ENEMY);
@@ -518,7 +545,15 @@ void BattleStacksController::endAction(const BattleAction* action)
 			addNewAnim(new CReverseAnimation(owner, s, s->getPosition()));
 		}
 	}
-	owner.waitForAnims();
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+
+	//FIXME: there should be no more ongoing animations. If not - then some other method created animations but did not wait for them to end
+	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);
+	assert(owner.getAnimationCondition(EAnimationEvents::PROJECTILES) == false);
 }
 
 void BattleStacksController::startAction(const BattleAction* action)

+ 2 - 0
lib/CondSh.h

@@ -18,6 +18,8 @@ template <typename T> struct CondSh
 	boost::condition_variable cond;
 	boost::mutex mx;
 
+	CondSh() : data(T()) {}
+
 	CondSh(T t) : data(t) {}
 
 	// set data