فهرست منبع

Multiple fixes & improvements to animation ordering

Ivan Savenko 2 سال پیش
والد
کامیت
60a00b450e

+ 24 - 79
client/CPlayerInterface.cpp

@@ -986,91 +986,36 @@ void CPlayerInterface::battleAttack(const BattleAttack * ba)
 
 	assert(curAction);
 
-	const CStack * attacker = cb->battleGetStackByID(ba->stackAttacking);
-
-	if(!attacker)
-	{
-		logGlobal->error("Attacking stack not found");
-		return;
-	}
-
-	if(ba->lucky()) //lucky hit
-	{
-		battleInt->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())
-	{
-		battleInt->controlPanel->console->addText(attacker->formatGeneralMessage(365));
-		for(auto & elem : ba->bsa)
-		{
-			const CStack * attacked = cb->battleGetStackByID(elem.stackAttacked);
-			battleInt->effectsController->displayEffect(EBattleEffect::DEATH_BLOW, attacked->getPosition());
-		}
-		CCS->soundh->playSound(soundBase::deathBlow);
-	}
-
-	battleInt->effectsController->displayCustomEffects(ba->customEffects);
-
-	battleInt->waitForAnimationCondition(EAnimationEvents::ACTION, false);
-
-	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)
+	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 (ba->spellLike())
+		info.spellEffect = ba->spellID;
+
+	for(auto & elem : ba->bsa)
+	{
+		if(!elem.isSecondary())
 		{
-			if(!elem.isSecondary()) //display projectile only for primary target
-			{
-				const CStack * attacked = cb->battleGetStackByID(elem.stackAttacked);
-				battleInt->stackAttacking(attacker, attacked->getPosition(), attacked, true);
-				break;
-			}
+			assert(info.defender == nullptr);
+			info.defender = cb->battleGetStackByID(elem.stackAttacked);
 		}
-	}
-	else
-	{
-		auto attackTarget = actionTarget.at(1).hexValue;
-
-		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 * defender = cb->battleGetStackByID(ba->bsa.begin()->stackAttacked);
-			battleInt->stackAttacking(attacker, BattleHex(attackTarget + shift), defender, false);
+			info.secondaryDefender.push_back(cb->battleGetStackByID(elem.stackAttacked));
 		}
 	}
+	assert(info.defender != nullptr);
+	assert(info.attacker != nullptr);
 
-	//battleInt->waitForAnimationCondition(EAnimationEvents::ACTION, false); // FIXME: freeze
-
-	if(ba->spellLike())
-	{
-		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)

+ 21 - 8
client/battle/BattleAnimationClasses.cpp

@@ -159,7 +159,7 @@ CDefenceAnimation::CDefenceAnimation(StackAttackedInfo _attackedInfo, BattleInte
 
 bool CDefenceAnimation::init()
 {
-	logAnim->info("CDefenceAnimation::init: stack %d", stack->ID);
+	logAnim->info("CDefenceAnimation::init: stack %s", stack->getName());
 
 	CCS->soundh->playSound(getMySound());
 	myAnim->setType(getMyAnimType());
@@ -231,13 +231,16 @@ void CDummyAnimation::nextFrame()
 
 bool CMeleeAttackAnimation::init()
 {
+	assert(attackingStack);
+	assert(!myAnim->isDeadOrDying());
+
 	if(!attackingStack || myAnim->isDeadOrDying())
 	{
 		delete this;
 		return false;
 	}
 
-	logAnim->info("CMeleeAttackAnimation::init: stack %d -> stack %d", stack->ID, attackedStack->ID);
+	logAnim->info("CMeleeAttackAnimation::init: stack %s -> stack %s", stack->getName(), attackedStack->getName());
 	//reversed
 
 	shooting = false;
@@ -351,7 +354,7 @@ bool CMovementAnimation::init()
 		return false;
 	}
 
-	logAnim->info("CMovementAnimation::init: stack %d moves %d -> %d", stack->ID, oldPos, currentHex);
+	logAnim->info("CMovementAnimation::init: stack %s moves %d -> %d", stack->getName(), oldPos, currentHex);
 
 	//reverse unit if necessary
 	if(owner.stacksController->shouldRotate(stack, oldPos, currentHex))
@@ -467,7 +470,7 @@ bool CMovementEndAnimation::init()
 		return false;
 	}
 
-	logAnim->info("CMovementEndAnimation::init: stack %d", stack->ID);
+	logAnim->info("CMovementEndAnimation::init: stack %s", stack->getName());
 
 	CCS->soundh->playSound(battle_sound(stack->getCreature(), endMoving));
 
@@ -509,7 +512,7 @@ bool CMovementStartAnimation::init()
 		return false;
 	}
 
-	logAnim->info("CMovementStartAnimation::init: stack %d", stack->ID);
+	logAnim->info("CMovementStartAnimation::init: stack %s", stack->getName());
 	CCS->soundh->playSound(battle_sound(stack->getCreature(), startMoving));
 
 	if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_START))
@@ -531,13 +534,16 @@ CReverseAnimation::CReverseAnimation(BattleInterface & owner, const CStack * sta
 
 bool CReverseAnimation::init()
 {
+	assert(myAnim);
+	assert(!myAnim->isDeadOrDying());
+
 	if(myAnim == nullptr || myAnim->isDeadOrDying())
 	{
 		delete this;
 		return false; //there is no such creature
 	}
 
-	logAnim->info("CReverseAnimation::init: stack %d", stack->ID);
+	logAnim->info("CReverseAnimation::init: stack %s", stack->getName());
 	if(myAnim->framesInGroup(ECreatureAnimType::TURN_L))
 	{
 		myAnim->playOnce(ECreatureAnimType::TURN_L);
@@ -559,6 +565,8 @@ void CBattleStackAnimation::rotateStack(BattleHex hex)
 
 void CReverseAnimation::setupSecondPart()
 {
+	assert(stack);
+
 	if(!stack)
 	{
 		delete this;
@@ -578,13 +586,15 @@ void CReverseAnimation::setupSecondPart()
 
 bool CResurrectionAnimation::init()
 {
+	assert(stack);
+
 	if(!stack)
 	{
 		delete this;
 		return false;
 	}
 
-	logAnim->info("CResurrectionAnimation::init: stack %d", stack->ID);
+	logAnim->info("CResurrectionAnimation::init: stack %s", stack->getName());
 	myAnim->playOnce(ECreatureAnimType::RESURRECTION);
 	myAnim->onAnimationReset += [&](){ delete this; };
 
@@ -616,7 +626,7 @@ bool CRangedAttackAnimation::init()
 		return false;
 	}
 
-	logAnim->info("CRangedAttackAnimation::init: stack %d", stack->ID);
+	logAnim->info("CRangedAttackAnimation::init: stack %s", stack->getName());
 
 	setAnimationGroup();
 	initializeProjectile();
@@ -1067,6 +1077,9 @@ CWaitingAnimation::CWaitingAnimation(BattleInterface & owner):
 void CWaitingAnimation::nextFrame()
 {
 	// initialization conditions fulfilled, delay is over
+	if(owner.getAnimationCondition(EAnimationEvents::HIT) == false)
+		owner.setAnimationCondition(EAnimationEvents::HIT, true);
+
 	delete this;
 }
 

+ 9 - 6
client/battle/BattleConstants.h

@@ -10,12 +10,15 @@
 #pragma once
 
 enum class EAnimationEvents {
-	OPENING     = 0, // battle opening sound is playing
+	OPENING     = 0, // TODO 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
+	BEFORE_MOVE = 2, // TODO effects played before stack can act, e.g. regen or bad morale
+	MOVEMENT    = 3, // stacks are moving or turning around
+	BEFORE_HIT  = 4, // effects played before all attack/defence/hit animations
+	ATTACK      = 5, // attack and defence animations are playing
+	HIT         = 6, // hit & death animations are playing
+	AFTER_HIT   = 7, // after all hit & death animations are over
+	PROJECTILES = 8, // TODO there are any flying projectiles
 	COUNT
 };
 
@@ -30,7 +33,7 @@ namespace EBattleEffect
 		BAD_MORALE   = 30,
 		BAD_LUCK     = 48,
 		RESURRECT    = 50,
-		DRAIN_LIFE   = 52, // hardcoded constant in CGameHandler
+		DRAIN_LIFE   = 52,
 		POISON       = 67,
 		DEATH_BLOW   = 73,
 		REGENERATION = 74,

+ 45 - 24
client/battle/BattleInterface.cpp

@@ -372,9 +372,9 @@ void BattleInterface::stacksAreAttacked(std::vector<StackAttackedInfo> attackedI
 	}
 }
 
-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 )
@@ -490,6 +490,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 {
 	const SpellID spellID = sc->spellID;
 	const CSpell * spell = spellID.toSpell();
+	auto targetedTile = sc->tile;
 
 	assert(spell);
 	if(!spell)
@@ -498,7 +499,11 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 	const std::string & castSoundPath = spell->getCastSound();
 
 	if (!castSoundPath.empty())
-		CCS->soundh->playSound(castSoundPath);
+	{
+		executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() {
+			CCS->soundh->playSound(castSoundPath);
+		});
+	}
 
 	if ( sc->activeCast )
 	{
@@ -506,58 +511,74 @@ 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 CCastAnimation(*this, casterStack, targetedTile, curInt->cb->battleGetStackByPos(targetedTile), spell));
+				displaySpellCast(spellID, casterStack->getPosition());
+			});
 		}
 		else
-		if (sc->tile.isValid() && !spell->animationInfo.projectile.empty())
+		if (targetedTile.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);
+			const CStack * target = curInt->cb->battleGetStackByPos(targetedTile);
 			Point srccoord = (sc->side ? Point(770, 60) : Point(30, 60)) + pos;	//hero position
-			Point destcoord = stacksController->getStackPositionAtHex(sc->tile, target); //position attacked by projectile
+			Point destcoord = stacksController->getStackPositionAtHex(targetedTile, target); //position attacked by projectile
 			destcoord += Point(250, 240); // FIXME: what are these constants?
 
-			projectilesController->createSpellProjectile( nullptr, srccoord, destcoord, spell);
-			projectilesController->emitStackProjectile( nullptr );
-
-			// wait fo projectile to end
-			stacksController->addNewAnim(new CWaitingProjectileAnimation(*this, nullptr));
+			//FIXME: should be replaced with new hero cast animation type
+			executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]()
+			{
+				projectilesController->createSpellProjectile( nullptr, srccoord, destcoord, spell);
+				projectilesController->emitStackProjectile( nullptr );
+				stacksController->addNewAnim(new CWaitingProjectileAnimation(*this, nullptr));
+			});
 		}
 	}
 
-	waitForAnimationCondition(EAnimationEvents::ACTION, false);//wait for projectile animation
-
-	displaySpellHit(spellID, sc->tile);
+	executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+		displaySpellHit(spellID, 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(spellID, stack->getPosition());
+			});
+		}
 	}
 
-	//queuing additional animation
+	//queuing additional animation (magic mirror / resistance)
 	for(auto & elem : sc->customEffects)
 	{
 		auto stack = curInt->cb->battleGetStackByID(elem.stack, false);
+		assert(stack);
 		if(stack)
-			effectsController->displayEffect(EBattleEffect::EBattleEffect(elem.effect), stack->getPosition());
+		{
+			executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+				effectsController->displayEffect(EBattleEffect::EBattleEffect(elem.effect), stack->getPosition());
+			});
+		}
 	}
 
-	waitForAnimationCondition(EAnimationEvents::ACTION, false);
 	//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));
+		bool side = sc->side;
+
+		executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=](){
+			stacksController->addNewAnim(new CPointEffectAnimation(*this, soundBase::invalid, side ? "SP07_A.DEF" : "SP07_B.DEF", leftHero));
+			stacksController->addNewAnim(new CPointEffectAnimation(*this, soundBase::invalid, side ? "SP07_B.DEF" : "SP07_A.DEF", rightHero));
+		});
 	}
-	waitForAnimationCondition(EAnimationEvents::ACTION, false);
 }
 
 void BattleInterface::battleStacksEffectsSet(const SetStackEffect & sse)

+ 22 - 1
client/battle/BattleInterface.h

@@ -72,6 +72,24 @@ struct StackAttackedInfo
 	bool cloneKilled;
 };
 
+struct StackAttackInfo
+{
+	const CStack *attacker;
+	const CStack *defender;
+	std::vector< const CStack *> secondaryDefender;
+
+	//EBattleEffect::EBattleEffect battleEffect;
+	SpellID spellEffect;
+	BattleHex tile;
+
+	bool indirectAttack;
+	bool lucky;
+	bool unlucky;
+	bool deathBlow;
+	bool lifeDrain;
+};
+
+
 /// Big class which handles the overall battle interface actions and it is also responsible for
 /// drawing everything correctly.
 class BattleInterface : public WindowBase
@@ -121,6 +139,9 @@ private:
 	void showInterface(SDL_Surface * to);
 
 	void setHeroAnimation(ui8 side, int phase);
+
+	void executeSpellCast(); //called when a hero casts a spell
+
 public:
 	std::unique_ptr<BattleProjectileController> projectilesController;
 	std::unique_ptr<BattleSiegeController> siegeController;
@@ -182,7 +203,7 @@ public:
 	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 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

+ 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

+ 5 - 2
client/battle/BattleSiegeController.cpp

@@ -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
@@ -327,6 +327,10 @@ bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const
 
 void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca)
 {
+	//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);
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+
 	if (ca.attacker != -1)
 	{
 		const CStack *stack = owner.curInt->cb->battleGetStackByID(ca.attacker);
@@ -343,7 +347,6 @@ 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));
 	}
 

+ 110 - 31
client/battle/BattleStacksController.cpp

@@ -148,8 +148,11 @@ 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.getAnimationCondition(EAnimationEvents::ACTION) == false);
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	//assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+	//owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+
+	//reset orientation?
+	//stackFacingRight[stack->ID] = stack->side == BattleSide::ATTACKER;
 
 	auto iter = stackAnimation.find(stack->ID);
 
@@ -163,7 +166,10 @@ void BattleStacksController::stackReset(const CStack * stack)
 
 	if(stack->alive() && animation->isDeadOrDying())
 	{
-		addNewAnim(new CResurrectionAnimation(owner, stack));
+		owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=]()
+		{
+			addNewAnim(new CResurrectionAnimation(owner, stack));
+		});
 	}
 
 	static const ColorShifterMultiplyAndAdd shifterClone ({255, 255, 0, 255}, {0, 0, 255, 0});
@@ -173,7 +179,7 @@ void BattleStacksController::stackReset(const CStack * stack)
 		animation->shiftColor(&shifterClone);
 	}
 
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	//owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 }
 
 void BattleStacksController::stackAdded(const CStack * stack)
@@ -410,15 +416,14 @@ void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> at
 					facingRight(attackedInfo.attacker));
 
 		if (needsReverse)
-			addNewAnim(new CReverseAnimation(owner, attackedInfo.defender, attackedInfo.defender->getPosition()));
+		{
+			owner.executeOnAnimationCondition(EAnimationEvents::MOVEMENT, true, [=]()
+			{
+				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)
 	{
 		bool useDefenceAnim = attackedInfo.defender->defendingAnim && !attackedInfo.indirectAttack && !attackedInfo.killed;
@@ -437,23 +442,24 @@ void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> at
 		});
 	}
 
-	owner.setAnimationCondition(EAnimationEvents::ATTACK, true);
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
-	owner.setAnimationCondition(EAnimationEvents::ATTACK, false);
-	owner.setAnimationCondition(EAnimationEvents::HIT, false);
-
 	for (auto & attackedInfo : attackedInfos)
 	{
 		if (attackedInfo.rebirth)
 		{
-			owner.effectsController->displayEffect(EBattleEffect::RESURRECT, soundBase::RESURECT, attackedInfo.defender->getPosition());
-			addNewAnim(new CResurrectionAnimation(owner, attackedInfo.defender));
+			owner.executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=](){
+				owner.effectsController->displayEffect(EBattleEffect::RESURRECT, soundBase::RESURECT, attackedInfo.defender->getPosition());
+				addNewAnim(new CResurrectionAnimation(owner, attackedInfo.defender));
+			});
 		}
 
 		if (attackedInfo.cloneKilled)
-			stackRemoved(attackedInfo.defender->ID);
+		{
+			owner.executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=](){
+				stackRemoved(attackedInfo.defender->ID);
+			});
+		}
 	}
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	executeAttackAnimations();
 }
 
 void BattleStacksController::stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance)
@@ -477,40 +483,113 @@ void BattleStacksController::stackMoved(const CStack *stack, std::vector<BattleH
 	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 }
 
-void BattleStacksController::stackAttacking( const CStack *attacker, BattleHex dest, const CStack *defender, bool shooting )
+void BattleStacksController::stackAttacking( const StackAttackInfo & info )
 {
 	//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(),
-				defender->getPosition(),
-				facingRight(attacker),
-				attacker->doubleWide(),
-				facingRight(defender));
+				info.attacker->getPosition(),
+				info.defender->getPosition(),
+				facingRight(info.attacker),
+				info.attacker->doubleWide(),
+				facingRight(info.defender));
 
 	if (needsReverse)
 	{
 		owner.executeOnAnimationCondition(EAnimationEvents::MOVEMENT, true, [=]()
 		{
-			addNewAnim(new CReverseAnimation(owner, attacker, attacker->getPosition()));
+			addNewAnim(new CReverseAnimation(owner, info.attacker, info.attacker->getPosition()));
+		});
+	}
+
+	if(info.lucky)
+	{
+		owner.executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() {
+			owner.controlPanel->console->addText(info.attacker->formatGeneralMessage(-45));
+			owner.effectsController->displayEffect(EBattleEffect::GOOD_LUCK, soundBase::GOODLUCK, info.attacker->getPosition());
+		});
+	}
+
+	if(info.unlucky)
+	{
+		owner.executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() {
+			owner.controlPanel->console->addText(info.attacker->formatGeneralMessage(-44));
+			owner.effectsController->displayEffect(EBattleEffect::BAD_LUCK, soundBase::BADLUCK, info.attacker->getPosition());
+		});
+	}
+
+	if(info.deathBlow)
+	{
+		owner.executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() {
+			owner.controlPanel->console->addText(info.attacker->formatGeneralMessage(365));
+			owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, soundBase::deathBlow, info.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 (shooting)
+		if (info.indirectAttack)
 		{
-			addNewAnim(new CShootingAnimation(owner, attacker, dest, defender));
+			addNewAnim(new CShootingAnimation(owner, info.attacker, info.tile, info.defender));
 		}
 		else
 		{
-			addNewAnim(new CMeleeAttackAnimation(owner, attacker, dest, defender));
+			addNewAnim(new CMeleeAttackAnimation(owner, info.attacker, info.tile, info.defender));
 		}
 	});
 
-	//waiting will be done in stacksAreAttacked
+	if (info.spellEffect)
+	{
+		owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=]()
+		{
+			owner.displaySpellHit(info.spellEffect, info.tile);
+		});
+	}
+
+	if (info.lifeDrain)
+	{
+		owner.executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=]()
+		{
+			owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, soundBase::DRAINLIF, info.attacker->getPosition());
+		});
+	}
+
+	//return, animation playback will be handled by stacksAreAttacked
+}
+
+void BattleStacksController::executeAttackAnimations()
+{
+	owner.setAnimationCondition(EAnimationEvents::MOVEMENT, true);
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.setAnimationCondition(EAnimationEvents::MOVEMENT, false);
+
+	owner.setAnimationCondition(EAnimationEvents::BEFORE_HIT, true);
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.setAnimationCondition(EAnimationEvents::BEFORE_HIT, false);
+
+	owner.setAnimationCondition(EAnimationEvents::ATTACK, true);
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.setAnimationCondition(EAnimationEvents::ATTACK, false);
+
+	// Note that HIT event can also be emitted by attack animation
+	owner.setAnimationCondition(EAnimationEvents::HIT, true);
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.setAnimationCondition(EAnimationEvents::HIT, false);
+
+	owner.setAnimationCondition(EAnimationEvents::AFTER_HIT, true);
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.setAnimationCondition(EAnimationEvents::AFTER_HIT, false);
+
+	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
 }
 
 bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const

+ 3 - 1
client/battle/BattleStacksController.h

@@ -21,6 +21,7 @@ class SpellID;
 VCMI_LIB_NAMESPACE_END
 
 struct StackAttackedInfo;
+struct StackAttackInfo;
 
 class Canvas;
 class BattleInterface;
@@ -77,6 +78,7 @@ class BattleStacksController
 
 	std::shared_ptr<IImage> getStackAmountBox(const CStack * stack);
 
+	void executeAttackAnimations();
 public:
 	BattleStacksController(BattleInterface & owner);
 
@@ -89,7 +91,7 @@ public:
 	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 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);

+ 7 - 4
lib/NetPacks.h

@@ -1647,12 +1647,11 @@ struct BattleAttack : public CPackForClient
 	std::vector<BattleStackAttacked> bsa;
 	ui32 stackAttacking;
 	ui32 flags; //uses Eflags (below)
-	enum EFlags{SHOT = 1, COUNTER = 2, LUCKY = 4, UNLUCKY = 8, BALLISTA_DOUBLE_DMG = 16, DEATH_BLOW = 32, SPELL_LIKE = 64};
+	enum EFlags{SHOT = 1, COUNTER = 2, LUCKY = 4, UNLUCKY = 8, BALLISTA_DOUBLE_DMG = 16, DEATH_BLOW = 32, SPELL_LIKE = 64, LIFE_DRAIN = 128};
 
+	BattleHex tile;
 	SpellID spellID; //for SPELL_LIKE
 
-	std::vector<CustomEffectInfo> customEffects;
-
 	bool shot() const//distance attack - decrease number of shots
 	{
 		return flags & SHOT;
@@ -1681,13 +1680,17 @@ struct BattleAttack : public CPackForClient
 	{
 		return flags & SPELL_LIKE;
 	}
+	bool lifeDrain() const
+	{
+		return flags & LIFE_DRAIN;
+	}
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & bsa;
 		h & stackAttacking;
 		h & flags;
+		h & tile;
 		h & spellID;
-		h & customEffects;
 		h & attackerChanges;
 	}
 };

+ 2 - 2
lib/battle/CBattleInfoCallback.cpp

@@ -145,8 +145,8 @@ ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(con
 	{
 	case spells::Mode::HERO:
 	{
-		if(battleCastSpells(side.get()) > 0)
-			return ESpellCastProblem::CASTS_PER_TURN_LIMIT;
+		//if(battleCastSpells(side.get()) > 0)
+		//	return ESpellCastProblem::CASTS_PER_TURN_LIMIT;
 
 		auto hero = dynamic_cast<const CGHeroInstance *>(caster);
 

+ 5 - 2
lib/spells/BattleSpellMechanics.cpp

@@ -330,8 +330,6 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target)
 	for(auto & p : effectsToApply)
 		p.first->apply(server, this, p.second);
 
-//	afterCast();
-
 	if(sc.activeCast)
 	{
 		caster->spendMana(server, spellCost);
@@ -342,6 +340,11 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target)
 			otherHero->spendMana(server, -sc.manaGained);
 		}
 	}
+
+	// send empty event to client
+	// temporary(?) workaround to force animations to trigger
+	StacksInjured fake_event;
+	server->apply(&fake_event);
 }
 
 void BattleSpellMechanics::beforeCast(BattleSpellCast & sc, vstd::RNG & rng, const Target & target)

+ 4 - 11
server/CGameHandler.cpp

@@ -1009,6 +1009,7 @@ void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender,
 	BattleAttack bat;
 	BattleLogMessage blm;
 	bat.stackAttacking = attacker->unitId();
+	bat.tile = targetHex;
 
 	std::shared_ptr<battle::CUnitState> attackerState = attacker->acquireState();
 
@@ -1114,6 +1115,9 @@ void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender,
 		bat.attackerChanges.changedStacks.push_back(info);
 	}
 
+	if (drainedLife > 0)
+		bat.flags |= BattleAttack::LIFE_DRAIN;
+
 	sendAndApply(&bat);
 
 	{
@@ -1142,17 +1146,6 @@ void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender,
 	// drain life effect (as well as log entry) must be applied after the attack
 	if(drainedLife > 0)
 	{
-		BattleAttack bat;
-		bat.stackAttacking = attacker->unitId();
-		{
-			CustomEffectInfo customEffect;
-			customEffect.sound = soundBase::DRAINLIF;
-			customEffect.effect = 52;
-			customEffect.stack = attackerState->unitId();
-			bat.customEffects.push_back(std::move(customEffect));
-		}
-		sendAndApply(&bat);
-
 		MetaString text;
 		attackerState->addText(text, MetaString::GENERAL_TXT, 361);
 		attackerState->addNameReplacement(text, false);