Prechádzať zdrojové kódy

Refactoring of spell animations, multiple fixes for spell visuals

Ivan Savenko 2 rokov pred
rodič
commit
5094fab4d9

+ 8 - 5
client/CPlayerInterface.cpp

@@ -756,21 +756,24 @@ void CPlayerInterface::battleObstaclesChanged(const std::vector<ObstacleChanges>
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	BATTLE_EVENT_POSSIBLE_RETURN;
 
+	std::vector<std::shared_ptr<const CObstacleInstance>> newObstacles;
+
 	for(auto & change : obstacles)
 	{
 		if(change.operation == BattleChanges::EOperation::ADD)
 		{
 			auto instance = cb->battleGetObstacleByID(change.id);
 			if(instance)
-				battleInt->obstaclePlaced(*instance);
+				newObstacles.push_back(instance);
 			else
 				logNetwork->error("Invalid obstacle instance %d", change.id);
 		}
-		else
-		{
-			battleInt->fieldController->redrawBackgroundWithHexes();
-		}
 	}
+
+	if (!newObstacles.empty())
+		battleInt->obstaclePlaced(newObstacles);
+
+	battleInt->fieldController->redrawBackgroundWithHexes();
 }
 
 void CPlayerInterface::battleCatapultAttacked(const CatapultAttack & ca)

+ 272 - 243
client/battle/CBattleAnimations.cpp

@@ -93,18 +93,18 @@ void CBattleAnimation::setStackFacingRight(const CStack * stack, bool facingRigh
 bool CBattleAnimation::checkInitialConditions()
 {
 	int lowestMoveID = ID;
-	CBattleStackAnimation * thAnim = dynamic_cast<CBattleStackAnimation *>(this);
-	CEffectAnimation * thSen = dynamic_cast<CEffectAnimation *>(this);
+	auto * thAnim = dynamic_cast<CBattleStackAnimation *>(this);
+	auto * thSen = dynamic_cast<CPointEffectAnimation *>(this);
 
 	for(auto & elem : pendingAnimations())
 	{
-		CEffectAnimation * sen = dynamic_cast<CEffectAnimation *>(elem);
+		auto * sen = dynamic_cast<CPointEffectAnimation *>(elem);
 
 		// all effect animations can play concurrently with each other
 		if(sen && thSen && sen != thSen)
 			continue;
 
-		CReverseAnimation * revAnim = dynamic_cast<CReverseAnimation *>(elem);
+		auto * revAnim = dynamic_cast<CReverseAnimation *>(elem);
 
 		// if there is high-priority reverse animation affecting our stack then this animation will wait
 		if(revAnim && thAnim && revAnim && revAnim->stack->ID == thAnim->stack->ID && revAnim->priority)
@@ -206,15 +206,15 @@ bool CDefenceAnimation::init()
 	for(auto & elem : pendingAnimations())
 	{
 
-		CDefenceAnimation * defAnim = dynamic_cast<CDefenceAnimation *>(elem);
+		auto * defAnim = dynamic_cast<CDefenceAnimation *>(elem);
 		if(defAnim && defAnim->stack->ID != stack->ID)
 			continue;
 
-		CAttackAnimation * attAnim = dynamic_cast<CAttackAnimation *>(elem);
+		auto * attAnim = dynamic_cast<CAttackAnimation *>(elem);
 		if(attAnim && attAnim->stack->ID != stack->ID)
 			continue;
 
-		CEffectAnimation * sen = dynamic_cast<CEffectAnimation *>(elem);
+		auto * sen = dynamic_cast<CPointEffectAnimation *>(elem);
 		if (sen && attacker == nullptr)
 			return false;
 
@@ -699,22 +699,13 @@ void CReverseAnimation::setupSecondPart()
 }
 
 CRangedAttackAnimation::CRangedAttackAnimation(CBattleInterface * owner_, const CStack * attacker, BattleHex dest_, const CStack * defender)
-	: CAttackAnimation(owner_, attacker, dest_, defender)
+	: CAttackAnimation(owner_, attacker, dest_, defender),
+	  projectileEmitted(false)
 {
-
-}
-
-
-CShootingAnimation::CShootingAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool _catapult, int _catapultDmg)
-	: CRangedAttackAnimation(_owner, attacker, _dest, _attacked),
-	catapultDamage(_catapultDmg),
-	projectileEmitted(false),
-	explosionEmitted(false)
-{
-	logAnim->debug("Created shooting anim for %s", stack->getName());
+	logAnim->info("Ranged attack animation created");
 }
 
-bool CShootingAnimation::init()
+bool CRangedAttackAnimation::init()
 {
 	if( !CAttackAnimation::checkInitialConditions() )
 		return false;
@@ -737,13 +728,14 @@ bool CShootingAnimation::init()
 		return false;
 	}
 
+	logAnim->info("Ranged attack animation initialized");
 	setAnimationGroup();
 	initializeProjectile();
 	shooting = true;
 	return true;
 }
 
-void CShootingAnimation::setAnimationGroup()
+void CRangedAttackAnimation::setAnimationGroup()
 {
 	Point shooterPos = stackAnimation(attackingStack)->pos.topLeft();
 	Point shotTarget = owner->stacksController->getStackPositionAtHex(dest, attackedStack) + Point(225, 225);
@@ -755,14 +747,14 @@ void CShootingAnimation::setAnimationGroup()
 
 	// Calculate projectile start position. Offsets are read out of the CRANIM.TXT.
 	if (projectileAngle > straightAngle)
-		group = CCreatureAnim::SHOOT_UP;
+		group = getUpwardsGroup();
 	else if (projectileAngle < -straightAngle)
-		group = CCreatureAnim::SHOOT_DOWN;
+		group = getDownwardsGroup();
 	else
-		group = CCreatureAnim::SHOOT_FRONT;
+		group = getForwardGroup();
 }
 
-void CShootingAnimation::initializeProjectile()
+void CRangedAttackAnimation::initializeProjectile()
 {
 	const CCreature *shooterInfo = getCreature();
 	Point shotTarget = owner->stacksController->getStackPositionAtHex(dest, attackedStack) + Point(225, 225);
@@ -789,38 +781,36 @@ void CShootingAnimation::initializeProjectile()
 		assert(0);
 	}
 
-	owner->projectilesController->createProjectile(attackingStack, attackedStack, shotOrigin, shotTarget);
+	createProjectile(shotOrigin, shotTarget);
 }
 
-void CShootingAnimation::emitProjectile()
+void CRangedAttackAnimation::emitProjectile()
 {
+	logAnim->info("Ranged attack projectile emitted");
 	owner->projectilesController->emitStackProjectile(attackingStack);
 	projectileEmitted = true;
 }
 
-void CShootingAnimation::nextFrame()
+void CRangedAttackAnimation::nextFrame()
 {
 	for(auto & it : pendingAnimations())
 	{
 		CMovementStartAnimation * anim = dynamic_cast<CMovementStartAnimation *>(it);
 		CReverseAnimation * anim2 = dynamic_cast<CReverseAnimation *>(it);
 		if( (anim && anim->stack->ID == stack->ID) || (anim2 && anim2->stack->ID == stack->ID && anim2->priority ) )
+		{
+			assert(0); // FIXME: our stack started to move even though we are playing shooting animation? How?
 			return;
+		}
 	}
 
 	// animation should be paused if there is an active projectile
 	if (projectileEmitted)
 	{
 		if (owner->projectilesController->hasActiveProjectile(attackingStack))
-		{
 			stackAnimation(attackingStack)->pause();
-			return;
-		}
 		else
-		{
 			stackAnimation(attackingStack)->play();
-			emitExplosion();
-		}
 	}
 
 	CAttackAnimation::nextFrame();
@@ -831,41 +821,25 @@ void CShootingAnimation::nextFrame()
 
 		assert(stackAnimation(attackingStack)->isShooting());
 
+		logAnim->info("Ranged attack executing, %d / %d / %d",
+					  stackAnimation(attackingStack)->getCurrentFrame(),
+					  shooterInfo->animation.attackClimaxFrame,
+					  stackAnimation(attackingStack)->framesInGroup(group));
+
 		// emit projectile once animation playback reached "climax" frame
 		if ( stackAnimation(attackingStack)->getCurrentFrame() >= shooterInfo->animation.attackClimaxFrame )
 		{
 			emitProjectile();
+			stackAnimation(attackingStack)->pause();
 			return;
 		}
 	}
 }
 
-void CShootingAnimation::emitExplosion()
-{
-	if (attackedStack)
-		return;
-
-	if (explosionEmitted)
-		return;
-
-	explosionEmitted = true;
-
-	Point shotTarget = owner->stacksController->getStackPositionAtHex(dest, attackedStack) + Point(225, 225) - Point(126, 105);
-
-	owner->stacksController->addNewAnim( new CEffectAnimation(owner, catapultDamage ? "SGEXPL.DEF" : "CSGRCK.DEF", shotTarget.x, shotTarget.y));
-
-	if(catapultDamage > 0)
-	{
-		CCS->soundh->playSound("WALLHIT");
-	}
-	else
-	{
-		CCS->soundh->playSound("WALLMISS");
-	}
-}
-
-CShootingAnimation::~CShootingAnimation()
+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);
 
@@ -877,180 +851,167 @@ CShootingAnimation::~CShootingAnimation()
 	}
 }
 
-CCastAnimation::CCastAnimation(CBattleInterface * owner_, const CStack * attacker, BattleHex dest_, const CStack * defender)
-	: CRangedAttackAnimation(owner_, attacker, dest_, defender)
+CShootingAnimation::CShootingAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked)
+	: CRangedAttackAnimation(_owner, attacker, _dest, _attacked)
 {
-	if(!dest_.isValid() && defender)
-		dest = defender->getPosition();
+	logAnim->debug("Created shooting anim for %s", stack->getName());
 }
 
-bool CCastAnimation::init()
+void CShootingAnimation::createProjectile(const Point & from, const Point & dest) const
 {
-	if(!CAttackAnimation::checkInitialConditions())
-		return false;
+	owner->projectilesController->createProjectile(attackingStack, attackedStack, from, dest);
+}
 
-	if(!attackingStack || myAnim->isDeadOrDying())
-	{
-		delete this;
-		return false;
-	}
+CCreatureAnim::EAnimType CShootingAnimation::getUpwardsGroup() const
+{
+	return CCreatureAnim::SHOOT_UP;
+}
 
-	//reverse unit if necessary
-	if(attackedStack)
-	{
-		if(owner->getCurrentPlayerInterface()->cb->isToReverse(attackingStack->getPosition(), attackedStack->getPosition(), stackFacingRight(attackingStack), attackingStack->doubleWide(), stackFacingRight(attackedStack)))
-		{
-			owner->stacksController->addNewAnim(new CReverseAnimation(owner, attackingStack, attackingStack->getPosition(), true));
-			return false;
-		}
-	}
-	else
-	{
-		if(dest.isValid() && owner->getCurrentPlayerInterface()->cb->isToReverse(attackingStack->getPosition(), dest, stackFacingRight(attackingStack), false, false))
-		{
-			owner->stacksController->addNewAnim(new CReverseAnimation(owner, attackingStack, attackingStack->getPosition(), true));
-			return false;
-		}
-	}
+CCreatureAnim::EAnimType CShootingAnimation::getForwardGroup() const
+{
+	return CCreatureAnim::SHOOT_FRONT;
+}
 
-	//TODO: display spell projectile here
+CCreatureAnim::EAnimType CShootingAnimation::getDownwardsGroup() const
+{
+	return CCreatureAnim::SHOOT_DOWN;
+}
 
-	static const double straightAngle = 0.2;
+CCatapultAnimation::CCatapultAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, int _catapultDmg)
+	: CShootingAnimation(_owner, attacker, _dest, _attacked),
+	catapultDamage(_catapultDmg),
+	explosionEmitted(false)
+{
+	logAnim->debug("Created shooting anim for %s", stack->getName());
+}
 
+void CCatapultAnimation::nextFrame()
+{
+	CShootingAnimation::nextFrame();
 
-	Point fromPos;
-	Point destPos;
+	if ( explosionEmitted)
+		return;
 
-	// NOTE: two lines below return different positions (very notable with 2-hex creatures). Obtaining via creanims seems to be more precise
-	fromPos = stackAnimation(attackingStack)->pos.topLeft();
-	//xycoord = owner->stacksController->getStackPositionAtHex(shooter->getPosition(), shooter);
+	if ( !projectileEmitted)
+		return;
 
-	destPos = owner->stacksController->getStackPositionAtHex(dest, attackedStack);
+	if (owner->projectilesController->hasActiveProjectile(attackingStack))
+		return;
 
+	explosionEmitted = true;
+	Point shotTarget = owner->stacksController->getStackPositionAtHex(dest, attackedStack) + Point(225, 225) - Point(126, 105);
 
-	double projectileAngle = atan2(fabs((double)destPos.y - fromPos.y), fabs((double)destPos.x - fromPos.x));
-	if(attackingStack->getPosition() < dest)
-		projectileAngle = -projectileAngle;
+	if(catapultDamage > 0)
+		owner->stacksController->addNewAnim( new CPointEffectAnimation(owner, soundBase::WALLHIT, "SGEXPL.DEF", shotTarget));
+	else
+		owner->stacksController->addNewAnim( new CPointEffectAnimation(owner, soundBase::WALLMISS, "CSGRCK.DEF", shotTarget));
+}
 
+void CCatapultAnimation::createProjectile(const Point & from, const Point & dest) const
+{
+	owner->projectilesController->createCatapultProjectile(attackingStack, from, dest);
+}
 
-	if(projectileAngle > straightAngle)
-		group = CCreatureAnim::VCMI_CAST_UP;
-	else if(projectileAngle < -straightAngle)
-		group = CCreatureAnim::VCMI_CAST_DOWN;
-	else
-		group = CCreatureAnim::VCMI_CAST_FRONT;
 
-	//fall back to H3 cast/2hex
-	//even if creature have 2hex attack instead of cast it is ok since we fall back to attack anyway
-	if(myAnim->framesInGroup(group) == 0)
-	{
-		if(projectileAngle > straightAngle)
-			group = CCreatureAnim::CAST_UP;
-		else if(projectileAngle < -straightAngle)
-			group = CCreatureAnim::CAST_DOWN;
-		else
-			group = CCreatureAnim::CAST_FRONT;
-	}
+CCastAnimation::CCastAnimation(CBattleInterface * owner_, const CStack * attacker, BattleHex dest_, const CStack * defender, const CSpell * spell)
+	: CRangedAttackAnimation(owner_, attacker, dest_, defender),
+	  spell(spell)
+{
+	assert(dest.isValid());// FIXME: when?
 
-	//fall back to ranged attack
-	if(myAnim->framesInGroup(group) == 0)
-	{
-		if(projectileAngle > straightAngle)
-			group = CCreatureAnim::SHOOT_UP;
-		else if(projectileAngle < -straightAngle)
-			group = CCreatureAnim::SHOOT_DOWN;
-		else
-			group = CCreatureAnim::SHOOT_FRONT;
-	}
+	if(!dest_.isValid() && defender)
+		dest = defender->getPosition();
+}
 
-	//fall back to normal attack
-	if(myAnim->framesInGroup(group) == 0)
+CCreatureAnim::EAnimType CCastAnimation::findValidGroup( const std::vector<CCreatureAnim::EAnimType> candidates ) const
+{
+	for ( auto group : candidates)
 	{
-		if(projectileAngle > straightAngle)
-			group = CCreatureAnim::ATTACK_UP;
-		else if(projectileAngle < -straightAngle)
-			group = CCreatureAnim::ATTACK_DOWN;
-		else
-			group = CCreatureAnim::ATTACK_FRONT;
+		if(myAnim->framesInGroup(group) > 0)
+			return group;
 	}
 
-	return true;
+	assert(0);
+	return CCreatureAnim::HOLDING;
 }
 
-void CCastAnimation::nextFrame()
+CCreatureAnim::EAnimType CCastAnimation::getUpwardsGroup() const
 {
-	for(auto & it : pendingAnimations())
-	{
-		CReverseAnimation * anim = dynamic_cast<CReverseAnimation *>(it);
-		if(anim && anim->stack->ID == stack->ID && anim->priority)
-			return;
-	}
+	return findValidGroup({
+		CCreatureAnim::VCMI_CAST_UP,
+		CCreatureAnim::CAST_UP,
+		CCreatureAnim::SHOOT_UP,
+		CCreatureAnim::ATTACK_UP
+	});
+}
 
-	if(myAnim->getType() != group)
-	{
-		myAnim->setType(group);
-		myAnim->onAnimationReset += [&](){ delete this; };
-	}
+CCreatureAnim::EAnimType CCastAnimation::getForwardGroup() const
+{
+	return findValidGroup({
+		CCreatureAnim::VCMI_CAST_FRONT,
+		CCreatureAnim::CAST_FRONT,
+		CCreatureAnim::SHOOT_FRONT,
+		CCreatureAnim::ATTACK_FRONT
+	});
+}
 
-	CBattleAnimation::nextFrame();
+CCreatureAnim::EAnimType CCastAnimation::getDownwardsGroup() const
+{
+	return findValidGroup({
+		CCreatureAnim::VCMI_CAST_DOWN,
+		CCreatureAnim::CAST_DOWN,
+		CCreatureAnim::SHOOT_DOWN,
+		CCreatureAnim::ATTACK_DOWN
+	});
 }
 
-CEffectAnimation::CEffectAnimation(CBattleInterface * _owner, std::string _customAnim, int _x, int _y, int _dx, int _dy, bool _Vflip, bool _alignToBottom)
-	: CBattleAnimation(_owner),
-	destTile(BattleHex::INVALID),
-	x(_x),
-	y(_y),
-	dx(_dx),
-	dy(_dy),
-	Vflip(_Vflip),
-	alignToBottom(_alignToBottom)
+void CCastAnimation::createProjectile(const Point & from, const Point & dest) const
 {
-	logAnim->debug("Created effect animation %s", _customAnim);
+	owner->projectilesController->createSpellProjectile(attackingStack, attackedStack, from, dest, spell);
+}
 
-	customAnim = std::make_shared<CAnimation>(_customAnim);
+CPointEffectAnimation::CPointEffectAnimation(CBattleInterface * _owner, soundBase::soundID sound, std::string animationName, int effects):
+	CBattleAnimation(_owner),
+	animation(std::make_shared<CAnimation>(animationName)),
+	sound(sound),
+	effectFlags(effects),
+	soundPlayed(false),
+	soundFinished(false),
+	effectFinished(false)
+{
 }
 
-CEffectAnimation::CEffectAnimation(CBattleInterface * _owner, std::shared_ptr<CAnimation> _customAnim, int _x, int _y, int _dx, int _dy)
-	: CBattleAnimation(_owner),
-	destTile(BattleHex::INVALID),
-	customAnim(_customAnim),
-	x(_x),
-	y(_y),
-	dx(_dx),
-	dy(_dy),
-	Vflip(false),
-	alignToBottom(false)
+CPointEffectAnimation::CPointEffectAnimation(CBattleInterface * _owner, soundBase::soundID sound, std::string animationName, std::vector<BattleHex> pos, int effects):
+	CPointEffectAnimation(_owner, sound, animationName, effects)
 {
-	logAnim->debug("Created custom effect animation");
+	battlehexes = pos;
 }
 
+CPointEffectAnimation::CPointEffectAnimation(CBattleInterface * _owner, soundBase::soundID sound, std::string animationName, BattleHex pos, int effects):
+	CPointEffectAnimation(_owner, sound, animationName, effects)
+{
+	assert(pos.isValid());
+	battlehexes.push_back(pos);
+}
 
-CEffectAnimation::CEffectAnimation(CBattleInterface * _owner, std::string _customAnim, BattleHex _destTile, bool _Vflip, bool _alignToBottom)
-	: CBattleAnimation(_owner),
-	destTile(_destTile),
-	x(-1),
-	y(-1),
-	dx(0),
-	dy(0),
-	Vflip(_Vflip),
-	alignToBottom(_alignToBottom)
+CPointEffectAnimation::CPointEffectAnimation(CBattleInterface * _owner, soundBase::soundID sound, std::string animationName, std::vector<Point> pos, int effects):
+	CPointEffectAnimation(_owner, sound, animationName, effects)
+{
+	positions = pos;
+}
+
+CPointEffectAnimation::CPointEffectAnimation(CBattleInterface * _owner, soundBase::soundID sound, std::string animationName, Point pos, int effects):
+	CPointEffectAnimation(_owner, sound, animationName, effects)
 {
-	logAnim->debug("Created effect animation %s", _customAnim);
-	customAnim = std::make_shared<CAnimation>(_customAnim);
+	positions.push_back(pos);
 }
 
-bool CEffectAnimation::init()
+bool CPointEffectAnimation::init()
 {
 	if(!CBattleAnimation::checkInitialConditions())
 		return false;
 
-	const bool areaEffect = (!destTile.isValid() && x == -1 && y == -1);
-
-	std::shared_ptr<CAnimation> animation = customAnim;
-
 	animation->preload();
-	if(Vflip)
-		animation->verticalFlip();
 
 	auto first = animation->getImage(0, 0, true);
 	if(!first)
@@ -1059,72 +1020,105 @@ bool CEffectAnimation::init()
 		return false;
 	}
 
-	if(areaEffect) //f.e. armageddon
+	if (positions.empty() && battlehexes.empty())
 	{
+		//armageddon, create screen fill
 		for(int i=0; i * first->width() < owner->pos.w ; ++i)
-		{
 			for(int j=0; j * first->height() < owner->pos.h ; ++j)
-			{
-				BattleEffect be;
-				be.effectID = ID;
-				be.animation = animation;
-				be.currentFrame = 0;
+				positions.push_back(Point(i * first->width(), j * first->height()));
+	}
 
-				be.x = i * first->width() + owner->pos.x;
-				be.y = j * first->height() + owner->pos.y;
-				be.position = BattleHex::INVALID;
+	BattleEffect be;
+	be.effectID = ID;
+	be.animation = animation;
+	be.currentFrame = 0;
 
-				owner->effectsController->battleEffects.push_back(be);
-			}
-		}
-	}
-	else // Effects targeted at a specific creature/hex.
+	for ( auto const position : positions)
 	{
-		const CStack * destStack = owner->getCurrentPlayerInterface()->cb->battleGetStackByPos(destTile, false);
-		BattleEffect be;
-		be.effectID = ID;
-		be.animation = animation;
-		be.currentFrame = 0;
+		be.x = position.x;
+		be.y = position.y;
+		be.position = BattleHex::INVALID;
 
+		owner->effectsController->battleEffects.push_back(be);
+	}
 
-		//todo: lightning anim frame count override
-
-//			if(effect == 1)
-//				be.maxFrame = 3;
+	for ( auto const tile : battlehexes)
+	{
+		const CStack * destStack = owner->getCurrentPlayerInterface()->cb->battleGetStackByPos(tile, false);
 
-		be.x = x;
-		be.y = y;
-		if(destTile.isValid())
-		{
-			Rect tilePos = owner->fieldController->hexPosition(destTile);
-			if(x == -1)
-				be.x = tilePos.x + tilePos.w/2 - first->width()/2;
-			if(y == -1)
-			{
-				if(alignToBottom)
-					be.y = tilePos.y + tilePos.h - first->height();
-				else
-					be.y = tilePos.y - first->height()/2;
-			}
+		assert(tile.isValid());
+		if(!tile.isValid())
+			continue;
 
-			// Correction for 2-hex creatures.
-			if(destStack != nullptr && destStack->doubleWide())
-				be.x += (destStack->side == BattleSide::ATTACKER ? -1 : 1)*tilePos.w/2;
-		}
+		Rect tilePos = owner->fieldController->hexPosition(tile);
+		be.position = tile;
+		be.x = tilePos.x + tilePos.w/2 - first->width()/2;
 
-		assert(be.x != -1 && be.y != -1);
+		if(destStack && destStack->doubleWide()) // Correction for 2-hex creatures.
+			be.x += (destStack->side == BattleSide::ATTACKER ? -1 : 1)*tilePos.w/2;
 
-		//Indicate if effect should be drawn on top of everything or just on top of the hex
-		be.position = destTile;
+		if (alignToBottom())
+			be.y = tilePos.y + tilePos.h - first->height();
+		else
+			be.y = tilePos.y - first->height()/2;
 
 		owner->effectsController->battleEffects.push_back(be);
 	}
 	return true;
 }
 
-void CEffectAnimation::nextFrame()
+void CPointEffectAnimation::nextFrame()
+{
+	playSound();
+	playEffect();
+
+	if (soundFinished && effectFinished)
+		delete this;
+}
+
+bool CPointEffectAnimation::alignToBottom() const
+{
+	return effectFlags & ALIGN_TO_BOTTOM;
+}
+
+bool CPointEffectAnimation::waitForSound() const
+{
+	return effectFlags & WAIT_FOR_SOUND;
+}
+
+void CPointEffectAnimation::onEffectFinished()
+{
+	effectFinished = true;
+	clearEffect();
+}
+
+void CPointEffectAnimation::onSoundFinished()
+{
+	soundFinished = true;
+}
+
+void CPointEffectAnimation::playSound()
+{
+	if (soundPlayed)
+		return;
+
+	soundPlayed = true;
+	if (sound == soundBase::invalid)
+	{
+		onSoundFinished();
+		return;
+	}
+
+	int channel = CCS->soundh->playSound(sound);
+
+	if (!waitForSound() || channel == -1)
+		onSoundFinished();
+	else
+		CCS->soundh->setCallback(channel, [&](){ onSoundFinished(); });
+}
+
+void CPointEffectAnimation::playEffect()
 {
-	//notice: there may be more than one effect in owner->battleEffects correcponding to this animation (ie. armageddon)
 	for(auto & elem : owner->effectsController->battleEffects)
 	{
 		if(elem.effectID == ID)
@@ -1133,19 +1127,14 @@ void CEffectAnimation::nextFrame()
 
 			if(elem.currentFrame >= elem.animation->size())
 			{
-				delete this;
-				return;
-			}
-			else
-			{
-				elem.x += dx;
-				elem.y += dy;
+				onEffectFinished();
+				break;
 			}
 		}
 	}
 }
 
-CEffectAnimation::~CEffectAnimation()
+void CPointEffectAnimation::clearEffect()
 {
 	auto & effects = owner->effectsController->battleEffects;
 
@@ -1157,3 +1146,43 @@ CEffectAnimation::~CEffectAnimation()
 			it++;
 	}
 }
+
+CPointEffectAnimation::~CPointEffectAnimation()
+{
+	assert(effectFinished);
+	assert(soundFinished);
+}
+
+CWaitingAnimation::CWaitingAnimation(CBattleInterface * owner_):
+	CBattleAnimation(owner_)
+{}
+
+void CWaitingAnimation::nextFrame()
+{
+	// initialization conditions fulfilled, delay is over
+	delete this;
+}
+
+CWaitingProjectileAnimation::CWaitingProjectileAnimation(CBattleInterface * owner_, const CStack * shooter):
+	CWaitingAnimation(owner_),
+	shooter(shooter)
+{}
+
+bool CWaitingProjectileAnimation::init()
+{
+	for(auto & elem : pendingAnimations())
+	{
+		auto * attackAnim = dynamic_cast<CRangedAttackAnimation *>(elem);
+
+		if( attackAnim && shooter && attackAnim->stack->ID == shooter->ID && !attackAnim->isInitialized() )
+		{
+			// there is ongoing ranged attack that involves our stack, but projectile was not created yet
+			return false;
+		}
+	}
+
+	if(owner->projectilesController->hasActiveProjectile(shooter))
+		return false;
+
+	return true;
+}

+ 104 - 28
client/battle/CBattleAnimations.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "../../lib/battle/BattleHex.h"
+#include "../../lib/CSoundBase.h"
 #include "../widgets/Images.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -128,11 +129,13 @@ public:
 	CMeleeAttackAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked);
 };
 
+/// Base class for all animations that play during stack movement
 class CStackMoveAnimation : public CBattleStackAnimation
 {
 public:
 	BattleHex currentHex;
 
+protected:
 	CStackMoveAnimation(CBattleInterface * _owner, const CStack * _stack, BattleHex _currentHex);
 };
 
@@ -193,60 +196,133 @@ public:
 
 class CRangedAttackAnimation : public CAttackAnimation
 {
-public:
-	CRangedAttackAnimation(CBattleInterface * owner_, const CStack * attacker, BattleHex dest_, const CStack * defender);
+
+	void setAnimationGroup();
+	void initializeProjectile();
+	void emitProjectile();
+	void emitExplosion();
+
 protected:
+	bool projectileEmitted;
+
+	virtual CCreatureAnim::EAnimType getUpwardsGroup() const = 0;
+	virtual CCreatureAnim::EAnimType getForwardGroup() const = 0;
+	virtual CCreatureAnim::EAnimType getDownwardsGroup() const = 0;
+
+	virtual void createProjectile(const Point & from, const Point & dest) const = 0;
 
+public:
+	CRangedAttackAnimation(CBattleInterface * owner_, const CStack * attacker, BattleHex dest, const CStack * defender);
+	~CRangedAttackAnimation();
+
+	bool init() override;
+	void nextFrame() override;
 };
 
 /// Shooting attack
 class CShootingAnimation : public CRangedAttackAnimation
+{
+	CCreatureAnim::EAnimType getUpwardsGroup() const override;
+	CCreatureAnim::EAnimType getForwardGroup() const override;
+	CCreatureAnim::EAnimType getDownwardsGroup() const override;
+
+	void createProjectile(const Point & from, const Point & dest) const override;
+
+public:
+	CShootingAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex dest, const CStack * defender);
+
+};
+
+/// Catapult attack
+class CCatapultAnimation : public CShootingAnimation
 {
 private:
-	bool projectileEmitted;
 	bool explosionEmitted;
 	int catapultDamage;
 
-	void setAnimationGroup();
-	void initializeProjectile();
-	void emitProjectile();
-	void emitExplosion();
 public:
-	bool init() override;
-	void nextFrame() override;
+	CCatapultAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex dest, const CStack * defender, int _catapultDmg = 0);
 
-	//last two params only for catapult attacks
-	CShootingAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex _dest,
-		const CStack * _attacked, bool _catapult = false, int _catapultDmg = 0);
-	~CShootingAnimation();
+	void createProjectile(const Point & from, const Point & dest) const override;
+	void nextFrame() override;
 };
 
 class CCastAnimation : public CRangedAttackAnimation
 {
+	const CSpell * spell;
+
+	CCreatureAnim::EAnimType findValidGroup( const std::vector<CCreatureAnim::EAnimType> candidates ) const;
+	CCreatureAnim::EAnimType getUpwardsGroup() const override;
+	CCreatureAnim::EAnimType getForwardGroup() const override;
+	CCreatureAnim::EAnimType getDownwardsGroup() const override;
+
+	void createProjectile(const Point & from, const Point & dest) const override;
+
 public:
-	CCastAnimation(CBattleInterface * owner_, const CStack * attacker, BattleHex dest_, const CStack * defender);
+	CCastAnimation(CBattleInterface * owner_, const CStack * attacker, BattleHex dest_, const CStack * defender, const CSpell * spell);
+};
+
+/// Class that plays effect at one or more positions along with (single) sound effect
+class CPointEffectAnimation : public CBattleAnimation
+{
+	soundBase::soundID sound;
+	bool soundPlayed;
+	bool soundFinished;
+	bool effectFinished;
+	int effectFlags;
+
+	std::shared_ptr<CAnimation>	animation;
+	std::vector<Point> positions;
+	std::vector<BattleHex> battlehexes;
+
+	bool alignToBottom() const;
+	bool waitForSound() const;
+
+	void onEffectFinished();
+	void onSoundFinished();
+	void clearEffect();
+
+	void playSound();
+	void playEffect();
+
+public:
+	enum EEffectFlags
+	{
+		ALIGN_TO_BOTTOM = 1,
+		WAIT_FOR_SOUND  = 2
+	};
+
+	/// Create animation with screen-wide effect
+	CPointEffectAnimation(CBattleInterface * _owner, soundBase::soundID sound, std::string animationName, int effects = 0);
+
+	/// Create animation positioned at point(s). Note that positions must be are absolute, including battleint position offset
+	CPointEffectAnimation(CBattleInterface * _owner, soundBase::soundID sound, std::string animationName, Point pos                 , int effects = 0);
+	CPointEffectAnimation(CBattleInterface * _owner, soundBase::soundID sound, std::string animationName, std::vector<Point> pos    , int effects = 0);
+
+	/// Create animation positioned at certain hex(es)
+	CPointEffectAnimation(CBattleInterface * _owner, soundBase::soundID sound, std::string animationName, BattleHex pos             , int effects = 0);
+	CPointEffectAnimation(CBattleInterface * _owner, soundBase::soundID sound, std::string animationName, std::vector<BattleHex> pos, int effects = 0);
+	 ~CPointEffectAnimation();
 
 	bool init() override;
 	void nextFrame() override;
 };
 
-/// This class manages effect animation
-class CEffectAnimation : public CBattleAnimation
+/// Base class (e.g. for use in dynamic_cast's) for "animations" that wait for certain event
+class CWaitingAnimation : public CBattleAnimation
 {
-private:
-	BattleHex destTile;
-	std::shared_ptr<CAnimation>	customAnim;
-	int	x, y, dx, dy;
-	bool Vflip;
-	bool alignToBottom;
+protected:
+	CWaitingAnimation(CBattleInterface * owner_);
 public:
-	bool init() override;
 	void nextFrame() override;
+};
 
-	CEffectAnimation(CBattleInterface * _owner, std::string _customAnim, int _x, int _y, int _dx = 0, int _dy = 0, bool _Vflip = false, bool _alignToBottom = false);
-
-	CEffectAnimation(CBattleInterface * _owner, std::shared_ptr<CAnimation> _customAnim, int _x, int _y, int _dx = 0, int _dy = 0);
+/// Class that waits till projectile of certain shooter hits a target
+class CWaitingProjectileAnimation : public CWaitingAnimation
+{
+	const CStack * shooter;
+public:
+	CWaitingProjectileAnimation(CBattleInterface * owner_, const CStack * shooter);
 
-	CEffectAnimation(CBattleInterface * _owner, std::string _customAnim, BattleHex _destTile, bool _Vflip = false, bool _alignToBottom = false);
-	 ~CEffectAnimation();
+	bool init() override;
 };

+ 9 - 10
client/battle/CBattleEffectsController.cpp

@@ -35,27 +35,26 @@ CBattleEffectsController::CBattleEffectsController(CBattleInterface * owner):
 
 void CBattleEffectsController::displayEffect(EBattleEffect::EBattleEffect effect, const BattleHex & destTile)
 {
-	std::string customAnim = graphics->battleACToDef[effect][0];
-
-	owner->stacksController->addNewAnim(new CEffectAnimation(owner, customAnim, destTile));//FIXME: check positioning for double-hex creatures
+	displayEffect(effect, soundBase::invalid, destTile);
 }
 
 void CBattleEffectsController::displayEffect(EBattleEffect::EBattleEffect effect, uint32_t soundID, const BattleHex & destTile)
 {
-	displayEffect(effect, destTile);
-	if(soundBase::soundID(soundID) != soundBase::invalid )
-		CCS->soundh->playSound(soundBase::soundID(soundID));
+	std::string customAnim = graphics->battleACToDef[effect][0];
+
+	owner->stacksController->addNewAnim(new CPointEffectAnimation(owner, soundBase::soundID(soundID), customAnim, destTile));
 }
 
 void CBattleEffectsController::displayCustomEffects(const std::vector<CustomEffectInfo> & customEffects)
 {
 	for(const CustomEffectInfo & one : customEffects)
 	{
-		if(one.sound != 0)
-			CCS->soundh->playSound(soundBase::soundID(one.sound));
 		const CStack * s = owner->curInt->cb->battleGetStackByID(one.stack, false);
-		if(s && one.effect != 0)
-			displayEffect(EBattleEffect::EBattleEffect(one.effect), s->getPosition());
+
+		assert(s);
+		assert(one.effect != 0);
+
+		displayEffect(EBattleEffect::EBattleEffect(one.effect), soundBase::soundID(one.sound), s->getPosition());
 	}
 }
 

+ 8 - 5
client/battle/CBattleEffectsController.h

@@ -23,7 +23,7 @@ struct SDL_Surface;
 class CAnimation;
 class CCanvas;
 class CBattleInterface;
-class CEffectAnimation;
+class CPointEffectAnimation;
 
 namespace EBattleEffect
 {
@@ -36,6 +36,7 @@ namespace EBattleEffect
 		BAD_MORALE   = 30,
 		BAD_LUCK     = 48,
 		RESURRECT    = 50,
+		DRAIN_LIFE   = 52, // hardcoded constant in CGameHandler
 		POISON       = 67,
 		DEATH_BLOW   = 73,
 		REGENERATION = 74,
@@ -69,12 +70,14 @@ public:
 
 	void displayCustomEffects(const std::vector<CustomEffectInfo> & customEffects);
 
-	void displayEffect(EBattleEffect::EBattleEffect effect, const BattleHex & destTile); //displays custom effect on the battlefield
-	void displayEffect(EBattleEffect::EBattleEffect effect, uint32_t soundID, const BattleHex & destTile); //displays custom effect on the battlefield
+	//displays custom effect on the battlefield
+	void displayEffect(EBattleEffect::EBattleEffect effect, const BattleHex & destTile);
+	void displayEffect(EBattleEffect::EBattleEffect effect, uint32_t soundID, const BattleHex & destTile);
+	//void displayEffects(EBattleEffect::EBattleEffect effect, uint32_t soundID, const std::vector<BattleHex> & destTiles);
+
 	void battleTriggerEffect(const BattleTriggerEffect & bte);
 
 	void showBattlefieldObjects(std::shared_ptr<CCanvas> canvas, const BattleHex & destTile);
 
-
-	friend class CEffectAnimation; // currently, battleEffects is largely managed by CEffectAnimation, TODO: move this logic into CBattleEffectsController
+	friend class CPointEffectAnimation;
 };

+ 35 - 66
client/battle/CBattleInterface.cpp

@@ -81,9 +81,7 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
 	tacticsMode = static_cast<bool>(tacticianInterface);
 
 	//create stack queue
-
 	bool embedQueue;
-
 	std::string queueSize = settings["battle"]["queueSize"].String();
 
 	if(queueSize == "auto")
@@ -120,7 +118,6 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
 	actionsController.reset( new CBattleActionsController(this));
 	effectsController.reset(new CBattleEffectsController(this));
 
-
 	//loading hero animations
 	if(hero1) // attacking hero
 	{
@@ -182,7 +179,7 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
 			CCS->musich->playMusicFromSet("battle", true, true);
 			battleActionsStarted = true;
 			activateStack();
-			controlPanel->blockUI(settings["session"]["spectate"].Bool());
+			controlPanel->blockUI(settings["session"]["spectate"].Bool() || stacksController->getActiveStack() == nullptr);
 			battleIntroSoundChannel = -1;
 		}
 	};
@@ -203,12 +200,9 @@ CBattleInterface::~CBattleInterface()
 		deactivate();
 	}
 
-	//TODO: play AI tracks if battle was during AI turn
-	//if (!curInt->makingTurn)
-	//CCS->musich->playMusicFromSet(CCS->musich->aiMusics, -1);
-
 	if (adventureInt && adventureInt->selection)
 	{
+		//FIXME: this should be moved to adventureInt which should restore correct track based on selection/active player
 		const auto & terrain = *(LOCPLINT->cb->getTile(adventureInt->selection->visitablePos())->terType);
 		CCS->musich->playMusicFromSet("terrain", terrain.name, true, false);
 	}
@@ -360,9 +354,9 @@ void CBattleInterface::stacksAreAttacked(std::vector<StackAttackedInfo> attacked
 	for(ui8 side = 0; side < 2; side++)
 	{
 		if(killedBySide.at(side) > killedBySide.at(1-side))
-			setHeroAnimation(side, 2);
+			setHeroAnimation(side, CCreatureAnim::HERO_DEFEAT);
 		else if(killedBySide.at(side) < killedBySide.at(1-side))
-			setHeroAnimation(side, 3);
+			setHeroAnimation(side, CCreatureAnim::HERO_VICTORY);
 	}
 }
 
@@ -488,6 +482,7 @@ void CBattleInterface::spellCast(const BattleSpellCast * sc)
 	const SpellID spellID = sc->spellID;
 	const CSpell * spell = spellID.toSpell();
 
+	assert(spell);
 	if(!spell)
 		return;
 
@@ -496,64 +491,31 @@ void CBattleInterface::spellCast(const BattleSpellCast * sc)
 	if (!castSoundPath.empty())
 		CCS->soundh->playSound(castSoundPath);
 
-	const auto casterStackID = sc->casterStack;
-	const CStack * casterStack = nullptr;
-	if(casterStackID >= 0)
+	if ( sc->activeCast )
 	{
-		casterStack = curInt->cb->battleGetStackByID(casterStackID);
-	}
+		const CStack * casterStack = curInt->cb->battleGetStackByID(sc->casterStack);
 
-	Point srccoord = (sc->side ? Point(770, 60) : Point(30, 60)) + pos;	//hero position by default
-	{
-		if(casterStack != nullptr)
+		if(casterStack != nullptr )
 		{
-			srccoord = stacksController->getStackPositionAtHex(casterStack->getPosition(), casterStack);
-			srccoord.x += 250;
-			srccoord.y += 240;
-		}
-	}
-
-	if(casterStack != nullptr && sc->activeCast)
-	{
-		//todo: custom cast animation for hero
-		displaySpellCast(spellID, casterStack->getPosition());
-
-		stacksController->addNewAnim(new CCastAnimation(this, casterStack, sc->tile, curInt->cb->battleGetStackByPos(sc->tile)));
-	}
-
-	waitForAnims(); //wait for cast animation
-
-	//playing projectile animation
-	if (sc->tile.isValid())
-	{
-		Point destcoord = stacksController->getStackPositionAtHex(sc->tile, curInt->cb->battleGetStackByPos(sc->tile)); //position attacked by projectile
-		destcoord.x += 250; destcoord.y += 240;
-
-		//animation angle
-		double angle = atan2(static_cast<double>(destcoord.x - srccoord.x), static_cast<double>(destcoord.y - srccoord.y));
-		bool Vflip = (angle < 0);
-		if (Vflip)
-			angle = -angle;
-
-		std::string animToDisplay = spell->animationInfo.selectProjectile(angle);
+			displaySpellCast(spellID, casterStack->getPosition());
 
-		if(!animToDisplay.empty())
+			stacksController->addNewAnim(new CCastAnimation(this, casterStack, sc->tile, curInt->cb->battleGetStackByPos(sc->tile), spell));
+		}
+		else
+		if (sc->tile.isValid() && !spell->animationInfo.projectile.empty())
 		{
-			//TODO: calculate inside CEffectAnimation
-			std::shared_ptr<CAnimation> tmp = std::make_shared<CAnimation>(animToDisplay);
-			tmp->load(0, 0);
-			auto first = tmp->getImage(0, 0);
+			// this is spell cast by hero with valid destination & valid projectile -> play animation
 
-			//displaying animation
-			double diffX = (destcoord.x - srccoord.x)*(destcoord.x - srccoord.x);
-			double diffY = (destcoord.y - srccoord.y)*(destcoord.y - srccoord.y);
-			double distance = sqrt(diffX + diffY);
+			const CStack * target = curInt->cb->battleGetStackByPos(sc->tile);
+			Point srccoord = (sc->side ? Point(770, 60) : Point(30, 60)) + pos;	//hero position
+			Point destcoord = stacksController->getStackPositionAtHex(sc->tile, target); //position attacked by projectile
+			destcoord += Point(250, 240); // FIXME: what are these constants?
 
-			int steps = static_cast<int>(distance / AnimationControls::getSpellEffectSpeed() + 1);
-			int dx = (destcoord.x - srccoord.x - first->width())/steps;
-			int dy = (destcoord.y - srccoord.y - first->height())/steps;
+			projectilesController->createSpellProjectile( nullptr, target, srccoord, destcoord, spell);
+			projectilesController->emitStackProjectile( nullptr );
 
-			stacksController->addNewAnim(new CEffectAnimation(this, animToDisplay, srccoord.x, srccoord.y, dx, dy, Vflip));
+			// wait fo projectile to end
+			stacksController->addNewAnim(new CWaitingProjectileAnimation(this, nullptr));
 		}
 	}
 
@@ -583,8 +545,8 @@ void CBattleInterface::spellCast(const BattleSpellCast * sc)
 	{
 		Point leftHero = Point(15, 30) + pos;
 		Point rightHero = Point(755, 30) + pos;
-		stacksController->addNewAnim(new CEffectAnimation(this, sc->side ? "SP07_A.DEF" : "SP07_B.DEF", leftHero.x, leftHero.y, 0, 0, false));
-		stacksController->addNewAnim(new CEffectAnimation(this, sc->side ? "SP07_B.DEF" : "SP07_A.DEF", rightHero.x, rightHero.y, 0, 0, false));
+		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));
 	}
 }
 
@@ -626,7 +588,14 @@ void CBattleInterface::displaySpellAnimationQueue(const CSpell::TAnimationQueue
 		if(animation.pause > 0)
 			stacksController->addNewAnim(new CDummyAnimation(this, animation.pause));
 		else
-			stacksController->addNewAnim(new CEffectAnimation(this, animation.resourceName, destinationTile, false, animation.verticalPosition == VerticalPosition::BOTTOM));
+		{
+			if (!destinationTile.isValid())
+				stacksController->addNewAnim(new CPointEffectAnimation(this, soundBase::invalid, animation.resourceName));
+			else if (animation.verticalPosition == VerticalPosition::BOTTOM)
+				stacksController->addNewAnim(new CPointEffectAnimation(this, soundBase::invalid, animation.resourceName, destinationTile, CPointEffectAnimation::ALIGN_TO_BOTTOM));
+			else
+				stacksController->addNewAnim(new CPointEffectAnimation(this, soundBase::invalid, animation.resourceName, destinationTile));
+		}
 	}
 }
 
@@ -705,7 +674,7 @@ void CBattleInterface::endAction(const BattleAction* action)
 	const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber);
 
 	if(action->actionType == EActionType::HERO_SPELL)
-		setHeroAnimation(action->side, 0);
+		setHeroAnimation(action->side, CCreatureAnim::HERO_HOLDING);
 
 	stacksController->endAction(action);
 
@@ -784,7 +753,7 @@ void CBattleInterface::startAction(const BattleAction* action)
 
 	if(action->actionType == EActionType::HERO_SPELL) //when hero casts spell
 	{
-		setHeroAnimation(action->side, 4);
+		setHeroAnimation(action->side, CCreatureAnim::HERO_CAST_SPELL);
 		return;
 	}
 
@@ -839,7 +808,7 @@ void CBattleInterface::tacticNextStack(const CStack * current)
 
 }
 
-void CBattleInterface::obstaclePlaced(const CObstacleInstance & oi)
+void CBattleInterface::obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> oi)
 {
 	obstacleController->obstaclePlaced(oi);
 }

+ 1 - 2
client/battle/CBattleInterface.h

@@ -173,7 +173,7 @@ public:
 	void hideQueue();
 	void showQueue();
 
-	void obstaclePlaced(const CObstacleInstance & oi);
+	void obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> oi);
 
 	void gateStateChanged(const EGateState state);
 
@@ -185,7 +185,6 @@ public:
 
 	friend class CBattleResultWindow;
 	friend class CBattleHero;
-	friend class CEffectAnimation;
 	friend class CBattleStackAnimation;
 	friend class CReverseAnimation;
 	friend class CDefenceAnimation;

+ 78 - 86
client/battle/CBattleObstacleController.cpp

@@ -29,87 +29,91 @@ CBattleObstacleController::CBattleObstacleController(CBattleInterface * owner):
 	auto obst = owner->curInt->cb->battleGetAllObstacles();
 	for(auto & elem : obst)
 	{
-		if(elem->obstacleType == CObstacleInstance::USUAL)
+		if ( elem->obstacleType == CObstacleInstance::MOAT )
+			continue; // handled by siege controller;
+		loadObstacleImage(*elem);
+	}
+}
+
+void CBattleObstacleController::loadObstacleImage(const CObstacleInstance & oi)
+{
+	std::string animationName;
+
+	if (auto spellObstacle = dynamic_cast<const SpellCreatedObstacle*>(&oi))
+	{
+		animationName = spellObstacle->animation;
+	}
+	else
+	{
+		assert( oi.obstacleType == CObstacleInstance::USUAL || oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE);
+		animationName = oi.getInfo().animation;
+	}
+
+	if (animationsCache.count(animationName) == 0)
+	{
+		if (oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
 		{
-			std::string animationName = elem->getInfo().animation;
-
-			auto cached = animationsCache.find(animationName);
-
-			if(cached == animationsCache.end())
-			{
-				auto animation = std::make_shared<CAnimation>(animationName);
-				animationsCache[animationName] = animation;
-				obstacleAnimations[elem->uniqueID] = animation;
-				animation->preload();
-			}
-			else
-			{
-				obstacleAnimations[elem->uniqueID] = cached->second;
-			}
+			// obstacle use single bitmap image for animations
+			auto animation = std::make_shared<CAnimation>();
+			animation->setCustom(animationName, 0, 0);
+			animationsCache[animationName] = animation;
 		}
-		else if (elem->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
+		else
 		{
-			std::string animationName = elem->getInfo().animation;
-
-			auto cached = animationsCache.find(animationName);
-
-			if(cached == animationsCache.end())
-			{
-				auto animation = std::make_shared<CAnimation>();
-				animation->setCustom(animationName, 0, 0);
-				animationsCache[animationName] = animation;
-				obstacleAnimations[elem->uniqueID] = animation;
-				animation->preload();
-			}
-			else
-			{
-				obstacleAnimations[elem->uniqueID] = cached->second;
-			}
+			auto animation = std::make_shared<CAnimation>(animationName);
+			animationsCache[animationName] = animation;
+			animation->preload();
 		}
 	}
+	obstacleAnimations[oi.uniqueID] = animationsCache[animationName];
 }
 
-void CBattleObstacleController::obstaclePlaced(const CObstacleInstance & oi)
+void CBattleObstacleController::obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> & obstacles)
 {
-	//so when multiple obstacles are added, they show up one after another
-	owner->waitForAnims();
+	assert(obstaclesBeingPlaced.empty());
+	for (auto const & oi : obstacles)
+		obstaclesBeingPlaced.push_back(oi->uniqueID);
 
-	//soundBase::soundID sound; // FIXME(v.markovtsev): soundh->playSound() is commented in the end => warning
-
-	std::string defname;
-
-	switch(oi.obstacleType)
+	for (auto const & oi : obstacles)
 	{
-	case CObstacleInstance::SPELL_CREATED:
+		auto spellObstacle = dynamic_cast<const SpellCreatedObstacle*>(oi.get());
+
+		if (!spellObstacle)
 		{
-			auto &spellObstacle = dynamic_cast<const SpellCreatedObstacle&>(oi);
-			defname = spellObstacle.appearAnimation;
-			//TODO: sound
-			//soundBase::QUIKSAND
-			//soundBase::LANDMINE
-			//soundBase::FORCEFLD
-			//soundBase::fireWall
+			logGlobal->error("I don't know how to animate appearing obstacle of type %d", (int)oi->obstacleType);
+			obstaclesBeingPlaced.erase(obstaclesBeingPlaced.begin());
+			continue;
 		}
-		break;
-	default:
-		logGlobal->error("I don't know how to animate appearing obstacle of type %d", (int)oi.obstacleType);
-		return;
-	}
 
-	auto animation = std::make_shared<CAnimation>(defname);
-	animation->preload();
+		std::string defname = spellObstacle->appearAnimation;
+
+		//TODO: sound
+		//soundBase::QUIKSAND
+		//soundBase::LANDMINE
+		//soundBase::FORCEFLD
+		//soundBase::fireWall
 
-	auto first = animation->getImage(0, 0);
-	if(!first)
-		return;
+		auto animation = std::make_shared<CAnimation>(defname);
+		animation->preload();
 
-	//we assume here that effect graphics have the same size as the usual obstacle image
-	// -> if we know how to blit obstacle, let's blit the effect in the same place
-	Point whereTo = getObstaclePosition(first, oi);
-	owner->stacksController->addNewAnim(new CEffectAnimation(owner, animation, whereTo.x, whereTo.y));
+		auto first = animation->getImage(0, 0);
+		if(!first)
+		{
+			obstaclesBeingPlaced.erase(obstaclesBeingPlaced.begin());
+			continue;
+		}
 
-	//TODO we need to wait after playing sound till it's finished, otherwise it overlaps and sounds really bad
-	//CCS->soundh->playSound(sound);
+		//we assume here that effect graphics have the same size as the usual obstacle image
+		// -> if we know how to blit obstacle, let's blit the effect in the same place
+		Point whereTo = getObstaclePosition(first, *oi);
+		owner->stacksController->addNewAnim(new CPointEffectAnimation(owner, soundBase::QUIKSAND, defname, whereTo, CPointEffectAnimation::WAIT_FOR_SOUND));
+
+		//so when multiple obstacles are added, they show up one after another
+		owner->waitForAnims();
+
+		obstaclesBeingPlaced.erase(obstaclesBeingPlaced.begin());
+		loadObstacleImage(*spellObstacle);
+	}
 }
 
 void CBattleObstacleController::showAbsoluteObstacles(std::shared_ptr<CCanvas> canvas, const Point & offset)
@@ -153,40 +157,28 @@ std::shared_ptr<IImage> CBattleObstacleController::getObstacleImage(const CObsta
 	int frameIndex = (owner->animCount+1) *25 / owner->getAnimSpeed();
 	std::shared_ptr<CAnimation> animation;
 
-	if(oi.obstacleType == CObstacleInstance::USUAL || oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
-	{
-		animation = obstacleAnimations[oi.uniqueID];
-	}
-	else if(oi.obstacleType == CObstacleInstance::SPELL_CREATED)
+	if (obstacleAnimations.count(oi.uniqueID) == 0)
 	{
-		const SpellCreatedObstacle * spellObstacle = dynamic_cast<const SpellCreatedObstacle *>(&oi);
-		if(!spellObstacle)
-			return std::shared_ptr<IImage>();
-
-		std::string animationName = spellObstacle->animation;
-
-		auto cacheIter = animationsCache.find(animationName);
-
-		if(cacheIter == animationsCache.end())
+		if (boost::range::find(obstaclesBeingPlaced, oi.uniqueID) != obstaclesBeingPlaced.end())
 		{
-			logAnim->trace("Creating obstacle animation %s", animationName);
-
-			animation = std::make_shared<CAnimation>(animationName);
-			animation->preload();
-			animationsCache[animationName] = animation;
+			// obstacle is not loaded yet, don't show anything
+			return nullptr;
 		}
 		else
 		{
-			animation = cacheIter->second;
+			assert(0); // how?
+			loadObstacleImage(oi);
 		}
 	}
 
+	animation = obstacleAnimations[oi.uniqueID];
+	assert(animation);
+
 	if(animation)
 	{
 		frameIndex %= animation->size(0);
 		return animation->getImage(frameIndex, 0);
 	}
-
 	return nullptr;
 }
 

+ 7 - 1
client/battle/CBattleObstacleController.h

@@ -31,13 +31,19 @@ class CBattleObstacleController
 
 	std::map<si32, std::shared_ptr<CAnimation>> obstacleAnimations;
 
+	// semi-debug member, contains obstacles that should not yet be visible due to ongoing placement animation
+	// used only for sanity checks to ensure that there are no invisible obstacles
+	std::vector<si32> obstaclesBeingPlaced;
+
+	void loadObstacleImage(const CObstacleInstance & oi);
+
 	std::shared_ptr<IImage> getObstacleImage(const CObstacleInstance & oi);
 	Point getObstaclePosition(std::shared_ptr<IImage> image, const CObstacleInstance & obstacle);
 
 public:
 	CBattleObstacleController(CBattleInterface * owner);
 
-	void obstaclePlaced(const CObstacleInstance & oi);
+	void obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> & oi);
 	void showObstacles(SDL_Surface *to, std::vector<std::shared_ptr<const CObstacleInstance>> &obstacles);
 	void showAbsoluteObstacles(std::shared_ptr<CCanvas> canvas, const Point & offset);
 

+ 129 - 75
client/battle/CBattleProjectileController.cpp

@@ -18,6 +18,7 @@
 #include "../gui/Geometries.h"
 #include "../gui/CAnimation.h"
 #include "../gui/CCanvas.h"
+#include "../gui/CGuiHandler.h"
 #include "../CGameInfo.h"
 
 #include "../../lib/CStack.h"
@@ -47,6 +48,7 @@ static double calculateCatapultParabolaY(const Point & from, const Point & dest,
 
 void ProjectileMissile::show(std::shared_ptr<CCanvas> canvas)
 {
+	logAnim->info("Projectile rendering, %d / %d", step, steps);
 	size_t group = reverse ? 1 : 0;
 	auto image = animation->getImage(frameNum, group, true);
 
@@ -61,14 +63,23 @@ void ProjectileMissile::show(std::shared_ptr<CCanvas> canvas)
 
 		canvas->draw(image, pos);
 	}
-
 	++step;
 }
 
+void ProjectileAnimatedMissile::show(std::shared_ptr<CCanvas> canvas)
+{
+	ProjectileMissile::show(canvas);
+	frameProgress += AnimationControls::getSpellEffectSpeed() * GH.mainFPSmng->getElapsedMilliseconds() / 1000;
+	size_t animationSize = animation->size(reverse ? 1 : 0);
+	while (frameProgress > animationSize)
+		frameProgress -= animationSize;
+
+	frameNum = std::floor(frameProgress);
+}
+
 void ProjectileCatapult::show(std::shared_ptr<CCanvas> canvas)
 {
-	size_t group = reverse ? 1 : 0;
-	auto image = animation->getImage(frameNum, group, true);
+	auto image = animation->getImage(frameNum, 0, true);
 
 	if(image)
 	{
@@ -171,8 +182,12 @@ void CBattleProjectileController::initStackProjectile(const CStack * stack)
 		return;
 
 	const CCreature * creature = getShooter(stack);
+	projectilesCache[creature->animation.projectileImageName] = createProjectileImage(creature->animation.projectileImageName);
+}
 
-	std::shared_ptr<CAnimation> projectile = std::make_shared<CAnimation>(creature->animation.projectileImageName);
+std::shared_ptr<CAnimation> CBattleProjectileController::createProjectileImage(const std::string & path )
+{
+	std::shared_ptr<CAnimation> projectile = std::make_shared<CAnimation>(path);
 	projectile->preload();
 
 	if(projectile->size(1) != 0)
@@ -180,7 +195,7 @@ void CBattleProjectileController::initStackProjectile(const CStack * stack)
 	else
 		projectile->createFlippedGroup(0, 1);
 
-	projectilesCache[creature->animation.projectileImageName] = projectile;
+	return projectile;
 }
 
 std::shared_ptr<CAnimation> CBattleProjectileController::getProjectileImage(const CStack * stack)
@@ -196,9 +211,11 @@ std::shared_ptr<CAnimation> CBattleProjectileController::getProjectileImage(cons
 
 void CBattleProjectileController::emitStackProjectile(const CStack * stack)
 {
+	int stackID = stack ? stack->ID : -1;
+
 	for (auto projectile : projectiles)
 	{
-		if ( !projectile->playing && projectile->shooterID == stack->ID)
+		if ( !projectile->playing && projectile->shooterID == stackID)
 		{
 			projectile->playing = true;
 			return;
@@ -228,9 +245,11 @@ void CBattleProjectileController::showProjectiles(std::shared_ptr<CCanvas> canva
 
 bool CBattleProjectileController::hasActiveProjectile(const CStack * stack)
 {
+	int stackID = stack ? stack->ID : -1;
+
 	for(auto const & instance : projectiles)
 	{
-		if(instance->shooterID == stack->ID)
+		if(instance->shooterID == stackID)
 		{
 			return true;
 		}
@@ -238,85 +257,94 @@ bool CBattleProjectileController::hasActiveProjectile(const CStack * stack)
 	return false;
 }
 
-void CBattleProjectileController::createProjectile(const CStack * shooter, const CStack * target, Point from, Point dest)
+int CBattleProjectileController::computeProjectileFlightTime( Point from, Point dest, double animSpeed)
 {
-	const CCreature *shooterInfo = getShooter(shooter);
+	double distanceSquared = (dest.x - from.x) * (dest.x - from.x) + (dest.y - from.y) * (dest.y - from.y);
+	double distance = sqrt(distanceSquared);
+	int steps = std::round(distance / animSpeed);
 
-	std::shared_ptr<ProjectileBase> projectile;
+	if (steps > 0)
+		return steps;
+	return 1;
+}
 
-	if (!target)
-	{
-		auto catapultProjectile= new ProjectileCatapult();
-		projectile.reset(catapultProjectile);
-
-		catapultProjectile->animation = getProjectileImage(shooter);
-		catapultProjectile->wallDamageAmount = 0; //FIXME - receive from caller
-		catapultProjectile->frameNum = 0;
-		catapultProjectile->reverse = false;
-		catapultProjectile->step = 0;
-		catapultProjectile->steps = 0;
-
-		//double animSpeed = AnimationControls::getProjectileSpeed() / 10;
-		//catapultProjectile->steps = std::round(std::abs((dest.x - from.x) / animSpeed));
-	}
-	else
+int CBattleProjectileController::computeProjectileFrameID( Point from, Point dest, const CStack * stack)
+{
+	const CCreature * creature = getShooter(stack);
+
+	auto & angles = creature->animation.missleFrameAngles;
+	auto animation = getProjectileImage(stack);
+
+	// only frames below maxFrame are usable: anything  higher is either no present or we don't know when it should be used
+	size_t maxFrame = std::min<size_t>(angles.size(), animation->size(0));
+
+	assert(maxFrame > 0);
+	double projectileAngle = -atan2(dest.y - from.y, std::abs(dest.x - from.x));
+
+	// values in angles array indicate position from which this frame was rendered, in degrees.
+	// possible range is 90 ... -90, where projectile for +90 will be used for shooting upwards, +0 for shots towards right and -90 for downwards shots
+	// find frame that has closest angle to one that we need for this shot
+	int bestID = 0;
+	double bestDiff = fabs( angles[0] / 180 * M_PI - projectileAngle );
+
+	for (int i=1; i<maxFrame; i++)
 	{
-		if (stackUsesRayProjectile(shooter) && stackUsesMissileProjectile(shooter))
+		double currentDiff = fabs( angles[i] / 180 * M_PI - projectileAngle );
+		if (currentDiff < bestDiff)
 		{
-			logAnim->error("Mod error: Creature '%s' has both missile and ray projectiles configured. Mod should be fixed. Using ray projectile configuration...", shooterInfo->nameSing);
+			bestID = i;
+			bestDiff = currentDiff;
 		}
+	}
+	return bestID;
+}
 
-		if (stackUsesRayProjectile(shooter))
-		{
-			auto rayProjectile = new ProjectileRay();
-			projectile.reset(rayProjectile);
+void CBattleProjectileController::createCatapultProjectile(const CStack * shooter, Point from, Point dest)
+{
+	auto catapultProjectile       = new ProjectileCatapult();
+
+	catapultProjectile->animation = getProjectileImage(shooter);
+	catapultProjectile->frameNum  = 0;
+	catapultProjectile->step      = 0;
+	catapultProjectile->steps     = computeProjectileFlightTime(from, dest, AnimationControls::getCatapultSpeed());
+	catapultProjectile->from      = from;
+	catapultProjectile->dest      = dest;
+	catapultProjectile->shooterID = shooter->ID;
+	catapultProjectile->step      = 0;
+	catapultProjectile->playing   = false;
+
+	projectiles.push_back(std::shared_ptr<ProjectileBase>(catapultProjectile));
+}
 
-			rayProjectile->rayConfig = shooterInfo->animation.projectileRay;
-		}
-		else if (stackUsesMissileProjectile(shooter))
-		{
-			auto missileProjectile = new ProjectileMissile();
-			projectile.reset(missileProjectile);
-
-			auto & angles = shooterInfo->animation.missleFrameAngles;
-
-			missileProjectile->animation = getProjectileImage(shooter);
-			missileProjectile->reverse  = !owner->stacksController->facingRight(shooter);
-
-			// only frames below maxFrame are usable: anything  higher is either no present or we don't know when it should be used
-			size_t maxFrame = std::min<size_t>(angles.size(), missileProjectile->animation->size(0));
-
-			assert(maxFrame > 0);
-			double projectileAngle = -atan2(dest.y - from.y, std::abs(dest.x - from.x));
-
-			// values in angles array indicate position from which this frame was rendered, in degrees.
-			// possible range is 90 ... -90, where projectile for +90 will be used for shooting upwards, +0 for shots towards right and -90 for downwards shots
-			// find frame that has closest angle to one that we need for this shot
-			int bestID = 0;
-			double bestDiff = fabs( angles[0] / 180 * M_PI - projectileAngle );
-
-			for (int i=1; i<maxFrame; i++)
-			{
-				double currentDiff = fabs( angles[i] / 180 * M_PI - projectileAngle );
-				if (currentDiff < bestDiff)
-				{
-					bestID = i;
-					bestDiff = currentDiff;
-				}
-			}
-			missileProjectile->frameNum = bestID;
-		}
+void CBattleProjectileController::createProjectile(const CStack * shooter, const CStack * target, Point from, Point dest)
+{
+	assert(target);
+
+	const CCreature *shooterInfo = getShooter(shooter);
+
+	std::shared_ptr<ProjectileBase> projectile;
+	if (stackUsesRayProjectile(shooter) && stackUsesMissileProjectile(shooter))
+	{
+		logAnim->error("Mod error: Creature '%s' has both missile and ray projectiles configured. Mod should be fixed. Using ray projectile configuration...", shooterInfo->nameSing);
 	}
 
-	double animSpeed = AnimationControls::getProjectileSpeed(); // flight speed of projectile
-	if (!target)
-		animSpeed *= 0.2; // catapult attack needs slower speed
+	if (stackUsesRayProjectile(shooter))
+	{
+		auto rayProjectile = new ProjectileRay();
+		projectile.reset(rayProjectile);
 
-	double distanceSquared = (dest.x - from.x) * (dest.x - from.x) + (dest.y - from.y) * (dest.y - from.y);
-	double distance = sqrt(distanceSquared);
-	projectile->steps = std::round(distance / animSpeed);
-	if(projectile->steps == 0)
-		projectile->steps = 1;
+		rayProjectile->rayConfig = shooterInfo->animation.projectileRay;
+	}
+	else if (stackUsesMissileProjectile(shooter))
+	{
+		auto missileProjectile = new ProjectileMissile();
+		projectile.reset(missileProjectile);
+
+		missileProjectile->animation = getProjectileImage(shooter);
+		missileProjectile->reverse  = !owner->stacksController->facingRight(shooter);
+		missileProjectile->frameNum = computeProjectileFrameID(from, dest, shooter);
+		missileProjectile->steps = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed());
+	}
 
 	projectile->from     = from;
 	projectile->dest     = dest;
@@ -326,3 +354,29 @@ void CBattleProjectileController::createProjectile(const CStack * shooter, const
 
 	projectiles.push_back(projectile);
 }
+
+void CBattleProjectileController::createSpellProjectile(const CStack * shooter, const CStack * target, Point from, Point dest, const CSpell * spell)
+{
+	double projectileAngle = std::abs(atan2(dest.x - from.x, dest.y - from.y));
+	std::string animToDisplay = spell->animationInfo.selectProjectile(projectileAngle);
+
+	assert(!animToDisplay.empty());
+
+	if(!animToDisplay.empty())
+	{
+		auto projectile = new ProjectileAnimatedMissile();
+
+		projectile->animation     = createProjectileImage(animToDisplay);
+		projectile->frameProgress = 0;
+		projectile->frameNum      = 0;
+		projectile->reverse       = from.x > dest.x;
+		projectile->from          = from;
+		projectile->dest          = dest;
+		projectile->shooterID     = shooter ? shooter->ID : -1;
+		projectile->step          = 0;
+		projectile->steps         = computeProjectileFlightTime(from, dest, AnimationControls::getSpellEffectSpeed());
+		projectile->playing       = false;
+
+		projectiles.push_back(std::shared_ptr<ProjectileBase>(projectile));
+	}
+}

+ 16 - 2
client/battle/CBattleProjectileController.h

@@ -15,6 +15,7 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 class CStack;
+class CSpell;
 
 VCMI_LIB_NAMESPACE_END
 
@@ -48,11 +49,18 @@ struct ProjectileMissile : ProjectileBase
 	bool reverse;  // if true, projectile will be flipped by vertical axis
 };
 
-struct ProjectileCatapult : ProjectileMissile
+struct ProjectileAnimatedMissile : ProjectileMissile
+{
+	void show(std::shared_ptr<CCanvas> canvas) override;
+	float frameProgress;
+};
+
+struct ProjectileCatapult : ProjectileBase
 {
 	void show(std::shared_ptr<CCanvas> canvas) override;
 
-	int wallDamageAmount;
+	std::shared_ptr<CAnimation> animation;
+	int frameNum;  // frame to display from projectile animation
 };
 
 struct ProjectileRay : ProjectileBase
@@ -76,6 +84,7 @@ class CBattleProjectileController
 	std::vector<std::shared_ptr<ProjectileBase>> projectiles;
 
 	std::shared_ptr<CAnimation> getProjectileImage(const CStack * stack);
+	std::shared_ptr<CAnimation> createProjectileImage(const std::string & path );
 	void initStackProjectile(const CStack * stack);
 
 	bool stackUsesRayProjectile(const CStack * stack);
@@ -84,6 +93,9 @@ class CBattleProjectileController
 	void showProjectile(std::shared_ptr<CCanvas> canvas, std::shared_ptr<ProjectileBase> projectile);
 
 	const CCreature * getShooter(const CStack * stack);
+
+	int computeProjectileFrameID( Point from, Point dest, const CStack * stack);
+	int computeProjectileFlightTime( Point from, Point dest, double speed);
 public:
 	CBattleProjectileController(CBattleInterface * owner);
 
@@ -92,4 +104,6 @@ public:
 	bool hasActiveProjectile(const CStack * stack);
 	void emitStackProjectile(const CStack * stack);
 	void createProjectile(const CStack * shooter, const CStack * target, Point from, Point dest);
+	void createSpellProjectile(const CStack * shooter, const CStack * target, Point from, Point dest, const CSpell * spell);
+	void createCatapultProjectile(const CStack * shooter, Point from, Point dest);
 };

+ 6 - 5
client/battle/CBattleSiegeController.cpp

@@ -321,18 +321,19 @@ void CBattleSiegeController::stackIsCatapulting(const CatapultAttack & ca)
 		const CStack *stack = owner->curInt->cb->battleGetStackByID(ca.attacker);
 		for (auto attackInfo : ca.attackedParts)
 		{
-			owner->stacksController->addNewAnim(new CShootingAnimation(owner, stack, attackInfo.destinationTile, nullptr, true, attackInfo.damageDealt));
+			owner->stacksController->addNewAnim(new CCatapultAnimation(owner, stack, attackInfo.destinationTile, nullptr, attackInfo.damageDealt));
 		}
 	}
 	else
 	{
+		std::vector<Point> positions;
+
 		//no attacker stack, assume spell-related (earthquake) - only hit animation
 		for (auto attackInfo : ca.attackedParts)
-		{
-			Point destPos = owner->stacksController->getStackPositionAtHex(attackInfo.destinationTile, nullptr) + Point(99, 120);
+			positions.push_back(owner->stacksController->getStackPositionAtHex(attackInfo.destinationTile, nullptr) + Point(99, 120));
 
-			owner->stacksController->addNewAnim(new CEffectAnimation(owner, "SGEXPL.DEF", destPos.x, destPos.y));
-		}
+
+		owner->stacksController->addNewAnim(new CPointEffectAnimation(owner, soundBase::invalid, "SGEXPL.DEF", positions));
 	}
 
 	owner->waitForAnims();

+ 5 - 0
client/battle/CCreatureAnimation.cpp

@@ -113,6 +113,11 @@ float AnimationControls::getProjectileSpeed()
 	return static_cast<float>(settings["battle"]["animationSpeed"].Float() * 100);
 }
 
+float AnimationControls::getCatapultSpeed()
+{
+	return static_cast<float>(settings["battle"]["animationSpeed"].Float() * 20);
+}
+
 float AnimationControls::getSpellEffectSpeed()
 {
 	return static_cast<float>(settings["battle"]["animationSpeed"].Float() * 30);

+ 3 - 0
client/battle/CCreatureAnimation.h

@@ -35,6 +35,9 @@ namespace AnimationControls
 	/// TODO: make it time-based
 	float getProjectileSpeed();
 
+	/// returns speed of catapult projectile
+	float getCatapultSpeed();
+
 	/// returns speed of any spell effects, including any special effects like morale (in frames per second)
 	float getSpellEffectSpeed();
 

+ 2 - 0
lib/battle/CObstacleInstance.cpp

@@ -34,6 +34,8 @@ CObstacleInstance::~CObstacleInstance()
 
 const ObstacleInfo & CObstacleInstance::getInfo() const
 {
+	assert( obstacleType == USUAL || obstacleType == ABSOLUTE_OBSTACLE);
+
 	return *Obstacle(ID).getInfo();
 }
 

+ 1 - 1
lib/spells/CSpellHandler.cpp

@@ -547,7 +547,7 @@ std::string CSpell::AnimationInfo::selectProjectile(const double angle) const
 
 	for(const auto & info : projectile)
 	{
-		if(info.minimumAngle < angle && info.minimumAngle > maximum)
+		if(info.minimumAngle < angle && info.minimumAngle >= maximum)
 		{
 			maximum = info.minimumAngle;
 			res = info.resourceName;