diff --git a/client/battle/CBattleAnimations.cpp b/client/battle/CBattleAnimations.cpp index d91909db5..5b80d757b 100644 --- a/client/battle/CBattleAnimations.cpp +++ b/client/battle/CBattleAnimations.cpp @@ -723,7 +723,8 @@ CRangedAttackAnimation::CRangedAttackAnimation(CBattleInterface * owner_, const CShootingAnimation::CShootingAnimation(CBattleInterface * _owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool _catapult, int _catapultDmg) : CRangedAttackAnimation(_owner, attacker, _dest, _attacked), - catapultDamage(_catapultDmg) + catapultDamage(_catapultDmg), + projectileEmitted(false) { logAnim->debug("Created shooting anim for %s", stack->getName()); } @@ -733,10 +734,9 @@ bool CShootingAnimation::init() if( !CAttackAnimation::checkInitialConditions() ) return false; - const CStack * shooter = attackingStack; - - if(!shooter || myAnim->isDead()) + if(!attackingStack || myAnim->isDead()) { + //FIXME: how is this possible? endAnim(); return false; } @@ -753,80 +753,81 @@ bool CShootingAnimation::init() //if (attackingStack && attackedStack && owner->creDir[attackingStack->ID] == owner->creDir[attackedStack->ID]) // return false; + setAnimationGroup(); + shooting = true; + return true; +} + +void CShootingAnimation::setAnimationGroup() +{ + Point shooterPos = stackAnimation(attackingStack)->pos.topLeft(); + Point shotTarget = CClickableHex::getXYUnitAnim(dest, attackedStack, owner) + Point(225, 225); + //maximal angle in radians between straight horizontal line and shooting line for which shot is considered to be straight (absoulte value) static const double straightAngle = 0.2; - // Get further info about the shooter e.g. relative pos of projectile to unit. - // If the creature id is 149 then it's a arrow tower which has no additional info so get the - // actual arrow tower shooter instead. - const CCreature *shooterInfo = shooter->getCreature(); + double projectileAngle = atan2(shotTarget.y - shooterPos.y, std::abs(shotTarget.x - shooterPos.x)); + + // Calculate projectile start position. Offsets are read out of the CRANIM.TXT. + if (projectileAngle > straightAngle) + group = CCreatureAnim::SHOOT_UP; + else if (projectileAngle < -straightAngle) + group = CCreatureAnim::SHOOT_DOWN; + else + group = CCreatureAnim::SHOOT_FRONT; +} + +void CShootingAnimation::initializeProjectile() +{ + const CCreature *shooterInfo = attackingStack->getCreature(); if(shooterInfo->idNumber == CreatureID::ARROW_TOWERS) shooterInfo = owner->siegeController->getTurretCreature(); - Point shooterPos; - Point shotPos; - Point destPos; + Point shotTarget = CClickableHex::getXYUnitAnim(dest, attackedStack, owner) + Point(225, 225); + Point shotOrigin = stackAnimation(attackingStack)->pos.topLeft() + Point(222, 265); + int multiplier = stackFacingRight(attackingStack) ? 1 : -1; - // NOTE: two lines below return different positions (very notable with 2-hex creatures). Obtaining via creanims seems to be more precise - shooterPos = stackAnimation(shooter)->pos.topLeft(); - //xycoord = CClickableHex::getXYUnitAnim(shooter->position, true, shooter, owner); - - destPos = CClickableHex::getXYUnitAnim(dest, attackedStack, owner) + Point(225, 225); - - // to properly translate coordinates when shooter is rotated - int multiplier = 0; - if (shooter) - multiplier = stackFacingRight(shooter) ? 1 : -1; - else + if (group == CCreatureAnim::SHOOT_UP) { - assert(false); // unreachable? - multiplier = shooter->getCreature()->idNumber == CreatureID::ARROW_TOWERS ? -1 : 1; + shotOrigin.x += ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier; + shotOrigin.y += shooterInfo->animation.upperRightMissleOffsetY; } - - double projectileAngle = atan2(fabs((double)destPos.y - shooterPos.y), fabs((double)destPos.x - shooterPos.x)); - if(shooter->getPosition() < dest) - projectileAngle = -projectileAngle; - - // Calculate projectile start position. Offsets are read out of the CRANIM.TXT. - if (projectileAngle > straightAngle) + else if (group == CCreatureAnim::SHOOT_DOWN) { - //upper shot - shotPos.x = shooterPos.x + 222 + ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier; - shotPos.y = shooterPos.y + 265 + shooterInfo->animation.upperRightMissleOffsetY; + shotOrigin.x += ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier; + shotOrigin.y += shooterInfo->animation.lowerRightMissleOffsetY; } - else if (projectileAngle < -straightAngle) + else if (group == CCreatureAnim::SHOOT_FRONT) { - //lower shot - shotPos.x = shooterPos.x + 222 + ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier; - shotPos.y = shooterPos.y + 265 + shooterInfo->animation.lowerRightMissleOffsetY; + shotOrigin.x += ( -25 + shooterInfo->animation.rightMissleOffsetX ) * multiplier; + shotOrigin.y += shooterInfo->animation.rightMissleOffsetY; } else { - //straight shot - shotPos.x = shooterPos.x + 222 + ( -25 + shooterInfo->animation.rightMissleOffsetX ) * multiplier; - shotPos.y = shooterPos.y + 265 + shooterInfo->animation.rightMissleOffsetY; + assert(0); } - owner->projectilesController->createProjectile(attackingStack, attackedStack, shotPos, destPos); + owner->projectilesController->createProjectile(attackingStack, attackedStack, shotOrigin, shotTarget); +} - //attack animation - - shooting = true; - - if(projectileAngle > straightAngle) - group = CCreatureAnim::SHOOT_UP; - else if(projectileAngle < -straightAngle) - group = CCreatureAnim::SHOOT_DOWN; - else //straight shot - group = CCreatureAnim::SHOOT_FRONT; - - return true; +void CShootingAnimation::emitProjectile() +{ + //owner->projectilesController->fireStackProjectile(attackingStack); + projectileEmitted = true; } void CShootingAnimation::nextFrame() { - if (owner->projectilesController->hasActiveProjectile(attackingStack)) + for(auto & it : pendingAnimations()) + { + CMovementStartAnimation * anim = dynamic_cast(it.first); + CReverseAnimation * anim2 = dynamic_cast(it.first); + if( (anim && anim->stack->ID == stack->ID) || (anim2 && anim2->stack->ID == stack->ID && anim2->priority ) ) + return; + } + + if (!projectileEmitted) { const CCreature *shooterInfo = attackingStack->getCreature(); @@ -836,18 +837,15 @@ void CShootingAnimation::nextFrame() // animation should be paused if there is an active projectile if ( stackAnimation(attackingStack)->getCurrentFrame() >= shooterInfo->animation.attackClimaxFrame ) { - owner->projectilesController->fireStackProjectile(attackingStack);//FIXME: should only be called once + initializeProjectile(); + emitProjectile(); return; } } - for(auto & it : pendingAnimations()) - { - CMovementStartAnimation * anim = dynamic_cast(it.first); - CReverseAnimation * anim2 = dynamic_cast(it.first); - if( (anim && anim->stack->ID == stack->ID) || (anim2 && anim2->stack->ID == stack->ID && anim2->priority ) ) - return; - } + if (projectileEmitted && owner->projectilesController->hasActiveProjectile(attackingStack)) + return; // projectile in air, pause animation + CAttackAnimation::nextFrame(); } @@ -855,7 +853,11 @@ void CShootingAnimation::nextFrame() void CShootingAnimation::endAnim() { // FIXME: is this possible? Animation is over but we're yet to fire projectile? - owner->projectilesController->fireStackProjectile(attackingStack); + if (!projectileEmitted) + { + initializeProjectile(); + emitProjectile(); + } // play wall hit/miss sound for catapult attack if(!attackedStack) diff --git a/client/battle/CBattleAnimations.h b/client/battle/CBattleAnimations.h index 0400e45fc..bf3e0259a 100644 --- a/client/battle/CBattleAnimations.h +++ b/client/battle/CBattleAnimations.h @@ -207,7 +207,12 @@ protected: class CShootingAnimation : public CRangedAttackAnimation { private: + bool projectileEmitted; int catapultDamage; + + void setAnimationGroup(); + void initializeProjectile(); + void emitProjectile(); public: bool init() override; void nextFrame() override; diff --git a/client/battle/CBattleInterface.cpp b/client/battle/CBattleInterface.cpp index b956f07bc..ce65a8bbc 100644 --- a/client/battle/CBattleInterface.cpp +++ b/client/battle/CBattleInterface.cpp @@ -30,6 +30,7 @@ #include "../CVideoHandler.h" #include "../Graphics.h" #include "../gui/CAnimation.h" +#include "../gui/CCanvas.h" #include "../gui/CCursorHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/SDL_Extensions.h" @@ -919,6 +920,8 @@ void CBattleInterface::showAll(SDL_Surface *to) void CBattleInterface::show(SDL_Surface *to) { + auto canvas = std::make_shared(to); + assert(to); SDL_Rect buf; @@ -942,7 +945,7 @@ void CBattleInterface::show(SDL_Surface *to) fieldController->showHighlightedHexes(to); showBattlefieldObjects(to); - projectilesController->showProjectiles(to); + projectilesController->showProjectiles(canvas); if(battleActionsStarted) stacksController->updateBattleAnimations(); diff --git a/client/battle/CBattleProjectileController.cpp b/client/battle/CBattleProjectileController.cpp index 8a9550867..247991947 100644 --- a/client/battle/CBattleProjectileController.cpp +++ b/client/battle/CBattleProjectileController.cpp @@ -14,14 +14,15 @@ #include "../../lib/mapObjects/CGTownInstance.h" #include "../CGameInfo.h" #include "../gui/CAnimation.h" +#include "../gui/CCanvas.h" #include "CBattleInterface.h" #include "CBattleSiegeController.h" #include "CBattleStacksController.h" #include "CCreatureAnimation.h" -CatapultProjectileInfo::CatapultProjectileInfo(const Point &from, const Point &dest) +static double calculateCatapultParabolaY(const Point & from, const Point & dest, int x) { - facA = 0.005; // seems to be constant + double facA = 0.005; // seems to be constant // system of 2 linear equations, solutions of which are missing coefficients // for quadratic equation a*x*x + b*x + c @@ -35,163 +36,205 @@ CatapultProjectileInfo::CatapultProjectileInfo(const Point &from, const Point &d double detB = eq[0][2] *eq[1][1] - eq[1][2] *eq[0][1]; double detC = eq[0][0] *eq[1][2] - eq[1][0] *eq[0][2]; - facB = detB / det; - facC = detC / det; + double facB = detB / det; + double facC = detC / det; - // make sure that parabola is correct e.g. passes through from and dest - assert(fabs(calculateY(from.x) - from.y) < 1.0); - assert(fabs(calculateY(dest.x) - dest.y) < 1.0); + return facA *pow(x, 2.0) + facB *x + facC; } -double CatapultProjectileInfo::calculateY(double x) +void ProjectileMissile::show(std::shared_ptr canvas) { - return facA *pow(x, 2.0) + facB *x + facC; + size_t group = reverse ? 1 : 0; + auto image = animation->getImage(frameNum, group, true); + + if(image) + { + float progress = float(step) / steps; + + Point pos { + CSDL_Ext::lerp(from.x, dest.x, progress) - image->width() / 2, + CSDL_Ext::lerp(from.y, dest.y, progress) - image->height() / 2, + }; + + canvas->draw(image, pos); + } + + ++step; +} + +void ProjectileCatapult::show(std::shared_ptr canvas) +{ + size_t group = reverse ? 1 : 0; + auto image = animation->getImage(frameNum, group, true); + + if(image) + { + float progress = float(step) / steps; + + int posX = CSDL_Ext::lerp(from.x, dest.x, progress); + int posY = calculateCatapultParabolaY(from, dest, posX); + Point pos(posX, posY); + + canvas->draw(image, pos); + + frameNum = (frameNum + 1) % animation->size(0); + + if (step == steps) + { + //TODO: re-enable. Move to ShootingAnimation? What about spells? + // last step - explosion effect + //Point explosion_pos = pos + image->dimensions() / 2 - Point(126, 105); + + //owner->addNewAnim( new CEffectAnimation(owner, catapultDamage ? "SGEXPL.DEF" : "CSGRCK.DEF", animPos.x, animPos.y)); + } + } + ++step; +} + +void ProjectileRay::show(std::shared_ptr canvas) +{ + float progress = float(step) / steps; + + Point curr { + CSDL_Ext::lerp(from.x, dest.x, progress), + CSDL_Ext::lerp(from.y, dest.y, progress), + }; + + Point length = curr - from; + + //select axis to draw ray on, we want angle to be less than 45 degrees so individual sub-rays won't overlap each other + + if (std::abs(length.x) > std::abs(length.y)) // draw in horizontal axis + { + int y1 = from.y - rayConfig.size() / 2; + int y2 = curr.y - rayConfig.size() / 2; + + int x1 = from.x; + int x2 = curr.x; + + for (size_t i = 0; i < rayConfig.size(); ++i) + { + auto ray = rayConfig[i]; + SDL_Color beginColor{ ray.r1, ray.g1, ray.b1, ray.a1}; + SDL_Color endColor { ray.r2, ray.g2, ray.b2, ray.a2}; + + canvas->drawLine(Point(x1, y1 + i), Point(x2, y2+i), beginColor, endColor); + } + } + else // draw in vertical axis + { + int x1 = from.x - rayConfig.size() / 2; + int x2 = curr.x - rayConfig.size() / 2; + + int y1 = from.y; + int y2 = curr.y; + + for (size_t i = 0; i < rayConfig.size(); ++i) + { + auto ray = rayConfig[i]; + SDL_Color beginColor{ ray.r1, ray.g1, ray.b1, ray.a1}; + SDL_Color endColor { ray.r2, ray.g2, ray.b2, ray.a2}; + + canvas->drawLine(Point(x1 + i, y1), Point(x2 + i, y2), beginColor, endColor); + } + } } CBattleProjectileController::CBattleProjectileController(CBattleInterface * owner): owner(owner) -{ +{} +const CCreature * CBattleProjectileController::getShooter(const CStack * stack) +{ + const CCreature * creature = stack->getCreature(); + + if(creature->idNumber == CreatureID::ARROW_TOWERS) + creature = owner->siegeController->getTurretCreature(); + + if(creature->animation.missleFrameAngles.empty()) + { + logAnim->error("Mod error: Creature '%s' on the Archer's tower is not a shooter. Mod should be fixed. Trying to use archer's data instead...", creature->nameSing); + creature = CGI->creh->objects[CreatureID::ARCHER]; + } + + return creature; +} + +bool CBattleProjectileController::stackUsesRayProjectile(const CStack * stack) +{ + return !getShooter(stack)->animation.projectileRay.empty(); +} + +bool CBattleProjectileController::stackUsesMissileProjectile(const CStack * stack) +{ + return !getShooter(stack)->animation.projectileImageName.empty(); } void CBattleProjectileController::initStackProjectile(const CStack * stack) { - const CCreature * creature;//creature whose shots should be loaded - if(stack->getCreature()->idNumber == CreatureID::ARROW_TOWERS) - creature = owner->siegeController->getTurretCreature(); + if (!stackUsesMissileProjectile(stack)) + return; + + const CCreature * creature = getShooter(stack); + + std::shared_ptr projectile = std::make_shared(creature->animation.projectileImageName); + projectile->preload(); + + if(projectile->size(1) != 0) + logAnim->error("Expected empty group 1 in stack projectile"); else - creature = stack->getCreature(); + projectile->createFlippedGroup(0, 1); - if (creature->animation.projectileRay.empty()) - { - std::shared_ptr projectile = std::make_shared(creature->animation.projectileImageName); - projectile->preload(); - - if(projectile->size(1) != 0) - logAnim->error("Expected empty group 1 in stack projectile"); - else - projectile->createFlippedGroup(0, 1); - - idToProjectile[stack->getCreature()->idNumber] = projectile; - } - else - { - idToRay[stack->getCreature()->idNumber] = creature->animation.projectileRay; - } + projectilesCache[creature->animation.projectileImageName] = projectile; } -void CBattleProjectileController::fireStackProjectile(const CStack * stack) +std::shared_ptr CBattleProjectileController::getProjectileImage(const CStack * stack) { - for (auto it = projectiles.begin(); it!=projectiles.end(); ++it) - { - if ( !it->shotDone && it->stackID == stack->ID) - { - it->shotDone = true; - return; - } - } + const CCreature * creature = getShooter(stack); + std::string imageName = creature->animation.projectileImageName; + + if (!projectilesCache.count(imageName)) + initStackProjectile(stack); + + return projectilesCache[imageName]; } -void CBattleProjectileController::showProjectiles(SDL_Surface *to) -{ - assert(to); +//void CBattleProjectileController::fireStackProjectile(const CStack * stack) +//{ +// for (auto it = projectiles.begin(); it!=projectiles.end(); ++it) +// { +// if ( !it->shotDone && it->stackID == stack->ID) +// { +// it->shotDone = true; +// return; +// } +// } +//} - std::list< std::list::iterator > toBeDeleted; - for (auto it = projectiles.begin(); it!=projectiles.end(); ++it) + +void CBattleProjectileController::showProjectiles(std::shared_ptr canvas) +{ + for (auto projectile : projectiles) { // Check if projectile is already visible (shooter animation did the shot) - if (!it->shotDone) - continue; + //if (!it->shotDone) + // continue; - if (idToProjectile.count(it->creID)) - { - size_t group = it->reverse ? 1 : 0; - auto image = idToProjectile[it->creID]->getImage(it->frameNum, group, true); + projectile->show(canvas); - if(image) - { - SDL_Rect dst; - dst.h = image->height(); - dst.w = image->width(); - dst.x = static_cast(it->x - dst.w / 2); - dst.y = static_cast(it->y - dst.h / 2); - - image->draw(to, &dst, nullptr); - } - } - if (idToRay.count(it->creID)) - { - auto const & ray = idToRay[it->creID]; - - if (std::abs(it->dx) > std::abs(it->dy)) // draw in horizontal axis - { - int y1 = it->y0 - ray.size() / 2; - int y2 = it->y - ray.size() / 2; - - int x1 = it->x0; - int x2 = it->x; - - for (size_t i = 0; i < ray.size(); ++i) - { - SDL_Color beginColor{ ray[i].r1, ray[i].g1, ray[i].b1, ray[i].a1}; - SDL_Color endColor { ray[i].r2, ray[i].g2, ray[i].b2, ray[i].a2}; - - CSDL_Ext::drawLine(to, x1, y1 + i, x2, y2 + i, beginColor, endColor); - } - } - else // draw in vertical axis - { - int x1 = it->x0 - ray.size() / 2; - int x2 = it->x - ray.size() / 2; - - int y1 = it->y0; - int y2 = it->y; - - for (size_t i = 0; i < ray.size(); ++i) - { - SDL_Color beginColor{ ray[i].r1, ray[i].g1, ray[i].b1, ray[i].a1}; - SDL_Color endColor { ray[i].r2, ray[i].g2, ray[i].b2, ray[i].a2}; - - CSDL_Ext::drawLine(to, x1 + i, y1, x2 + i, y2, beginColor, endColor); - } - } - } - - // Update projectile - ++it->step; - if (it->step > it->lastStep) - { - toBeDeleted.insert(toBeDeleted.end(), it); - } - else - { - if (it->catapultInfo) - { - // Parabolic shot of the trajectory, as follows: f(x) = ax^2 + bx + c - it->x += it->dx; - it->y = it->catapultInfo->calculateY(it->x); - - ++(it->frameNum); - it->frameNum %= idToProjectile[it->creID]->size(0); - } - else - { - // Normal projectile, just add the calculated "deltas" to the x and y positions. - it->x += it->dx; - it->y += it->dy; - } - } + // finished flying + if ( projectile->step > projectile->steps) + projectile.reset(); } - for (auto & elem : toBeDeleted) - projectiles.erase(elem); + boost::range::remove( projectiles, std::shared_ptr()); } bool CBattleProjectileController::hasActiveProjectile(const CStack * stack) { for(auto const & instance : projectiles) { - if(instance.creID == stack->getCreature()->idNumber) + if(instance->shooterID == stack->getCreature()->idNumber) { return true; } @@ -199,117 +242,87 @@ bool CBattleProjectileController::hasActiveProjectile(const CStack * stack) return false; } - void CBattleProjectileController::createProjectile(const CStack * shooter, const CStack * target, Point from, Point dest) { - // Get further info about the shooter e.g. relative pos of projectile to unit. - // If the creature id is 149 then it's a arrow tower which has no additional info so get the - // actual arrow tower shooter instead. const CCreature *shooterInfo = shooter->getCreature(); - if(shooterInfo->idNumber == CreatureID::ARROW_TOWERS) - shooterInfo = owner->siegeController->getTurretCreature(); + std::shared_ptr projectile; - if(!shooterInfo->animation.missleFrameAngles.size()) - logAnim->error("Mod error: Creature '%s' on the Archer's tower is not a shooter. Mod should be fixed. Trying to use archer's data instead..." - , shooterInfo->nameSing); - - auto & angles = shooterInfo->animation.missleFrameAngles.size() - ? shooterInfo->animation.missleFrameAngles - : CGI->creh->operator[](CreatureID::ARCHER)->animation.missleFrameAngles; - - // recalculate angle taking in account offsets - //projectileAngle = atan2(fabs(destPos.y - spi.y), fabs(destPos.x - spi.x)); - //if(shooter->position < dest) - // projectileAngle = -projectileAngle; - - ProjectileInfo spi; - spi.shotDone = false; - spi.creID = shooter->getCreature()->idNumber; - spi.stackID = shooter->ID; - // reverse if creature is facing right OR this is non-existing stack that is not tower (war machines) - spi.reverse = shooter ? !owner->stacksController->facingRight(shooter) : shooter->getCreature()->idNumber != CreatureID::ARROW_TOWERS; - - spi.step = 0; - spi.frameNum = 0; - - spi.x0 = from.x; - spi.y0 = from.y; - - spi.x = from.x; - spi.y = from.y; - - if (target) + if (!target) { - double animSpeed = AnimationControls::getProjectileSpeed(); // flight speed of projectile - double distanceSquared = (dest.x - spi.x) * (dest.x - spi.x) + (dest.y - spi.y) * (dest.y - spi.y); - double distance = sqrt(distanceSquared); - spi.lastStep = std::round(distance / animSpeed); - if(spi.lastStep == 0) - spi.lastStep = 1; - spi.dx = (dest.x - spi.x) / spi.lastStep; - spi.dy = (dest.y - spi.y) / spi.lastStep; - } - else - { - // Catapult attack - spi.catapultInfo.reset(new CatapultProjectileInfo(Point((int)spi.x, (int)spi.y), dest)); + 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; - spi.lastStep = static_cast(std::abs((dest.x - spi.x) / animSpeed)); - spi.dx = animSpeed; - spi.dy = 0; - - auto img = idToProjectile[spi.creID]->getImage(0); - - // Add explosion anim - Point animPos(dest.x - 126 + img->width() / 2, - dest.y - 105 + img->height() / 2); - - //owner->addNewAnim( new CEffectAnimation(owner, catapultDamage ? "SGEXPL.DEF" : "CSGRCK.DEF", animPos.x, animPos.y)); - } - double pi = std::atan(1)*4; - - //in some cases (known one: hero grants shooter bonus to unit) the shooter stack's projectile may not be properly initialized - if (!idToProjectile.count(spi.creID) && !idToRay.count(spi.creID)) - initStackProjectile(shooter); - - if (idToProjectile.count(spi.creID)) - { - // 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(), idToProjectile.at(spi.creID)->size(0)); - - assert(maxFrame > 0); - double projectileAngle = atan2(fabs((double)dest.y - from.y), fabs((double)dest.x - from.x)); - //if(shooter->getPosition() < dest) - // projectileAngle = -projectileAngle; - - // values in angles array indicate position from which this frame was rendered, in degrees. - // find frame that has closest angle to one that we need for this shot - size_t bestID = 0; - double bestDiff = fabs( angles[0] / 180 * pi - projectileAngle ); - - for (size_t i=1; i(bestID); - } - else if (idToRay.count(spi.creID)) - { - // no-op + catapultProjectile->steps = std::round(std::abs((dest.x - from.x) / animSpeed)); } else { - logGlobal->error("Unable to find valid projectile for shooter %d", spi.creID); + 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. + // 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; + } + + double animSpeed = AnimationControls::getProjectileSpeed(); // flight speed of projectile + 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; } - // Set projectile animation start delay which is specified in frames - projectiles.push_back(spi); + projectile->from = from; + projectile->dest = dest; + projectile->shooterID = shooter->ID; + projectile->step = 0; + + projectiles.push_back(projectile); } diff --git a/client/battle/CBattleProjectileController.h b/client/battle/CBattleProjectileController.h index 36b8bfecf..515115e97 100644 --- a/client/battle/CBattleProjectileController.h +++ b/client/battle/CBattleProjectileController.h @@ -10,56 +10,80 @@ #pragma once #include "../../lib/CCreatureHandler.h" +#include "../gui/Geometries.h" struct Point; struct SDL_Surface; class CAnimation; +class CCanvas; class CStack; class CBattleInterface; -/// Small struct which is needed for drawing the parabolic trajectory of the catapult cannon -struct CatapultProjectileInfo +/// Small struct which contains information about the position and the velocity of a projectile +struct ProjectileBase { - CatapultProjectileInfo(const Point &from, const Point &dest); + virtual ~ProjectileBase() = default; + virtual void show(std::shared_ptr canvas) = 0; - double facA, facB, facC; + Point from; // initial position on the screen + Point dest; // target position on the screen - double calculateY(double x); + int step; // current step counter + int steps; // total number of steps/frames to show + int shooterID; // ID of shooter stack }; -/// Small struct which contains information about the position and the velocity of a projectile -struct ProjectileInfo +struct ProjectileMissile : ProjectileBase { - double x0, y0; //initial position on the screen - double x, y; //position on the screen - double dx, dy; //change in position in one step - int step, lastStep; //to know when finish showing this projectile - int creID; //ID of creature that shot this projectile - int stackID; //ID of stack - int frameNum; //frame to display form projectile animation - //bool spin; //if true, frameNum will be increased - bool shotDone; // actual shot already done, projectile is flying - bool reverse; //if true, projectile will be flipped by vertical asix - std::shared_ptr catapultInfo; // holds info about the parabolic trajectory of the cannon + void show(std::shared_ptr canvas) override; + + std::shared_ptr animation; + int frameNum; // frame to display from projectile animation + bool reverse; // if true, projectile will be flipped by vertical axis +}; + +struct ProjectileCatapult : ProjectileMissile +{ + void show(std::shared_ptr canvas) override; + + int wallDamageAmount; +}; + +struct ProjectileRay : ProjectileBase +{ + void show(std::shared_ptr canvas) override; + + std::vector rayConfig; }; class CBattleProjectileController { CBattleInterface * owner; - std::map> idToProjectile; - std::map> idToRay; + /// all projectiles loaded during current battle + std::map> projectilesCache; - std::list projectiles; //projectiles flying on battlefield +// std::map> idToProjectile; +// std::map> idToRay; + /// projectiles currently flying on battlefield + std::vector> projectiles; + + std::shared_ptr getProjectileImage(const CStack * stack); + void initStackProjectile(const CStack * stack); + + bool stackUsesRayProjectile(const CStack * stack); + bool stackUsesMissileProjectile(const CStack * stack); + + void showProjectile(std::shared_ptr canvas, std::shared_ptr projectile); + + const CCreature * getShooter(const CStack * stack); public: CBattleProjectileController(CBattleInterface * owner); - void showProjectiles(SDL_Surface *to); - void initStackProjectile(const CStack * stack); - void fireStackProjectile(const CStack * stack); + void showProjectiles(std::shared_ptr canvas); + //void fireStackProjectile(const CStack * stack); bool hasActiveProjectile(const CStack * stack); - void createProjectile(const CStack * shooter, const CStack * target, Point from, Point dest); }; diff --git a/client/battle/CBattleStacksController.cpp b/client/battle/CBattleStacksController.cpp index 3db051763..2ae4e1dea 100644 --- a/client/battle/CBattleStacksController.cpp +++ b/client/battle/CBattleStacksController.cpp @@ -199,12 +199,6 @@ void CBattleStacksController::stackAdded(const CStack * stack) creAnims[stack->ID]->pos.y = coords.y; creAnims[stack->ID]->pos.w = creAnims[stack->ID]->getWidth(); creAnims[stack->ID]->setType(CCreatureAnim::HOLDING); - - //loading projectiles for units - if(stack->isShooter()) - { - owner->projectilesController->initStackProjectile(stack); - } } void CBattleStacksController::setActiveStack(const CStack *stack) diff --git a/client/gui/CCanvas.cpp b/client/gui/CCanvas.cpp index 1d7fe3abc..356746ef9 100644 --- a/client/gui/CCanvas.cpp +++ b/client/gui/CCanvas.cpp @@ -40,6 +40,11 @@ void CCanvas::draw(std::shared_ptr image, const Point & pos) image->copyTo(surface, pos); } +void CCanvas::drawLine(const Point & from, const Point & dest, const SDL_Color & colorFrom, const SDL_Color & colorDest) +{ + CSDL_Ext::drawLine(surface, from.x, from.y, dest.x, dest.y, colorFrom, colorDest); +} + void CCanvas::copyTo(SDL_Surface * to, const Point & pos) { blitAt(to, pos.x, pos.y, surface); diff --git a/client/gui/CCanvas.h b/client/gui/CCanvas.h index 5f998be9a..b7aaf6bf1 100644 --- a/client/gui/CCanvas.h +++ b/client/gui/CCanvas.h @@ -9,6 +9,7 @@ */ #pragma once +struct SDL_Color; struct SDL_Surface; struct Point; class IImage; @@ -30,6 +31,9 @@ public: // renders another canvas onto this canvas void draw(std::shared_ptr image, const Point & pos); + // renders continuous, 1-pixel wide line with color gradient + void drawLine(const Point & from, const Point & dest, const SDL_Color & colorFrom, const SDL_Color & colorDest); + // for compatibility, copies content of this canvas onto SDL_Surface void copyTo(SDL_Surface * to, const Point & pos); }; diff --git a/client/gui/SDL_Extensions.cpp b/client/gui/SDL_Extensions.cpp index 6cb332865..c0164413d 100644 --- a/client/gui/SDL_Extensions.cpp +++ b/client/gui/SDL_Extensions.cpp @@ -362,23 +362,17 @@ void CSDL_Ext::update(SDL_Surface * what) logGlobal->error("%s SDL_UpdateTexture %s", __FUNCTION__, SDL_GetError()); } -template -Int lerp(Int a, Int b, float f) -{ - return a + std::round((b - a) * f); -} - static void drawLineX(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color1, const SDL_Color & color2) { for(int x = x1; x <= x2; x++) { float f = float(x - x1) / float(x2 - x1); - int y = lerp(y1, y2, f); + int y = CSDL_Ext::lerp(y1, y2, f); - uint8_t r = lerp(color1.r, color2.r, f); - uint8_t g = lerp(color1.g, color2.g, f); - uint8_t b = lerp(color1.b, color2.b, f); - uint8_t a = lerp(color1.a, color2.a, f); + uint8_t r = CSDL_Ext::lerp(color1.r, color2.r, f); + uint8_t g = CSDL_Ext::lerp(color1.g, color2.g, f); + uint8_t b = CSDL_Ext::lerp(color1.b, color2.b, f); + uint8_t a = CSDL_Ext::lerp(color1.a, color2.a, f); Uint8 *p = CSDL_Ext::getPxPtr(sur, x, y); ColorPutter<4, 0>::PutColor(p, r,g,b,a); @@ -390,12 +384,12 @@ static void drawLineY(SDL_Surface * sur, int x1, int y1, int x2, int y2, const S for(int y = y1; y <= y2; y++) { float f = float(y - y1) / float(y2 - y1); - int x = lerp(x1, x2, f); + int x = CSDL_Ext::lerp(x1, x2, f); - uint8_t r = lerp(color1.r, color2.r, f); - uint8_t g = lerp(color1.g, color2.g, f); - uint8_t b = lerp(color1.b, color2.b, f); - uint8_t a = lerp(color1.a, color2.a, f); + uint8_t r = CSDL_Ext::lerp(color1.r, color2.r, f); + uint8_t g = CSDL_Ext::lerp(color1.g, color2.g, f); + uint8_t b = CSDL_Ext::lerp(color1.b, color2.b, f); + uint8_t a = CSDL_Ext::lerp(color1.a, color2.a, f); Uint8 *p = CSDL_Ext::getPxPtr(sur, x, y); ColorPutter<4, 0>::PutColor(p, r,g,b,a); diff --git a/client/gui/SDL_Extensions.h b/client/gui/SDL_Extensions.h index 0da5dbecf..2a982b10c 100644 --- a/client/gui/SDL_Extensions.h +++ b/client/gui/SDL_Extensions.h @@ -49,6 +49,12 @@ inline bool isShiftKeyDown() } namespace CSDL_Ext { + template + Int lerp(Int a, Int b, float f) + { + return a + std::round((b - a) * f); + } + //todo: should this better be assignment operator? STRONG_INLINE void colorAssign(SDL_Color & dest, const SDL_Color & source) {