Browse Source

Merge pull request #1729 from IvanSavenko/battle_animation_fixes

Fixes for battle UI regressions
Ivan Savenko 2 years ago
parent
commit
9dceed4f56

+ 2 - 0
Mods/vcmi/config/vcmi/english.json

@@ -81,6 +81,8 @@
 	"vcmi.battleOptions.animationsSpeed6.help": "Sets animation speed to instantaneous",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Skip Intro Music",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\n Skip short music that plays at beginning of each battle before action starts. Can also be skipped by pressing ESC key.",
+	
+	"vcmi.battleWindow.pressKeyToSkipIntro" : "Press any key to skip battle intro",
 
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Show Available Creatures",
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Show Available Creatures}\n\n Shows creatures available to purchase instead of their growth in town summary (bottom-left corner).",

+ 2 - 0
Mods/vcmi/config/vcmi/ukrainian.json

@@ -82,6 +82,8 @@
 	"vcmi.battleOptions.animationsSpeed6.help": "Встановити миттєву швидкість анімації",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Пропускати вступну музику",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускати вступну музику}\n\n Пропускати коротку музику, яка грає на початку кожної битви перед початком дії. Також можна пропустити, натиснувши клавішу ESC.",
+	
+	"vcmi.battleWindow.pressKeyToSkipIntro" : "Натисніть будь-яку клавішу, щоб розпочати бій",
 
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Показувати доступних істот",
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Показувати доступних істот}\n\n Показує істот, яких можна придбати, замість їхнього приросту у зведенні по місту (нижній лівий кут).",

+ 7 - 0
client/battle/BattleActionsController.cpp

@@ -677,6 +677,13 @@ PossiblePlayerBattleAction BattleActionsController::selectAction(BattleHex targe
 
 void BattleActionsController::onHexHovered(BattleHex hoveredHex)
 {
+	if (owner.openingPlaying())
+	{
+		currentConsoleMsg = VLC->generaltexth->translate("vcmi.battleWindow.pressKeyToSkipIntro");
+		GH.statusbar->write(currentConsoleMsg);
+		return;
+	}
+
 	if (owner.stacksController->getActiveStack() == nullptr)
 		return;
 

+ 17 - 18
client/battle/BattleAnimationClasses.cpp

@@ -306,10 +306,8 @@ void MeleeAttackAnimation::nextFrame()
 	size_t totalFrames = stackAnimation(attackingStack)->framesInGroup(getGroup());
 
 	if ( currentFrame * 2 >= totalFrames )
-	{
-		if(owner.getAnimationCondition(EAnimationEvents::HIT) == false)
-			owner.setAnimationCondition(EAnimationEvents::HIT, true);
-	}
+		owner.executeAnimationStage(EAnimationEvents::HIT);
+
 	AttackAnimation::nextFrame();
 }
 
@@ -356,9 +354,9 @@ bool MovementAnimation::init()
 		myAnim->setType(ECreatureAnimType::MOVING);
 	}
 
-	if (owner.moveSoundHander == -1)
+	if (moveSoundHander == -1)
 	{
-		owner.moveSoundHander = CCS->soundh->playSound(battle_sound(stack->getCreature(), move), -1);
+		moveSoundHander = CCS->soundh->playSound(battle_sound(stack->getCreature(), move), -1);
 	}
 
 	Point begPosition = owner.stacksController->getStackPositionAtHex(prevHex, stack);
@@ -418,11 +416,8 @@ MovementAnimation::~MovementAnimation()
 {
 	assert(stack);
 
-	if(owner.moveSoundHander != -1)
-	{
-		CCS->soundh->stopSound(owner.moveSoundHander);
-		owner.moveSoundHander = -1;
-	}
+	if(moveSoundHander != -1)
+		CCS->soundh->stopSound(moveSoundHander);
 }
 
 MovementAnimation::MovementAnimation(BattleInterface & owner, const CStack *stack, std::vector<BattleHex> _destTiles, int _distance)
@@ -432,6 +427,7 @@ MovementAnimation::MovementAnimation(BattleInterface & owner, const CStack *stac
 	  begX(0), begY(0),
 	  distanceX(0), distanceY(0),
 	  progressPerSecond(0.0),
+	  moveSoundHander(-1),
 	  progress(0.0)
 {
 	logAnim->debug("Created MovementAnimation for %s", stack->getName());
@@ -709,12 +705,17 @@ void RangedAttackAnimation::nextFrame()
 	if (projectileEmitted)
 	{
 		if (!owner.projectilesController->hasActiveProjectile(attackingStack, false))
-		{
-			if(owner.getAnimationCondition(EAnimationEvents::HIT) == false)
-				owner.setAnimationCondition(EAnimationEvents::HIT, true);
-		}
+			owner.executeAnimationStage(EAnimationEvents::HIT);
+
 	}
 
+	bool stackHasProjectile = owner.projectilesController->hasActiveProjectile(stack, true);
+
+	if (!projectileEmitted || stackHasProjectile)
+		stackAnimation(attackingStack)->playUntil(getAttackClimaxFrame());
+	else
+		stackAnimation(attackingStack)->playUntil(static_cast<size_t>(-1));
+
 	AttackAnimation::nextFrame();
 
 	if (!projectileEmitted)
@@ -1052,7 +1053,6 @@ bool HeroCastAnimation::init()
 	hero->setPhase(EHeroAnimType::CAST_SPELL);
 
 	hero->onPhaseFinished([&](){
-		assert(owner.getAnimationCondition(EAnimationEvents::HIT) == true);
 		delete this;
 	});
 
@@ -1093,8 +1093,7 @@ void HeroCastAnimation::emitProjectile()
 
 void HeroCastAnimation::emitAnimationEvent()
 {
-	if(owner.getAnimationCondition(EAnimationEvents::HIT) == false)
-		owner.setAnimationCondition(EAnimationEvents::HIT, true);
+	owner.executeAnimationStage(EAnimationEvents::HIT);
 }
 
 void HeroCastAnimation::nextFrame()

+ 2 - 0
client/battle/BattleAnimationClasses.h

@@ -141,6 +141,8 @@ protected:
 class MovementAnimation : public StackMoveAnimation
 {
 private:
+	int moveSoundHander; // sound handler used when moving a unit
+
 	std::vector<BattleHex> destTiles; //full path, includes already passed hexes
 	ui32 curentMoveIndex; // index of nextHex in destTiles
 

+ 16 - 8
client/battle/BattleConstants.h

@@ -30,14 +30,22 @@ enum class EBattleEffect
 	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
+enum class EAnimationEvents
+{
+	// any action
+	ROTATE,      // stacks rotate before action
+
+	// movement action
+	MOVE_START,  // stack starts movement
+	MOVEMENT,    // movement animation loop starts
+	MOVE_END,    // stack end movement
+
+	// attack/spellcast action
+	BEFORE_HIT,  // attack and defence effects play, e.g. luck/death blow
+	ATTACK,      // attack and defence animations are playing
+	HIT,         // hit & death animations are playing
+	AFTER_HIT,   // post-attack effect, e.g. phoenix rebirth
+
 	COUNT
 };
 

+ 4 - 4
client/battle/BattleEffectsController.cpp

@@ -56,7 +56,7 @@ void BattleEffectsController::displayEffect(EBattleEffect effect, std::string so
 
 void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bte)
 {
-	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+	owner.checkForAnimations();
 
 	const CStack * stack = owner.curInt->cb->battleGetStackByID(bte.stackID);
 	if(!stack)
@@ -90,12 +90,12 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt
 		default:
 			return;
 	}
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.waitForAnimations();
 }
 
 void BattleEffectsController::startAction(const BattleAction* action)
 {
-	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+	owner.checkForAnimations();
 
 	const CStack *stack = owner.curInt->cb->battleGetStackByID(action->stackNumber);
 
@@ -110,7 +110,7 @@ void BattleEffectsController::startAction(const BattleAction* action)
 		break;
 	}
 
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.waitForAnimations();
 }
 
 void BattleEffectsController::collectRenderableObjects(BattleRenderer & renderer)

+ 78 - 50
client/battle/BattleInterface.cpp

@@ -54,11 +54,7 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *
 	, attackerInt(att)
 	, defenderInt(defen)
 	, curInt(att)
-	, moveSoundHander(-1)
 {
-	for ( auto & event : animationEvents)
-		event.setn(false);
-
 	if(spectatorInt)
 	{
 		curInt = spectatorInt;
@@ -96,7 +92,7 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *
 	obstacleController.reset(new BattleObstacleController(*this));
 
 	adventureInt->onAudioPaused();
-	setAnimationCondition(EAnimationEvents::OPENING, true);
+	ongoingAnimationsState.set(true);
 
 	GH.pushInt(windowObject);
 	windowObject->blockUI(true);
@@ -107,12 +103,6 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *
 
 void BattleInterface::playIntroSoundAndUnlockInterface()
 {
-	if(settings["gameTweaks"]["skipBattleIntroMusic"].Bool())
-	{
-		onIntroSoundPlayed();
-		return;
-	}
-
 	auto onIntroPlayed = [this]()
 	{
 		if(LOCPLINT->battleInt)
@@ -124,19 +114,22 @@ void BattleInterface::playIntroSoundAndUnlockInterface()
 
 	battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds);
 	if (battleIntroSoundChannel != -1)
+	{
 		CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed);
+
+		if (settings["gameTweaks"]["skipBattleIntroMusic"].Bool())
+			openingEnd();
+	}
 	else
 		onIntroSoundPlayed();
 }
 
 void BattleInterface::onIntroSoundPlayed()
 {
-	setAnimationCondition(EAnimationEvents::OPENING, false);
+	if (openingPlaying())
+		openingEnd();
+
 	CCS->musich->playMusicFromSet("battle", true, true);
-	if(tacticsMode)
-		tacticNextStack(nullptr);
-	activateStack();
-	battleIntroSoundChannel = -1;
 }
 
 BattleInterface::~BattleInterface()
@@ -147,10 +140,7 @@ BattleInterface::~BattleInterface()
 	if (adventureInt)
 		adventureInt->onAudioResumed();
 
-	// 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);
+	onAnimationsFinished();
 }
 
 void BattleInterface::redrawBattlefield()
@@ -217,7 +207,7 @@ void BattleInterface::stackAttacking( const StackAttackInfo & attackInfo )
 
 void BattleInterface::newRoundFirst( int round )
 {
-	waitForAnimationCondition(EAnimationEvents::OPENING, false);
+	waitForAnimations();
 }
 
 void BattleInterface::newRound(int number)
@@ -299,8 +289,7 @@ void BattleInterface::gateStateChanged(const EGateState state)
 
 void BattleInterface::battleFinished(const BattleResult& br)
 {
-	assert(getAnimationCondition(EAnimationEvents::ACTION) == false);
-	waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	checkForAnimations();
 	stacksController->setActiveStack(nullptr);
 
 	CCS->curh->set(Cursor::Map::POINTER);
@@ -337,7 +326,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 					EAnimationEvents::HIT:
 					EAnimationEvents::BEFORE_HIT;//FIXME: recheck whether this should be on projectile spawning
 
-		executeOnAnimationCondition(group, true, [=]() {
+		addToAnimationStage(group, [=]() {
 			CCS->soundh->playSound(castSoundPath);
 		});
 	}
@@ -348,7 +337,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 
 		if(casterStack != nullptr )
 		{
-			executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]()
+			addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]()
 			{
 				stacksController->addNewAnim(new CastAnimation(*this, casterStack, targetedTile, curInt->cb->battleGetStackByPos(targetedTile), spell));
 				displaySpellCast(spell, casterStack->getPosition());
@@ -359,14 +348,14 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 			auto hero = sc->side ? defendingHero : attackingHero;
 			assert(hero);
 
-			executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]()
+			addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]()
 			{
 				stacksController->addNewAnim(new HeroCastAnimation(*this, hero, targetedTile, curInt->cb->battleGetStackByPos(targetedTile), spell));
 			});
 		}
 	}
 
-	executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+	addToAnimationStage(EAnimationEvents::HIT, [=](){
 		displaySpellHit(spell, targetedTile);
 	});
 
@@ -377,7 +366,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 		assert(stack);
 		if(stack)
 		{
-			executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+			addToAnimationStage(EAnimationEvents::HIT, [=](){
 				displaySpellEffect(spell, stack->getPosition());
 			});
 		}
@@ -387,14 +376,14 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 	{
 		auto stack = curInt->cb->battleGetStackByID(elem, false);
 		assert(stack);
-		executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+		addToAnimationStage(EAnimationEvents::HIT, [=](){
 			effectsController->displayEffect(EBattleEffect::MAGIC_MIRROR, stack->getPosition());
 		});
 	}
 
 	if (!sc->resistedCres.empty())
 	{
-		executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+		addToAnimationStage(EAnimationEvents::HIT, [=](){
 			CCS->soundh->playSound("MAGICRES");
 		});
 	}
@@ -403,7 +392,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 	{
 		auto stack = curInt->cb->battleGetStackByID(elem, false);
 		assert(stack);
-		executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+		addToAnimationStage(EAnimationEvents::HIT, [=](){
 			effectsController->displayEffect(EBattleEffect::RESISTANCE, stack->getPosition());
 		});
 	}
@@ -415,7 +404,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
 		Point rightHero = Point(755, 30);
 		bool side = sc->side;
 
-		executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=](){
+		addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){
 			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));
 		});
@@ -535,6 +524,24 @@ void BattleInterface::activateStack()
 	GH.fakeMouseMove();
 }
 
+bool BattleInterface::openingPlaying()
+{
+	return battleIntroSoundChannel != -1;
+}
+
+void BattleInterface::openingEnd()
+{
+	assert(openingPlaying());
+	if (!openingPlaying())
+		return;
+
+	onAnimationsFinished();
+	if(tacticsMode)
+		tacticNextStack(nullptr);
+	activateStack();
+	battleIntroSoundChannel = -1;
+}
+
 bool BattleInterface::makingTurn() const
 {
 	return stacksController->getActiveStack() != nullptr;
@@ -611,8 +618,7 @@ void BattleInterface::tacticNextStack(const CStack * current)
 		current = stacksController->getActiveStack();
 
 	//no switching stacks when the current one is moving
-	assert(getAnimationCondition(EAnimationEvents::ACTION) == false);
-	waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	checkForAnimations();
 
 	TStacks stacksOfMine = tacticianInterface->cb->battleGetStacks(CBattleCallback::ONLY_MINE);
 	vstd::erase_if (stacksOfMine, &immobile);
@@ -698,18 +704,24 @@ void BattleInterface::castThisSpell(SpellID spellID)
 	actionsController->castThisSpell(spellID);
 }
 
-void BattleInterface::setAnimationCondition( EAnimationEvents event, bool state)
+void BattleInterface::executeStagedAnimations()
 {
-	logAnim->debug("setAnimationCondition: %d -> %s", static_cast<int>(event), state ? "ON" : "OFF");
+	EAnimationEvents earliestStage = EAnimationEvents::COUNT;
+
+	for(const auto & event : awaitingEvents)
+		earliestStage = std::min(earliestStage, event.event);
 
-	size_t index = static_cast<size_t>(event);
-	animationEvents[index].setn(state);
+	if(earliestStage != EAnimationEvents::COUNT)
+		executeAnimationStage(earliestStage);
+}
 
+void BattleInterface::executeAnimationStage(EAnimationEvents event)
+{
 	decltype(awaitingEvents) executingEvents;
 
-	for (auto it = awaitingEvents.begin(); it != awaitingEvents.end();)
+	for(auto it = awaitingEvents.begin(); it != awaitingEvents.end();)
 	{
-		if (it->event == event && it->eventState == state)
+		if(it->event == event)
 		{
 			executingEvents.push_back(*it);
 			it = awaitingEvents.erase(it);
@@ -717,27 +729,43 @@ void BattleInterface::setAnimationCondition( EAnimationEvents event, bool state)
 		else
 			++it;
 	}
-
-	for (auto const & event : executingEvents)
+	for(const auto & event : executingEvents)
 		event.action();
 }
 
-bool BattleInterface::getAnimationCondition( EAnimationEvents event)
+void BattleInterface::onAnimationsStarted()
 {
-	size_t index = static_cast<size_t>(event);
-	return animationEvents[index].get();
+	ongoingAnimationsState.setn(true);
 }
 
-void BattleInterface::waitForAnimationCondition( EAnimationEvents event, bool state)
+void BattleInterface::onAnimationsFinished()
+{
+	ongoingAnimationsState.setn(false);
+}
+
+void BattleInterface::waitForAnimations()
 {
 	auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim);
-	size_t index = static_cast<size_t>(event);
-	animationEvents[index].waitUntil(state);
+	ongoingAnimationsState.waitUntil(false);
+}
+
+bool BattleInterface::hasAnimations()
+{
+	return ongoingAnimationsState.get();
+}
+
+void BattleInterface::checkForAnimations()
+{
+	assert(!hasAnimations());
+	if(hasAnimations())
+		logGlobal->error("Unexpected animations state: expected all animations to be over, but some are still ongoing!");
+
+	waitForAnimations();
 }
 
-void BattleInterface::executeOnAnimationCondition( EAnimationEvents event, bool state, const AwaitingAnimationAction & action)
+void BattleInterface::addToAnimationStage(EAnimationEvents event, const AwaitingAnimationAction & action)
 {
-	awaitingEvents.push_back({action, event, state});
+	awaitingEvents.push_back({action, event});
 }
 
 void BattleInterface::setBattleQueueVisibility(bool visible)

+ 15 - 18
client/battle/BattleInterface.h

@@ -94,11 +94,10 @@ class BattleInterface
 	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;
+	CondSh<bool> ongoingAnimationsState;
 
 	/// List of events that are waiting to be triggered
 	std::vector<AwaitingAnimationEvents> awaitingEvents;
@@ -112,6 +111,9 @@ class BattleInterface
 	/// defender interface, not null if attacker is human in our vcmiclient
 	std::shared_ptr<CPlayerInterface> defenderInt;
 
+	/// ID of channel on which battle opening sound is playing, or -1 if none
+	int battleIntroSoundChannel;
+
 	void playIntroSoundAndUnlockInterface();
 	void onIntroSoundPlayed();
 public:
@@ -119,9 +121,6 @@ public:
 	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;
 
@@ -146,9 +145,10 @@ public:
 
 	static CondSh<BattleAction *> givenCommand; //data != nullptr if we have i.e. moved current unit
 
-	bool makingTurn() const;
+	bool openingPlaying();
+	void openingEnd();
 
-	int moveSoundHander; // sound handler used when moving a unit
+	bool makingTurn() const;
 
 	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();
@@ -178,17 +178,14 @@ public:
 
 	void setBattleQueueVisibility(bool visible);
 
-	/// 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);
+	void executeStagedAnimations();
+	void executeAnimationStage( EAnimationEvents event);
+	void onAnimationsStarted();
+	void onAnimationsFinished();
+	void waitForAnimations();
+	bool hasAnimations();
+	void checkForAnimations();
+	void addToAnimationStage( EAnimationEvents event, const AwaitingAnimationAction & action);
 
 	//call-ins
 	void startAction(const BattleAction* action);

+ 1 - 1
client/battle/BattleObstacleController.cpp

@@ -100,7 +100,7 @@ void BattleObstacleController::obstaclePlaced(const std::vector<std::shared_ptr<
 		owner.stacksController->addNewAnim(new EffectAnimation(owner, spellObstacle->appearAnimation, whereTo, oi->pos));
 
 		//so when multiple obstacles are added, they show up one after another
-		owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+		owner.waitForAnimations();
 
 		loadObstacleImage(*spellObstacle);
 	}

+ 2 - 3
client/battle/BattleSiegeController.cpp

@@ -330,7 +330,7 @@ bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const
 
 void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca)
 {
-	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+	owner.checkForAnimations();
 
 	if (ca.attacker != -1)
 	{
@@ -352,8 +352,7 @@ void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca)
 		owner.stacksController->addNewAnim(new EffectAnimation(owner, "SGEXPL.DEF", positions));
 	}
 
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
-	owner.setAnimationCondition(EAnimationEvents::HIT, false);
+	owner.waitForAnimations();
 
 	for (auto attackInfo : ca.attackedParts)
 	{

+ 56 - 96
client/battle/BattleStacksController.cpp

@@ -160,7 +160,7 @@ void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer)
 
 void BattleStacksController::stackReset(const CStack * stack)
 {
-	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+	owner.checkForAnimations();
 
 	//reset orientation?
 	//stackFacingRight[stack->ID] = stack->side == BattleSide::ATTACKER;
@@ -177,7 +177,7 @@ void BattleStacksController::stackReset(const CStack * stack)
 
 	if(stack->alive() && animation->isDeadOrDying())
 	{
-		owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=]()
+		owner.addToAnimationStage(EAnimationEvents::HIT, [=]()
 		{
 			addNewAnim(new ResurrectionAnimation(owner, stack));
 		});
@@ -221,7 +221,7 @@ void BattleStacksController::stackAdded(const CStack * stack, bool instant)
 		auto shifterFade = ColorFilter::genAlphaShifter(0);
 		setStackColorFilter(shifterFade, stack, nullptr, true);
 
-		owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=]()
+		owner.addToAnimationStage(EAnimationEvents::HIT, [=]()
 		{
 			addNewAnim(new ColorTransformAnimation(owner, stack, "summonFadeIn", nullptr));
 			if (stack->isClone())
@@ -329,13 +329,6 @@ void BattleStacksController::showStack(Canvas & canvas, const CStack * 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);
 }
@@ -372,15 +365,19 @@ void BattleStacksController::updateBattleAnimations()
 	vstd::erase(currentAnimations, nullptr);
 
 	if (hadAnimations && currentAnimations.empty())
-		owner.setAnimationCondition(EAnimationEvents::ACTION, false);
+	{
+		owner.executeStagedAnimations();
+		if (currentAnimations.empty())
+			owner.onAnimationsFinished();
+	}
 
 	initializeBattleAnimations();
 }
 
 void BattleStacksController::addNewAnim(BattleAnimation *anim)
 {
+	owner.onAnimationsStarted();
 	currentAnimations.push_back(anim);
-	owner.setAnimationCondition(EAnimationEvents::ACTION, true);
 }
 
 void BattleStacksController::stackRemoved(uint32_t stackID)
@@ -398,7 +395,7 @@ void BattleStacksController::stackRemoved(uint32_t stackID)
 
 void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> attackedInfos)
 {
-	owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+	owner.addToAnimationStage(EAnimationEvents::HIT, [=](){
 		// remove any potentially erased petrification effect
 		removeExpiredColorFilters();
 	});
@@ -423,7 +420,7 @@ void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> at
 		// if (needsReverse && !attackedInfo.defender->isFrozen())
 		if (needsReverse && stackAnimation[attackedInfo.defender->ID]->getType() != ECreatureAnimType::FROZEN)
 		{
-			owner.executeOnAnimationCondition(EAnimationEvents::MOVEMENT, true, [=]()
+			owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [=]()
 			{
 				addNewAnim(new ReverseAnimation(owner, attackedInfo.defender, attackedInfo.defender->getPosition()));
 			});
@@ -437,7 +434,7 @@ void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> at
 
 		EAnimationEvents usedEvent = useDefenceAnim ? EAnimationEvents::ATTACK : EAnimationEvents::HIT;
 
-		owner.executeOnAnimationCondition(usedEvent, true, [=]()
+		owner.addToAnimationStage(usedEvent, [=]()
 		{
 			if (useDeathAnim)
 				addNewAnim(new DeathAnimation(owner, attackedInfo.defender, attackedInfo.indirectAttack));
@@ -465,7 +462,7 @@ void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> at
 	{
 		if (attackedInfo.rebirth)
 		{
-			owner.executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=](){
+			owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){
 				owner.effectsController->displayEffect(EBattleEffect::RESURRECT, "RESURECT", attackedInfo.defender->getPosition());
 				addNewAnim(new ResurrectionAnimation(owner, attackedInfo.defender));
 			});
@@ -473,25 +470,26 @@ void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> at
 
 		if (attackedInfo.killed && attackedInfo.defender->summoned)
 		{
-			owner.executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=](){
+			owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){
 				addNewAnim(new ColorTransformAnimation(owner, attackedInfo.defender, "summonFadeOut", nullptr));
 				stackRemoved(attackedInfo.defender->ID);
 			});
 		}
 	}
-	executeAttackAnimations();
+	owner.executeStagedAnimations();
+	owner.waitForAnimations();
 }
 
 void BattleStacksController::stackTeleported(const CStack *stack, std::vector<BattleHex> destHex, int distance)
 {
 	assert(destHex.size() > 0);
-	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+	owner.checkForAnimations();
 
-	owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=](){
+	owner.addToAnimationStage(EAnimationEvents::HIT, [=](){
 		addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeOut", nullptr) );
 	});
 
-	owner.executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=](){
+	owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){
 		stackAnimation[stack->ID]->pos.moveTo(getStackPositionAtHex(destHex.back(), stack));
 		addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeIn", nullptr) );
 	});
@@ -502,42 +500,36 @@ void BattleStacksController::stackTeleported(const CStack *stack, std::vector<Ba
 void BattleStacksController::stackMoved(const CStack *stack, std::vector<BattleHex> destHex, int distance)
 {
 	assert(destHex.size() > 0);
-	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+	owner.checkForAnimations();
 
-	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)
+	if(shouldRotate(stack, stack->getPosition(), destHex[0]))
+	{
+		owner.addToAnimationStage(EAnimationEvents::ROTATE, [&]()
 		{
-			addNewAnim(new MovementAnimation(owner, stack, destHex, distance));
-			owner.executeOnAnimationCondition(EAnimationEvents::ACTION, false, enqueMoveEnd);
-		}
-		else
-			enqueMoveEnd();
-	};
+			addNewAnim(new ReverseAnimation(owner, stack, stack->getPosition()));
+		});
+	}
 
-	auto enqueMoveStart = [&](){
+	owner.addToAnimationStage(EAnimationEvents::MOVE_START, [&]()
+	{
 		addNewAnim(new MovementStartAnimation(owner, stack));
-		owner.executeOnAnimationCondition(EAnimationEvents::ACTION, false, enqueMove);
-	};
+	});
 
-	if(shouldRotate(stack, stack->getPosition(), destHex[0]))
+	if (!stack->hasBonus(Selector::typeSubtype(Bonus::FLYING, 1)))
 	{
-		addNewAnim(new ReverseAnimation(owner, stack, stack->getPosition()));
-		owner.executeOnAnimationCondition(EAnimationEvents::ACTION, false, enqueMoveStart);
+		owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [&]()
+		{
+			addNewAnim(new MovementAnimation(owner, stack, destHex, distance));
+		});
 	}
-	else
-		enqueMoveStart();
 
-	owner.waitForAnimationCondition(EAnimationEvents::MOVEMENT, false);
+	owner.addToAnimationStage(EAnimationEvents::MOVE_END, [&]()
+	{
+		addNewAnim(new MovementEndAnimation(owner, stack, destHex.back()));
+	});
+
+	owner.executeStagedAnimations();
+	owner.waitForAnimations();
 }
 
 bool BattleStacksController::shouldAttackFacingRight(const CStack * attacker, const CStack * defender)
@@ -554,7 +546,7 @@ bool BattleStacksController::shouldAttackFacingRight(const CStack * attacker, co
 
 void BattleStacksController::stackAttacking( const StackAttackInfo & info )
 {
-	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+	owner.checkForAnimations();
 
 	auto attacker    = info.attacker;
 	auto defender    = info.defender;
@@ -574,7 +566,7 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info )
 
 	if (needsReverse)
 	{
-		owner.executeOnAnimationCondition(EAnimationEvents::MOVEMENT, true, [=]()
+		owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [=]()
 		{
 			addNewAnim(new ReverseAnimation(owner, attacker, attacker->getPosition()));
 		});
@@ -582,7 +574,7 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info )
 
 	if(info.lucky)
 	{
-		owner.executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() {
+		owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() {
 			owner.appendBattleLog(info.attacker->formatGeneralMessage(-45));
 			owner.effectsController->displayEffect(EBattleEffect::GOOD_LUCK, "GOODLUCK", attacker->getPosition());
 		});
@@ -590,7 +582,7 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info )
 
 	if(info.unlucky)
 	{
-		owner.executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() {
+		owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() {
 			owner.appendBattleLog(info.attacker->formatGeneralMessage(-44));
 			owner.effectsController->displayEffect(EBattleEffect::BAD_LUCK, "BADLUCK", attacker->getPosition());
 		});
@@ -598,20 +590,20 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info )
 
 	if(info.deathBlow)
 	{
-		owner.executeOnAnimationCondition(EAnimationEvents::BEFORE_HIT, true, [=]() {
+		owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() {
 			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.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() {
 				owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, elem->getPosition());
 			});
 		}
 	}
 
-	owner.executeOnAnimationCondition(EAnimationEvents::ATTACK, true, [=]()
+	owner.addToAnimationStage(EAnimationEvents::ATTACK, [=]()
 	{
 		if (info.indirectAttack)
 		{
@@ -625,7 +617,7 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info )
 
 	if (info.spellEffect != SpellID::NONE)
 	{
-		owner.executeOnAnimationCondition(EAnimationEvents::HIT, true, [=]()
+		owner.addToAnimationStage(EAnimationEvents::HIT, [=]()
 		{
 			owner.displaySpellHit(spellEffect.toSpell(), tile);
 		});
@@ -633,7 +625,7 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info )
 
 	if (info.lifeDrain)
 	{
-		owner.executeOnAnimationCondition(EAnimationEvents::AFTER_HIT, true, [=]()
+		owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=]()
 		{
 			owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, "DRAINLIF", attacker->getPosition());
 		});
@@ -642,32 +634,6 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info )
 	//return, animation playback will be handled by stacksAreAttacked
 }
 
-void BattleStacksController::executeAttackAnimations()
-{
-	owner.setAnimationCondition(EAnimationEvents::MOVEMENT, true);
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
-	owner.setAnimationCondition(EAnimationEvents::MOVEMENT, false);
-
-	owner.setAnimationCondition(EAnimationEvents::BEFORE_HIT, true);
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
-	owner.setAnimationCondition(EAnimationEvents::BEFORE_HIT, false);
-
-	owner.setAnimationCondition(EAnimationEvents::ATTACK, true);
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
-	owner.setAnimationCondition(EAnimationEvents::ATTACK, false);
-
-	// Note that HIT event can also be emitted by attack animation
-	owner.setAnimationCondition(EAnimationEvents::HIT, true);
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
-	owner.setAnimationCondition(EAnimationEvents::HIT, false);
-
-	owner.setAnimationCondition(EAnimationEvents::AFTER_HIT, true);
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
-	owner.setAnimationCondition(EAnimationEvents::AFTER_HIT, false);
-
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
-}
-
 bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const
 {
 	Point begPosition = getStackPositionAtHex(oldPos,stack);
@@ -683,7 +649,7 @@ bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex
 
 void BattleStacksController::endAction(const BattleAction* action)
 {
-	assert(owner.getAnimationCondition(EAnimationEvents::ACTION) == false);
+	owner.checkForAnimations();
 
 	//check if we should reverse stacks
 	TStacks stacks = owner.curInt->cb->battleGetStacks(CBattleCallback::MINE_AND_ENEMY);
@@ -697,14 +663,8 @@ void BattleStacksController::endAction(const BattleAction* action)
 			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.executeStagedAnimations();
+	owner.waitForAnimations();
 
 	owner.windowObject->blockUI(activeStack == nullptr);
 	removeExpiredColorFilters();
@@ -718,7 +678,8 @@ void BattleStacksController::startAction(const BattleAction* action)
 void BattleStacksController::stackActivated(const CStack *stack)
 {
 	stackToActivate = stack;
-	owner.waitForAnimationCondition(EAnimationEvents::ACTION, false);
+	owner.waitForAnimations();
+	logAnim->debug("Activating next stack");
 	owner.activateStack();
 }
 
@@ -850,7 +811,6 @@ void BattleStacksController::updateHoveredStacks()
 		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;
@@ -862,7 +822,7 @@ std::vector<const CStack *> BattleStacksController::selectHoveredStacks()
 	if (!activeStack)
 		return {};
 
-	if(owner.getAnimationCondition(EAnimationEvents::ACTION) == true)
+	if(owner.hasAnimations())
 		return {};
 
 	auto hoveredQueueUnitId = owner.windowObject->getQueueHoveredUnitId();

+ 0 - 1
client/battle/BattleStacksController.h

@@ -88,7 +88,6 @@ class BattleStacksController
 
 	std::shared_ptr<IImage> getStackAmountBox(const CStack * stack);
 
-	void executeAttackAnimations();
 	void removeExpiredColorFilters();
 
 	void initializeBattleAnimations();

+ 7 - 4
client/battle/BattleWindow.cpp

@@ -179,6 +179,12 @@ void BattleWindow::deactivate()
 
 void BattleWindow::keyPressed(const SDL_Keycode & key)
 {
+	if (owner.openingPlaying())
+	{
+		owner.openingEnd();
+		return;
+	}
+
 	if(key == SDLK_q)
 	{
 		toggleQueueVisibility();
@@ -189,10 +195,7 @@ void BattleWindow::keyPressed(const SDL_Keycode & key)
 	}
 	else if(key == SDLK_ESCAPE)
 	{
-		if(owner.getAnimationCondition(EAnimationEvents::OPENING) == true)
-			CCS->soundh->stopSound(owner.battleIntroSoundChannel);
-		else
-			owner.actionsController->endCastingSpell();
+		owner.actionsController->endCastingSpell();
 	}
 }
 

+ 11 - 13
client/battle/CreatureAnimation.cpp

@@ -183,7 +183,7 @@ void CreatureAnimation::setType(ECreatureAnimType type)
 	currentFrame = 0;
 	once = false;
 
-	play();
+	speed = speedController(this, type);
 }
 
 CreatureAnimation::CreatureAnimation(const std::string & name_, TSpeedController controller)
@@ -191,6 +191,7 @@ CreatureAnimation::CreatureAnimation(const std::string & name_, TSpeedController
 	  speed(0.1f),
 	  shadowAlpha(128),
 	  currentFrame(0),
+	  animationEnd(-1),
 	  elapsedTime(0),
 	  type(ECreatureAnimType::HOLDING),
 	  border(CSDL_Ext::makeColor(0, 0, 0, 0)),
@@ -248,7 +249,7 @@ CreatureAnimation::CreatureAnimation(const std::string & name_, TSpeedController
 
 	reverse->verticalFlip();
 
-	play();
+	speed = speedController(this, type);
 }
 
 void CreatureAnimation::endAnimation()
@@ -263,6 +264,9 @@ bool CreatureAnimation::incrementFrame(float timePassed)
 {
 	elapsedTime += timePassed;
 	currentFrame += timePassed * speed;
+	if (animationEnd >= 0)
+		currentFrame = std::min(currentFrame, animationEnd);
+
 	const auto framesNumber = framesInGroup(type);
 
 	if(framesNumber <= 0)
@@ -375,6 +379,11 @@ void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter,
 	}
 }
 
+void CreatureAnimation::playUntil(size_t frameIndex)
+{
+	animationEnd = frameIndex;
+}
+
 int CreatureAnimation::framesInGroup(ECreatureAnimType group) const
 {
 	return static_cast<int>(forward->size(size_t(group)));
@@ -421,14 +430,3 @@ bool CreatureAnimation::isShooting() const
 		|| getType() == ECreatureAnimType::SHOOT_FRONT
 		|| getType() == ECreatureAnimType::SHOOT_DOWN;
 }
-
-void CreatureAnimation::pause()
-{
-	speed = 0;
-}
-
-void CreatureAnimation::play()
-{
-	//logAnim->trace("Play %s group %d at %d:%d", name, static_cast<int>(getType()), pos.x, pos.y);
-	speed = speedController(this, type);
-}

+ 2 - 2
client/battle/CreatureAnimation.h

@@ -88,6 +88,7 @@ private:
 	/// currently displayed frame. Float to allow H3-style animations where frames
 	/// don't display for integer number of frames
 	float currentFrame;
+	float animationEnd;
 
 	/// cumulative, real-time duration of animation. Used for effects like selection border
 	float elapsedTime;
@@ -146,8 +147,7 @@ public:
 	/// returns number of frames in selected animation type
 	int framesInGroup(ECreatureAnimType group) const;
 
-	void pause();
-	void play();
+	void playUntil(size_t frameIndex);
 
 	/// helpers to classify current type of animation
 	bool isDead() const;