1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-03-25 21:38:59 +02:00

Refactoring of projectile controller

- projectiles now separated based on type
- each type has its own rendering method
- refactoring of CShootingAnimation
This commit is contained in:
Ivan Savenko 2022-11-25 16:32:23 +02:00
parent abb553d975
commit 4f91b062db
10 changed files with 388 additions and 338 deletions

View File

@ -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<CMovementStartAnimation *>(it.first);
CReverseAnimation * anim2 = dynamic_cast<CReverseAnimation *>(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<CMovementStartAnimation *>(it.first);
CReverseAnimation * anim2 = dynamic_cast<CReverseAnimation *>(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)

View File

@ -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;

View File

@ -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<CCanvas>(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();

View File

@ -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<CCanvas> 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<CCanvas> 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<CCanvas> 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<CAnimation> projectile = std::make_shared<CAnimation>(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<CAnimation> projectile = std::make_shared<CAnimation>(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<CAnimation> 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<ProjectileInfo>::iterator > toBeDeleted;
for (auto it = projectiles.begin(); it!=projectiles.end(); ++it)
void CBattleProjectileController::showProjectiles(std::shared_ptr<CCanvas> 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<int>(it->x - dst.w / 2);
dst.y = static_cast<int>(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<ProjectileBase>());
}
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<ProjectileBase> 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<int>(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<size_t>(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<maxFrame; i++)
{
double currentDiff = fabs( angles[i] / 180 * pi - projectileAngle );
if (currentDiff < bestDiff)
{
bestID = i;
bestDiff = currentDiff;
}
}
spi.frameNum = static_cast<int>(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<size_t>(angles.size(), missileProjectile->animation->size(0));
assert(maxFrame > 0);
double projectileAngle = atan2(dest.y - from.y, std::abs(dest.x - from.x));
// values in angles array indicate position from which this frame was rendered, in degrees.
// find frame that has closest angle to one that we need for this shot
int bestID = 0;
double bestDiff = fabs( angles[0] / 180 * M_PI - projectileAngle );
for (int i=1; i<maxFrame; i++)
{
double currentDiff = fabs( angles[i] / 180 * M_PI - projectileAngle );
if (currentDiff < bestDiff)
{
bestID = i;
bestDiff = currentDiff;
}
}
missileProjectile->frameNum = bestID;
}
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);
}

View File

@ -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<CCanvas> 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<CatapultProjectileInfo> catapultInfo; // holds info about the parabolic trajectory of the cannon
void show(std::shared_ptr<CCanvas> canvas) override;
std::shared_ptr<CAnimation> 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<CCanvas> canvas) override;
int wallDamageAmount;
};
struct ProjectileRay : ProjectileBase
{
void show(std::shared_ptr<CCanvas> canvas) override;
std::vector<CCreature::CreatureAnimation::RayColor> rayConfig;
};
class CBattleProjectileController
{
CBattleInterface * owner;
std::map<int, std::shared_ptr<CAnimation>> idToProjectile;
std::map<int, std::vector<CCreature::CreatureAnimation::RayColor>> idToRay;
/// all projectiles loaded during current battle
std::map<std::string, std::shared_ptr<CAnimation>> projectilesCache;
std::list<ProjectileInfo> projectiles; //projectiles flying on battlefield
// std::map<int, std::shared_ptr<CAnimation>> idToProjectile;
// std::map<int, std::vector<CCreature::CreatureAnimation::RayColor>> idToRay;
/// projectiles currently flying on battlefield
std::vector<std::shared_ptr<ProjectileBase>> projectiles;
std::shared_ptr<CAnimation> getProjectileImage(const CStack * stack);
void initStackProjectile(const CStack * stack);
bool stackUsesRayProjectile(const CStack * stack);
bool stackUsesMissileProjectile(const CStack * stack);
void showProjectile(std::shared_ptr<CCanvas> canvas, std::shared_ptr<ProjectileBase> 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<CCanvas> canvas);
//void fireStackProjectile(const CStack * stack);
bool hasActiveProjectile(const CStack * stack);
void createProjectile(const CStack * shooter, const CStack * target, Point from, Point dest);
};

View File

@ -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)

View File

@ -40,6 +40,11 @@ void CCanvas::draw(std::shared_ptr<CCanvas> 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);

View File

@ -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<CCanvas> 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);
};

View File

@ -362,23 +362,17 @@ void CSDL_Ext::update(SDL_Surface * what)
logGlobal->error("%s SDL_UpdateTexture %s", __FUNCTION__, SDL_GetError());
}
template<typename Int>
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);

View File

@ -49,6 +49,12 @@ inline bool isShiftKeyDown()
}
namespace CSDL_Ext
{
template<typename Int>
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)
{