diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 370d41ee0..7bdbd0f2a 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -756,21 +756,24 @@ void CPlayerInterface::battleObstaclesChanged(const std::vector EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; + std::vector> 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) diff --git a/client/battle/CBattleAnimations.cpp b/client/battle/CBattleAnimations.cpp index c84733b6a..f816cdd3f 100644 --- a/client/battle/CBattleAnimations.cpp +++ b/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(this); - CEffectAnimation * thSen = dynamic_cast(this); + auto * thAnim = dynamic_cast(this); + auto * thSen = dynamic_cast(this); for(auto & elem : pendingAnimations()) { - CEffectAnimation * sen = dynamic_cast(elem); + auto * sen = dynamic_cast(elem); // all effect animations can play concurrently with each other if(sen && thSen && sen != thSen) continue; - CReverseAnimation * revAnim = dynamic_cast(elem); + auto * revAnim = dynamic_cast(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(elem); + auto * defAnim = dynamic_cast(elem); if(defAnim && defAnim->stack->ID != stack->ID) continue; - CAttackAnimation * attAnim = dynamic_cast(elem); + auto * attAnim = dynamic_cast(elem); if(attAnim && attAnim->stack->ID != stack->ID) continue; - CEffectAnimation * sen = dynamic_cast(elem); + auto * sen = dynamic_cast(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(it); CReverseAnimation * anim2 = dynamic_cast(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 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(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(_customAnim); + return findValidGroup({ + CCreatureAnim::VCMI_CAST_FRONT, + CCreatureAnim::CAST_FRONT, + CCreatureAnim::SHOOT_FRONT, + CCreatureAnim::ATTACK_FRONT + }); } -CEffectAnimation::CEffectAnimation(CBattleInterface * _owner, std::shared_ptr _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(_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(animationName)), + sound(sound), + effectFlags(effects), + soundPlayed(false), + soundFinished(false), + effectFinished(false) +{ +} + +CPointEffectAnimation::CPointEffectAnimation(CBattleInterface * _owner, soundBase::soundID sound, std::string animationName, std::vector 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 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 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(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; +} diff --git a/client/battle/CBattleAnimations.h b/client/battle/CBattleAnimations.h index 6e25be2fe..25a3f8c0f 100644 --- a/client/battle/CBattleAnimations.h +++ b/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); -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 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 customAnim; - int x, y, dx, dy; - bool Vflip; - bool alignToBottom; + soundBase::soundID sound; + bool soundPlayed; + bool soundFinished; + bool effectFinished; + int effectFlags; + + std::shared_ptr animation; + std::vector positions; + std::vector 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 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 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 _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; }; diff --git a/client/battle/CBattleEffectsController.cpp b/client/battle/CBattleEffectsController.cpp index bf7c541d0..7db1078ca 100644 --- a/client/battle/CBattleEffectsController.cpp +++ b/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 & 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()); } } diff --git a/client/battle/CBattleEffectsController.h b/client/battle/CBattleEffectsController.h index b4b0ef50c..c97287ddc 100644 --- a/client/battle/CBattleEffectsController.h +++ b/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 & 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 & destTiles); + void battleTriggerEffect(const BattleTriggerEffect & bte); void showBattlefieldObjects(std::shared_ptr canvas, const BattleHex & destTile); - - friend class CEffectAnimation; // currently, battleEffects is largely managed by CEffectAnimation, TODO: move this logic into CBattleEffectsController + friend class CPointEffectAnimation; }; diff --git a/client/battle/CBattleInterface.cpp b/client/battle/CBattleInterface.cpp index 1a47c93df..21bc57789 100644 --- a/client/battle/CBattleInterface.cpp +++ b/client/battle/CBattleInterface.cpp @@ -81,9 +81,7 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet tacticsMode = static_cast(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 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(destcoord.x - srccoord.x), static_cast(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 tmp = std::make_shared(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(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> oi) { obstacleController->obstaclePlaced(oi); } diff --git a/client/battle/CBattleInterface.h b/client/battle/CBattleInterface.h index ca7ad9f9c..b5cc403b1 100644 --- a/client/battle/CBattleInterface.h +++ b/client/battle/CBattleInterface.h @@ -173,7 +173,7 @@ public: void hideQueue(); void showQueue(); - void obstaclePlaced(const CObstacleInstance & oi); + void obstaclePlaced(const std::vector> 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; diff --git a/client/battle/CBattleObstacleController.cpp b/client/battle/CBattleObstacleController.cpp index 792b3bd60..9243c3a1f 100644 --- a/client/battle/CBattleObstacleController.cpp +++ b/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) - { - std::string animationName = elem->getInfo().animation; - - auto cached = animationsCache.find(animationName); - - if(cached == animationsCache.end()) - { - auto animation = std::make_shared(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(); - 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(&oi)) { - case CObstacleInstance::SPELL_CREATED: - { - auto &spellObstacle = dynamic_cast(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(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(); + animation->setCustom(animationName, 0, 0); + animationsCache[animationName] = animation; + } + else + { + auto animation = std::make_shared(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> & 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(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(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 canvas, const Point & offset) @@ -153,40 +157,28 @@ std::shared_ptr CBattleObstacleController::getObstacleImage(const CObsta int frameIndex = (owner->animCount+1) *25 / owner->getAnimSpeed(); std::shared_ptr 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(&oi); - if(!spellObstacle) - return std::shared_ptr(); - - 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(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; } diff --git a/client/battle/CBattleObstacleController.h b/client/battle/CBattleObstacleController.h index 96b63bde7..ffd6da1f3 100644 --- a/client/battle/CBattleObstacleController.h +++ b/client/battle/CBattleObstacleController.h @@ -31,13 +31,19 @@ class CBattleObstacleController std::map> 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 obstaclesBeingPlaced; + + void loadObstacleImage(const CObstacleInstance & oi); + std::shared_ptr getObstacleImage(const CObstacleInstance & oi); Point getObstaclePosition(std::shared_ptr image, const CObstacleInstance & obstacle); public: CBattleObstacleController(CBattleInterface * owner); - void obstaclePlaced(const CObstacleInstance & oi); + void obstaclePlaced(const std::vector> & oi); void showObstacles(SDL_Surface *to, std::vector> &obstacles); void showAbsoluteObstacles(std::shared_ptr canvas, const Point & offset); diff --git a/client/battle/CBattleProjectileController.cpp b/client/battle/CBattleProjectileController.cpp index 2968d157c..0c1e114fb 100644 --- a/client/battle/CBattleProjectileController.cpp +++ b/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 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 canvas) canvas->draw(image, pos); } - ++step; } +void ProjectileAnimatedMissile::show(std::shared_ptr 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 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 projectile = std::make_shared(creature->animation.projectileImageName); +std::shared_ptr CBattleProjectileController::createProjectileImage(const std::string & path ) +{ + std::shared_ptr projectile = std::make_shared(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 CBattleProjectileController::getProjectileImage(const CStack * stack) @@ -196,9 +211,11 @@ std::shared_ptr 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 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(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; ianimation = 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(catapultProjectile)); +} + void CBattleProjectileController::createProjectile(const CStack * shooter, const CStack * target, Point from, Point dest) { + assert(target); + const CCreature *shooterInfo = getShooter(shooter); std::shared_ptr 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(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; iframeNum = 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(projectile)); + } +} diff --git a/client/battle/CBattleProjectileController.h b/client/battle/CBattleProjectileController.h index 1b3dac11b..916d4df53 100644 --- a/client/battle/CBattleProjectileController.h +++ b/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 canvas) override; + float frameProgress; +}; + +struct ProjectileCatapult : ProjectileBase { void show(std::shared_ptr canvas) override; - int wallDamageAmount; + std::shared_ptr animation; + int frameNum; // frame to display from projectile animation }; struct ProjectileRay : ProjectileBase @@ -76,6 +84,7 @@ class CBattleProjectileController std::vector> projectiles; std::shared_ptr getProjectileImage(const CStack * stack); + std::shared_ptr 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 canvas, std::shared_ptr 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); }; diff --git a/client/battle/CBattleSiegeController.cpp b/client/battle/CBattleSiegeController.cpp index da0fc3990..d5f6fef31 100644 --- a/client/battle/CBattleSiegeController.cpp +++ b/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 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(); diff --git a/client/battle/CCreatureAnimation.cpp b/client/battle/CCreatureAnimation.cpp index cfeccefd3..1defdb734 100644 --- a/client/battle/CCreatureAnimation.cpp +++ b/client/battle/CCreatureAnimation.cpp @@ -113,6 +113,11 @@ float AnimationControls::getProjectileSpeed() return static_cast(settings["battle"]["animationSpeed"].Float() * 100); } +float AnimationControls::getCatapultSpeed() +{ + return static_cast(settings["battle"]["animationSpeed"].Float() * 20); +} + float AnimationControls::getSpellEffectSpeed() { return static_cast(settings["battle"]["animationSpeed"].Float() * 30); diff --git a/client/battle/CCreatureAnimation.h b/client/battle/CCreatureAnimation.h index 10dc446f2..186d0d674 100644 --- a/client/battle/CCreatureAnimation.h +++ b/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(); diff --git a/lib/battle/CObstacleInstance.cpp b/lib/battle/CObstacleInstance.cpp index 92e07c76c..262fa0380 100644 --- a/lib/battle/CObstacleInstance.cpp +++ b/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(); } diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 6595ab950..b7e9fb44d 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/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;