mirror of
				https://github.com/vcmi/vcmi.git
				synced 2025-10-31 00:07:39 +02:00 
			
		
		
		
	Refactoring of spell animations, multiple fixes for spell visuals
This commit is contained in:
		| @@ -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) | ||||
|   | ||||
| @@ -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) | ||||
| { | ||||
|  | ||||
| 	logAnim->info("Ranged attack animation created"); | ||||
| } | ||||
|  | ||||
|  | ||||
| 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()); | ||||
| } | ||||
|  | ||||
| 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) | ||||
| { | ||||
| 	logAnim->debug("Created shooting anim for %s", stack->getName()); | ||||
| } | ||||
|  | ||||
| void CShootingAnimation::createProjectile(const Point & from, const Point & dest) const | ||||
| { | ||||
| 	owner->projectilesController->createProjectile(attackingStack, attackedStack, from, dest); | ||||
| } | ||||
|  | ||||
| CCreatureAnim::EAnimType CShootingAnimation::getUpwardsGroup() const | ||||
| { | ||||
| 	return CCreatureAnim::SHOOT_UP; | ||||
| } | ||||
|  | ||||
| CCreatureAnim::EAnimType CShootingAnimation::getForwardGroup() const | ||||
| { | ||||
| 	return CCreatureAnim::SHOOT_FRONT; | ||||
| } | ||||
|  | ||||
| CCreatureAnim::EAnimType CShootingAnimation::getDownwardsGroup() const | ||||
| { | ||||
| 	return CCreatureAnim::SHOOT_DOWN; | ||||
| } | ||||
|  | ||||
| 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(); | ||||
|  | ||||
| 	if ( explosionEmitted) | ||||
| 		return; | ||||
|  | ||||
| 	if ( !projectileEmitted) | ||||
| 		return; | ||||
|  | ||||
| 	if (owner->projectilesController->hasActiveProjectile(attackingStack)) | ||||
| 		return; | ||||
|  | ||||
| 	explosionEmitted = true; | ||||
| 	Point shotTarget = owner->stacksController->getStackPositionAtHex(dest, attackedStack) + Point(225, 225) - Point(126, 105); | ||||
|  | ||||
| 	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); | ||||
| } | ||||
|  | ||||
|  | ||||
| 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? | ||||
|  | ||||
| 	if(!dest_.isValid() && defender) | ||||
| 		dest = defender->getPosition(); | ||||
| } | ||||
|  | ||||
| bool CCastAnimation::init() | ||||
| CCreatureAnim::EAnimType CCastAnimation::findValidGroup( const std::vector<CCreatureAnim::EAnimType> candidates ) const | ||||
| { | ||||
| 	if(!CAttackAnimation::checkInitialConditions()) | ||||
| 		return false; | ||||
|  | ||||
| 	if(!attackingStack || myAnim->isDeadOrDying()) | ||||
| 	for ( auto group : candidates) | ||||
| 	{ | ||||
| 		delete this; | ||||
| 		return false; | ||||
| 		if(myAnim->framesInGroup(group) > 0) | ||||
| 			return group; | ||||
| 	} | ||||
|  | ||||
| 	//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; | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	//TODO: display spell projectile here | ||||
|  | ||||
| 	static const double straightAngle = 0.2; | ||||
|  | ||||
|  | ||||
| 	Point fromPos; | ||||
| 	Point destPos; | ||||
|  | ||||
| 	// 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); | ||||
|  | ||||
| 	destPos = owner->stacksController->getStackPositionAtHex(dest, attackedStack); | ||||
|  | ||||
|  | ||||
| 	double projectileAngle = atan2(fabs((double)destPos.y - fromPos.y), fabs((double)destPos.x - fromPos.x)); | ||||
| 	if(attackingStack->getPosition() < dest) | ||||
| 		projectileAngle = -projectileAngle; | ||||
|  | ||||
|  | ||||
| 	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; | ||||
| 	} | ||||
|  | ||||
| 	//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; | ||||
| 	} | ||||
|  | ||||
| 	//fall back to normal attack | ||||
| 	if(myAnim->framesInGroup(group) == 0) | ||||
| 	{ | ||||
| 		if(projectileAngle > straightAngle) | ||||
| 			group = CCreatureAnim::ATTACK_UP; | ||||
| 		else if(projectileAngle < -straightAngle) | ||||
| 			group = CCreatureAnim::ATTACK_DOWN; | ||||
| 		else | ||||
| 			group = CCreatureAnim::ATTACK_FRONT; | ||||
| 	} | ||||
|  | ||||
| 	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; | ||||
| 	} | ||||
|  | ||||
| 	if(myAnim->getType() != group) | ||||
| 	{ | ||||
| 		myAnim->setType(group); | ||||
| 		myAnim->onAnimationReset += [&](){ delete this; }; | ||||
| 	} | ||||
|  | ||||
| 	CBattleAnimation::nextFrame(); | ||||
| 	return findValidGroup({ | ||||
| 		CCreatureAnim::VCMI_CAST_UP, | ||||
| 		CCreatureAnim::CAST_UP, | ||||
| 		CCreatureAnim::SHOOT_UP, | ||||
| 		CCreatureAnim::ATTACK_UP | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| 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) | ||||
| CCreatureAnim::EAnimType CCastAnimation::getForwardGroup() const | ||||
| { | ||||
| 	logAnim->debug("Created effect animation %s", _customAnim); | ||||
|  | ||||
| 	customAnim = std::make_shared<CAnimation>(_customAnim); | ||||
| 	return findValidGroup({ | ||||
| 		CCreatureAnim::VCMI_CAST_FRONT, | ||||
| 		CCreatureAnim::CAST_FRONT, | ||||
| 		CCreatureAnim::SHOOT_FRONT, | ||||
| 		CCreatureAnim::ATTACK_FRONT | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| 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) | ||||
| CCreatureAnim::EAnimType CCastAnimation::getDownwardsGroup() const | ||||
| { | ||||
| 	logAnim->debug("Created custom effect animation"); | ||||
| 	return findValidGroup({ | ||||
| 		CCreatureAnim::VCMI_CAST_DOWN, | ||||
| 		CCreatureAnim::CAST_DOWN, | ||||
| 		CCreatureAnim::SHOOT_DOWN, | ||||
| 		CCreatureAnim::ATTACK_DOWN | ||||
| 	}); | ||||
| } | ||||
|  | ||||
|  | ||||
| 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) | ||||
| void CCastAnimation::createProjectile(const Point & from, const Point & dest) const | ||||
| { | ||||
| 	logAnim->debug("Created effect animation %s", _customAnim); | ||||
| 	customAnim = std::make_shared<CAnimation>(_customAnim); | ||||
| 	owner->projectilesController->createSpellProjectile(attackingStack, attackedStack, from, dest, spell); | ||||
| } | ||||
|  | ||||
| bool CEffectAnimation::init() | ||||
| 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) | ||||
| { | ||||
| } | ||||
|  | ||||
| CPointEffectAnimation::CPointEffectAnimation(CBattleInterface * _owner, soundBase::soundID sound, std::string animationName, std::vector<BattleHex> pos, int effects): | ||||
| 	CPointEffectAnimation(_owner, sound, animationName, effects) | ||||
| { | ||||
| 	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); | ||||
| } | ||||
|  | ||||
| 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) | ||||
| { | ||||
| 	positions.push_back(pos); | ||||
| } | ||||
|  | ||||
| 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; | ||||
|  | ||||
| 				be.x = i * first->width() + owner->pos.x; | ||||
| 				be.y = j * first->height() + owner->pos.y; | ||||
| 				be.position = BattleHex::INVALID; | ||||
|  | ||||
| 				owner->effectsController->battleEffects.push_back(be); | ||||
| 			} | ||||
| 		} | ||||
| 				positions.push_back(Point(i * first->width(), j * first->height())); | ||||
| 	} | ||||
| 	else // Effects targeted at a specific creature/hex. | ||||
|  | ||||
| 	BattleEffect be; | ||||
| 	be.effectID = ID; | ||||
| 	be.animation = animation; | ||||
| 	be.currentFrame = 0; | ||||
|  | ||||
| 	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 | ||||
| 	for ( auto const tile : battlehexes) | ||||
| 	{ | ||||
| 		const CStack * destStack = owner->getCurrentPlayerInterface()->cb->battleGetStackByPos(tile, false); | ||||
|  | ||||
| //			if(effect == 1) | ||||
| //				be.maxFrame = 3; | ||||
| 		assert(tile.isValid()); | ||||
| 		if(!tile.isValid()) | ||||
| 			continue; | ||||
|  | ||||
| 		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; | ||||
| 			} | ||||
| 		Rect tilePos = owner->fieldController->hexPosition(tile); | ||||
| 		be.position = tile; | ||||
| 		be.x = tilePos.x + tilePos.w/2 - first->width()/2; | ||||
|  | ||||
| 			// Correction for 2-hex creatures. | ||||
| 			if(destStack != nullptr && destStack->doubleWide()) | ||||
| 				be.x += (destStack->side == BattleSide::ATTACKER ? -1 : 1)*tilePos.w/2; | ||||
| 		} | ||||
| 		if(destStack && destStack->doubleWide()) // Correction for 2-hex creatures. | ||||
| 			be.x += (destStack->side == BattleSide::ATTACKER ? -1 : 1)*tilePos.w/2; | ||||
|  | ||||
| 		assert(be.x != -1 && be.y != -1); | ||||
|  | ||||
| 		//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; | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
| protected: | ||||
|  | ||||
| }; | ||||
|  | ||||
| /// Shooting attack | ||||
| class CShootingAnimation : public CRangedAttackAnimation | ||||
| { | ||||
| private: | ||||
| 	bool projectileEmitted; | ||||
| 	bool explosionEmitted; | ||||
| 	int catapultDamage; | ||||
|  | ||||
| 	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; | ||||
| }; | ||||
|  | ||||
| 	//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(); | ||||
| /// 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 explosionEmitted; | ||||
| 	int catapultDamage; | ||||
|  | ||||
| public: | ||||
| 	CCatapultAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex dest, const CStack * defender, int _catapultDmg = 0); | ||||
|  | ||||
| 	void createProjectile(const Point & from, const Point & dest) const override; | ||||
| 	void nextFrame() override; | ||||
| }; | ||||
|  | ||||
| class CCastAnimation : public CRangedAttackAnimation | ||||
| { | ||||
| public: | ||||
| 	CCastAnimation(CBattleInterface * owner_, const CStack * attacker, BattleHex dest_, const CStack * defender); | ||||
| 	const CSpell * spell; | ||||
|  | ||||
| 	bool init() override; | ||||
| 	void nextFrame() override; | ||||
| 	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, const CSpell * spell); | ||||
| }; | ||||
|  | ||||
| /// This class manages effect animation | ||||
| class CEffectAnimation : public CBattleAnimation | ||||
| /// Class that plays effect at one or more positions along with (single) sound effect | ||||
| class CPointEffectAnimation : public CBattleAnimation | ||||
| { | ||||
| private: | ||||
| 	BattleHex destTile; | ||||
| 	std::shared_ptr<CAnimation>	customAnim; | ||||
| 	int	x, y, dx, dy; | ||||
| 	bool Vflip; | ||||
| 	bool alignToBottom; | ||||
| 	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; | ||||
|  | ||||
| 	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); | ||||
|  | ||||
| 	CEffectAnimation(CBattleInterface * _owner, std::string _customAnim, BattleHex _destTile, bool _Vflip = false, bool _alignToBottom = false); | ||||
| 	 ~CEffectAnimation(); | ||||
| }; | ||||
|  | ||||
| /// Base class (e.g. for use in dynamic_cast's) for "animations" that wait for certain event | ||||
| class CWaitingAnimation : public CBattleAnimation | ||||
| { | ||||
| protected: | ||||
| 	CWaitingAnimation(CBattleInterface * owner_); | ||||
| public: | ||||
| 	void nextFrame() override; | ||||
| }; | ||||
|  | ||||
| /// 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); | ||||
|  | ||||
| 	bool init() override; | ||||
| }; | ||||
|   | ||||
| @@ -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()); | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
| }; | ||||
|   | ||||
| @@ -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; | ||||
| 			displaySpellCast(spellID, casterStack->getPosition()); | ||||
|  | ||||
| 			stacksController->addNewAnim(new CCastAnimation(this, casterStack, sc->tile, curInt->cb->battleGetStackByPos(sc->tile), spell)); | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	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); | ||||
|  | ||||
| 		if(!animToDisplay.empty()) | ||||
| 		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); | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -29,87 +29,91 @@ CBattleObstacleController::CBattleObstacleController(CBattleInterface * owner): | ||||
| 	auto obst = owner->curInt->cb->battleGetAllObstacles(); | ||||
| 	for(auto & elem : obst) | ||||
| 	{ | ||||
| 		if(elem->obstacleType == CObstacleInstance::USUAL) | ||||
| 		{ | ||||
| 			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; | ||||
| 			} | ||||
| 		} | ||||
| 		else if (elem->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>(); | ||||
| 				animation->setCustom(animationName, 0, 0); | ||||
| 				animationsCache[animationName] = animation; | ||||
| 				obstacleAnimations[elem->uniqueID] = animation; | ||||
| 				animation->preload(); | ||||
| 			} | ||||
| 			else | ||||
| 			{ | ||||
| 				obstacleAnimations[elem->uniqueID] = cached->second; | ||||
| 			} | ||||
| 		} | ||||
| 		if ( elem->obstacleType == CObstacleInstance::MOAT ) | ||||
| 			continue; // handled by siege controller; | ||||
| 		loadObstacleImage(*elem); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| void CBattleObstacleController::obstaclePlaced(const CObstacleInstance & oi) | ||||
| void CBattleObstacleController::loadObstacleImage(const CObstacleInstance & oi) | ||||
| { | ||||
| 	//so when multiple obstacles are added, they show up one after another | ||||
| 	owner->waitForAnims(); | ||||
| 	std::string animationName; | ||||
|  | ||||
| 	//soundBase::soundID sound; // FIXME(v.markovtsev): soundh->playSound() is commented in the end => warning | ||||
|  | ||||
| 	std::string defname; | ||||
|  | ||||
| 	switch(oi.obstacleType) | ||||
| 	if (auto spellObstacle = dynamic_cast<const SpellCreatedObstacle*>(&oi)) | ||||
| 	{ | ||||
| 	case CObstacleInstance::SPELL_CREATED: | ||||
| 		{ | ||||
| 			auto &spellObstacle = dynamic_cast<const SpellCreatedObstacle&>(oi); | ||||
| 			defname = spellObstacle.appearAnimation; | ||||
| 			//TODO: sound | ||||
| 			//soundBase::QUIKSAND | ||||
| 			//soundBase::LANDMINE | ||||
| 			//soundBase::FORCEFLD | ||||
| 			//soundBase::fireWall | ||||
| 		} | ||||
| 		break; | ||||
| 	default: | ||||
| 		logGlobal->error("I don't know how to animate appearing obstacle of type %d", (int)oi.obstacleType); | ||||
| 		return; | ||||
| 		animationName = spellObstacle->animation; | ||||
| 	} | ||||
| 	else | ||||
| 	{ | ||||
| 		assert( oi.obstacleType == CObstacleInstance::USUAL || oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE); | ||||
| 		animationName = oi.getInfo().animation; | ||||
| 	} | ||||
|  | ||||
| 	auto animation = std::make_shared<CAnimation>(defname); | ||||
| 	animation->preload(); | ||||
| 	if (animationsCache.count(animationName) == 0) | ||||
| 	{ | ||||
| 		if (oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) | ||||
| 		{ | ||||
| 			// obstacle use single bitmap image for animations | ||||
| 			auto animation = std::make_shared<CAnimation>(); | ||||
| 			animation->setCustom(animationName, 0, 0); | ||||
| 			animationsCache[animationName] = animation; | ||||
| 		} | ||||
| 		else | ||||
| 		{ | ||||
| 			auto animation = std::make_shared<CAnimation>(animationName); | ||||
| 			animationsCache[animationName] = animation; | ||||
| 			animation->preload(); | ||||
| 		} | ||||
| 	} | ||||
| 	obstacleAnimations[oi.uniqueID] = animationsCache[animationName]; | ||||
| } | ||||
|  | ||||
| 	auto first = animation->getImage(0, 0); | ||||
| 	if(!first) | ||||
| 		return; | ||||
| void CBattleObstacleController::obstaclePlaced(const std::vector<std::shared_ptr<const CObstacleInstance>> & obstacles) | ||||
| { | ||||
| 	assert(obstaclesBeingPlaced.empty()); | ||||
| 	for (auto const & oi : obstacles) | ||||
| 		obstaclesBeingPlaced.push_back(oi->uniqueID); | ||||
|  | ||||
| 	//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)); | ||||
| 	for (auto const & oi : obstacles) | ||||
| 	{ | ||||
| 		auto spellObstacle = dynamic_cast<const SpellCreatedObstacle*>(oi.get()); | ||||
|  | ||||
| 	//TODO we need to wait after playing sound till it's finished, otherwise it overlaps and sounds really bad | ||||
| 	//CCS->soundh->playSound(sound); | ||||
| 		if (!spellObstacle) | ||||
| 		{ | ||||
| 			logGlobal->error("I don't know how to animate appearing obstacle of type %d", (int)oi->obstacleType); | ||||
| 			obstaclesBeingPlaced.erase(obstaclesBeingPlaced.begin()); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		std::string defname = spellObstacle->appearAnimation; | ||||
|  | ||||
| 		//TODO: sound | ||||
| 		//soundBase::QUIKSAND | ||||
| 		//soundBase::LANDMINE | ||||
| 		//soundBase::FORCEFLD | ||||
| 		//soundBase::fireWall | ||||
|  | ||||
| 		auto animation = std::make_shared<CAnimation>(defname); | ||||
| 		animation->preload(); | ||||
|  | ||||
| 		auto first = animation->getImage(0, 0); | ||||
| 		if(!first) | ||||
| 		{ | ||||
| 			obstaclesBeingPlaced.erase(obstaclesBeingPlaced.begin()); | ||||
| 			continue; | ||||
| 		} | ||||
|  | ||||
| 		//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) | ||||
| 	if (obstacleAnimations.count(oi.uniqueID) == 0) | ||||
| 	{ | ||||
| 		animation = obstacleAnimations[oi.uniqueID]; | ||||
| 	} | ||||
| 	else if(oi.obstacleType == CObstacleInstance::SPELL_CREATED) | ||||
| 	{ | ||||
| 		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; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|  | ||||
| int CBattleProjectileController::computeProjectileFlightTime( Point from, Point dest, double animSpeed) | ||||
| { | ||||
| 	double distanceSquared = (dest.x - from.x) * (dest.x - from.x) + (dest.y - from.y) * (dest.y - from.y); | ||||
| 	double distance = sqrt(distanceSquared); | ||||
| 	int steps = std::round(distance / animSpeed); | ||||
|  | ||||
| 	if (steps > 0) | ||||
| 		return steps; | ||||
| 	return 1; | ||||
| } | ||||
|  | ||||
| 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++) | ||||
| 	{ | ||||
| 		double currentDiff = fabs( angles[i] / 180 * M_PI - projectileAngle ); | ||||
| 		if (currentDiff < bestDiff) | ||||
| 		{ | ||||
| 			bestID = i; | ||||
| 			bestDiff = currentDiff; | ||||
| 		} | ||||
| 	} | ||||
| 	return bestID; | ||||
| } | ||||
|  | ||||
| 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)); | ||||
| } | ||||
|  | ||||
| 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 (!target) | ||||
| 	if (stackUsesRayProjectile(shooter) && stackUsesMissileProjectile(shooter)) | ||||
| 	{ | ||||
| 		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 | ||||
| 	{ | ||||
| 		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); | ||||
| 		} | ||||
|  | ||||
| 		if (stackUsesRayProjectile(shooter)) | ||||
| 		{ | ||||
| 			auto rayProjectile = new ProjectileRay(); | ||||
| 			projectile.reset(rayProjectile); | ||||
|  | ||||
| 			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; | ||||
| 		} | ||||
| 		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)); | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
| }; | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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(); | ||||
|  | ||||
|   | ||||
| @@ -34,6 +34,8 @@ CObstacleInstance::~CObstacleInstance() | ||||
|  | ||||
| const ObstacleInfo & CObstacleInstance::getInfo() const | ||||
| { | ||||
| 	assert( obstacleType == USUAL || obstacleType == ABSOLUTE_OBSTACLE); | ||||
|  | ||||
| 	return *Obstacle(ID).getInfo(); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user