2022-11-17 13:21:03 +02:00
/*
2022-12-11 23:16:23 +02:00
* BattleProjectileController . cpp , part of VCMI engine
2022-11-17 13:21:03 +02:00
*
* Authors : listed in file AUTHORS in main folder
*
* License : GNU General Public License v2 .0 or later
* Full text of license available in license . txt file , in main folder
*
*/
# include "StdInc.h"
2022-12-09 13:38:46 +02:00
# include "BattleProjectileController.h"
2022-11-28 16:43:38 +02:00
2022-12-09 13:38:46 +02:00
# include "BattleInterface.h"
# include "BattleSiegeController.h"
# include "BattleStacksController.h"
# include "CreatureAnimation.h"
2022-11-17 13:21:03 +02:00
2022-11-28 16:43:38 +02:00
# include "../gui/Geometries.h"
# include "../gui/CAnimation.h"
2022-12-11 22:09:57 +02:00
# include "../gui/Canvas.h"
2022-12-01 22:06:42 +02:00
# include "../gui/CGuiHandler.h"
2022-11-28 16:43:38 +02:00
# include "../CGameInfo.h"
# include "../../lib/CStack.h"
# include "../../lib/mapObjects/CGTownInstance.h"
2022-11-25 16:32:23 +02:00
static double calculateCatapultParabolaY ( const Point & from , const Point & dest , int x )
2022-11-17 13:21:03 +02:00
{
2022-11-25 16:32:23 +02:00
double facA = 0.005 ; // seems to be constant
2022-11-17 13:21:03 +02:00
// system of 2 linear equations, solutions of which are missing coefficients
// for quadratic equation a*x*x + b*x + c
double eq [ 2 ] [ 3 ] = {
{ static_cast < double > ( from . x ) , 1.0 , from . y - facA * from . x * from . x } ,
{ static_cast < double > ( dest . x ) , 1.0 , dest . y - facA * dest . x * dest . x }
} ;
// solve system via determinants
double det = eq [ 0 ] [ 0 ] * eq [ 1 ] [ 1 ] - eq [ 1 ] [ 0 ] * eq [ 0 ] [ 1 ] ;
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 ] ;
2022-11-25 16:32:23 +02:00
double facB = detB / det ;
double facC = detC / det ;
2022-11-17 13:21:03 +02:00
2022-11-25 16:32:23 +02:00
return facA * pow ( x , 2.0 ) + facB * x + facC ;
2022-11-17 13:21:03 +02:00
}
2022-12-11 22:09:57 +02:00
void ProjectileMissile : : show ( Canvas & canvas )
2022-11-17 13:21:03 +02:00
{
2022-11-25 16:32:23 +02:00
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 ,
} ;
2022-12-11 22:09:57 +02:00
canvas . draw ( image , pos ) ;
2022-11-25 16:32:23 +02:00
}
+ + step ;
2022-11-17 13:21:03 +02:00
}
2022-12-11 22:09:57 +02:00
void ProjectileAnimatedMissile : : show ( Canvas & canvas )
2022-12-01 22:06:42 +02:00
{
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 ) ;
}
2022-12-11 22:09:57 +02:00
void ProjectileCatapult : : show ( Canvas & canvas )
2022-11-17 13:21:03 +02:00
{
2022-12-01 22:06:42 +02:00
auto image = animation - > getImage ( frameNum , 0 , true ) ;
2022-11-25 16:32:23 +02:00
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 ) ;
2022-12-11 22:09:57 +02:00
canvas . draw ( image , pos ) ;
2022-11-17 13:21:03 +02:00
2022-11-25 16:32:23 +02:00
frameNum = ( frameNum + 1 ) % animation - > size ( 0 ) ;
}
+ + step ;
2022-11-17 13:21:03 +02:00
}
2022-12-11 22:09:57 +02:00
void ProjectileRay : : show ( Canvas & canvas )
2022-11-17 13:21:03 +02:00
{
2022-11-25 16:32:23 +02:00
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 ;
2022-11-17 13:21:03 +02:00
2022-11-25 16:32:23 +02:00
//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
2022-11-17 13:21:03 +02:00
{
2022-11-25 16:32:23 +02:00
int y1 = from . y - rayConfig . size ( ) / 2 ;
int y2 = curr . y - rayConfig . size ( ) / 2 ;
int x1 = from . x ;
int x2 = curr . x ;
2022-11-17 13:21:03 +02:00
2022-11-25 16:32:23 +02:00
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 } ;
2022-11-17 13:21:03 +02:00
2022-12-11 22:09:57 +02:00
canvas . drawLine ( Point ( x1 , y1 + i ) , Point ( x2 , y2 + i ) , beginColor , endColor ) ;
2022-11-25 16:32:23 +02:00
}
2022-11-17 13:21:03 +02:00
}
2022-11-25 16:32:23 +02:00
else // draw in vertical axis
2022-11-17 13:21:03 +02:00
{
2022-11-25 16:32:23 +02:00
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 } ;
2022-12-11 22:09:57 +02:00
canvas . drawLine ( Point ( x1 + i , y1 ) , Point ( x2 + i , y2 ) , beginColor , endColor ) ;
2022-11-25 16:32:23 +02:00
}
2022-11-17 13:21:03 +02:00
}
2022-11-27 17:24:45 +02:00
+ + step ;
2022-11-17 13:21:03 +02:00
}
2022-12-13 13:58:16 +02:00
BattleProjectileController : : BattleProjectileController ( BattleInterface & owner ) :
2022-11-25 16:32:23 +02:00
owner ( owner )
{ }
2022-12-13 13:58:16 +02:00
const CCreature & BattleProjectileController : : getShooter ( const CStack * stack ) const
2022-11-20 19:11:34 +02:00
{
2022-11-25 16:32:23 +02:00
const CCreature * creature = stack - > getCreature ( ) ;
if ( creature - > idNumber = = CreatureID : : ARROW_TOWERS )
2022-12-13 13:58:16 +02:00
creature = owner . siegeController - > getTurretCreature ( ) ;
2022-11-25 16:32:23 +02:00
if ( creature - > animation . missleFrameAngles . empty ( ) )
2022-11-20 19:11:34 +02:00
{
2022-11-25 16:32:23 +02:00
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 ] ;
2022-11-20 19:11:34 +02:00
}
2022-11-25 16:32:23 +02:00
2022-12-13 13:58:16 +02:00
return * creature ;
2022-11-20 19:11:34 +02:00
}
2022-11-17 13:21:03 +02:00
2022-12-13 13:58:16 +02:00
bool BattleProjectileController : : stackUsesRayProjectile ( const CStack * stack ) const
2022-11-17 13:21:03 +02:00
{
2022-12-13 13:58:16 +02:00
return ! getShooter ( stack ) . animation . projectileRay . empty ( ) ;
2022-11-25 16:32:23 +02:00
}
2022-11-17 13:21:03 +02:00
2022-12-13 13:58:16 +02:00
bool BattleProjectileController : : stackUsesMissileProjectile ( const CStack * stack ) const
2022-11-25 16:32:23 +02:00
{
2022-12-13 13:58:16 +02:00
return ! getShooter ( stack ) . animation . projectileImageName . empty ( ) ;
2022-11-25 16:32:23 +02:00
}
2022-11-17 13:21:03 +02:00
2022-12-09 13:26:17 +02:00
void BattleProjectileController : : initStackProjectile ( const CStack * stack )
2022-11-25 16:32:23 +02:00
{
if ( ! stackUsesMissileProjectile ( stack ) )
return ;
2022-11-17 13:21:03 +02:00
2022-12-13 13:58:16 +02:00
const CCreature & creature = getShooter ( stack ) ;
projectilesCache [ creature . animation . projectileImageName ] = createProjectileImage ( creature . animation . projectileImageName ) ;
2022-12-01 22:06:42 +02:00
}
2022-11-17 13:21:03 +02:00
2022-12-09 13:26:17 +02:00
std : : shared_ptr < CAnimation > BattleProjectileController : : createProjectileImage ( const std : : string & path )
2022-12-01 22:06:42 +02:00
{
std : : shared_ptr < CAnimation > projectile = std : : make_shared < CAnimation > ( path ) ;
2022-11-25 16:32:23 +02:00
projectile - > preload ( ) ;
2022-11-17 13:21:03 +02:00
2022-11-25 16:32:23 +02:00
if ( projectile - > size ( 1 ) ! = 0 )
logAnim - > error ( " Expected empty group 1 in stack projectile " ) ;
else
projectile - > createFlippedGroup ( 0 , 1 ) ;
2022-11-17 13:21:03 +02:00
2022-12-01 22:06:42 +02:00
return projectile ;
2022-11-25 16:32:23 +02:00
}
2022-11-17 13:21:03 +02:00
2022-12-09 13:26:17 +02:00
std : : shared_ptr < CAnimation > BattleProjectileController : : getProjectileImage ( const CStack * stack )
2022-11-25 16:32:23 +02:00
{
2022-12-13 13:58:16 +02:00
const CCreature & creature = getShooter ( stack ) ;
std : : string imageName = creature . animation . projectileImageName ;
2022-11-17 13:21:03 +02:00
2022-11-25 16:32:23 +02:00
if ( ! projectilesCache . count ( imageName ) )
initStackProjectile ( stack ) ;
2022-11-17 13:21:03 +02:00
2022-11-25 16:32:23 +02:00
return projectilesCache [ imageName ] ;
}
2022-11-17 13:21:03 +02:00
2022-12-09 13:26:17 +02:00
void BattleProjectileController : : emitStackProjectile ( const CStack * stack )
2022-11-27 17:24:45 +02:00
{
2022-12-01 22:06:42 +02:00
int stackID = stack ? stack - > ID : - 1 ;
2022-11-27 17:24:45 +02:00
for ( auto projectile : projectiles )
{
2022-12-01 22:06:42 +02:00
if ( ! projectile - > playing & & projectile - > shooterID = = stackID )
2022-11-27 17:24:45 +02:00
{
projectile - > playing = true ;
return ;
}
}
}
2022-11-17 13:21:03 +02:00
2022-12-11 22:09:57 +02:00
void BattleProjectileController : : showProjectiles ( Canvas & canvas )
2022-11-25 16:32:23 +02:00
{
2022-12-13 15:17:42 +02:00
for ( auto projectile : projectiles )
2022-11-25 16:32:23 +02:00
{
2022-11-27 17:24:45 +02:00
if ( projectile - > playing )
projectile - > show ( canvas ) ;
2022-11-17 13:21:03 +02:00
}
2022-12-13 15:10:31 +02:00
vstd : : erase_if ( projectiles , [ & ] ( const std : : shared_ptr < ProjectileBase > & projectile ) {
return projectile - > step > projectile - > steps ;
} ) ;
2022-11-17 13:21:03 +02:00
}
2022-12-11 23:16:23 +02:00
bool BattleProjectileController : : hasActiveProjectile ( const CStack * stack ) const
2022-11-17 13:21:03 +02:00
{
2022-12-01 22:06:42 +02:00
int stackID = stack ? stack - > ID : - 1 ;
2022-11-17 13:21:03 +02:00
for ( auto const & instance : projectiles )
{
2022-12-01 22:06:42 +02:00
if ( instance - > shooterID = = stackID )
2022-11-17 13:21:03 +02:00
{
return true ;
}
}
return false ;
}
2022-12-09 13:26:17 +02:00
int BattleProjectileController : : computeProjectileFlightTime ( Point from , Point dest , double animSpeed )
2022-11-17 13:21:03 +02:00
{
2022-12-01 22:06:42 +02:00
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 ) ;
2022-11-17 13:21:03 +02:00
2022-12-01 22:06:42 +02:00
if ( steps > 0 )
return steps ;
return 1 ;
}
2022-11-17 13:21:03 +02:00
2022-12-09 13:26:17 +02:00
int BattleProjectileController : : computeProjectileFrameID ( Point from , Point dest , const CStack * stack )
2022-12-01 22:06:42 +02:00
{
2022-12-13 13:58:16 +02:00
const CCreature & creature = getShooter ( stack ) ;
2022-12-01 22:06:42 +02:00
2022-12-13 13:58:16 +02:00
auto & angles = creature . animation . missleFrameAngles ;
2022-12-01 22:06:42 +02:00
auto animation = getProjectileImage ( stack ) ;
// only frames below maxFrame are usable: anything higher is either no present or we don't know when it should be used
size_t maxFrame = std : : min < size_t > ( angles . size ( ) , animation - > size ( 0 ) ) ;
assert ( maxFrame > 0 ) ;
double projectileAngle = - atan2 ( dest . y - from . y , std : : abs ( dest . x - from . x ) ) ;
// values in angles array indicate position from which this frame was rendered, in degrees.
// possible range is 90 ... -90, where projectile for +90 will be used for shooting upwards, +0 for shots towards right and -90 for downwards shots
// find frame that has closest angle to one that we need for this shot
int bestID = 0 ;
double bestDiff = fabs ( angles [ 0 ] / 180 * M_PI - projectileAngle ) ;
for ( int i = 1 ; i < maxFrame ; i + + )
2022-11-17 13:21:03 +02:00
{
2022-12-01 22:06:42 +02:00
double currentDiff = fabs ( angles [ i ] / 180 * M_PI - projectileAngle ) ;
if ( currentDiff < bestDiff )
2022-11-25 16:32:23 +02:00
{
2022-12-01 22:06:42 +02:00
bestID = i ;
bestDiff = currentDiff ;
2022-11-25 16:32:23 +02:00
}
2022-12-01 22:06:42 +02:00
}
return bestID ;
}
2022-11-17 13:21:03 +02:00
2022-12-09 13:26:17 +02:00
void BattleProjectileController : : createCatapultProjectile ( const CStack * shooter , Point from , Point dest )
2022-12-01 22:06:42 +02:00
{
auto catapultProjectile = new ProjectileCatapult ( ) ;
catapultProjectile - > animation = getProjectileImage ( shooter ) ;
catapultProjectile - > frameNum = 0 ;
catapultProjectile - > step = 0 ;
catapultProjectile - > steps = computeProjectileFlightTime ( from , dest , AnimationControls : : getCatapultSpeed ( ) ) ;
catapultProjectile - > from = from ;
catapultProjectile - > dest = dest ;
catapultProjectile - > shooterID = shooter - > ID ;
catapultProjectile - > step = 0 ;
catapultProjectile - > playing = false ;
projectiles . push_back ( std : : shared_ptr < ProjectileBase > ( catapultProjectile ) ) ;
}
2022-11-17 13:21:03 +02:00
2022-12-11 23:16:23 +02:00
void BattleProjectileController : : createProjectile ( const CStack * shooter , Point from , Point dest )
2022-12-01 22:06:42 +02:00
{
2022-12-13 13:58:16 +02:00
const CCreature & shooterInfo = getShooter ( shooter ) ;
2022-12-01 22:06:42 +02:00
std : : shared_ptr < ProjectileBase > projectile ;
if ( stackUsesRayProjectile ( shooter ) & & stackUsesMissileProjectile ( shooter ) )
{
2022-12-13 13:58:16 +02:00
logAnim - > error ( " Mod error: Creature '%s' has both missile and ray projectiles configured. Mod should be fixed. Using ray projectile configuration... " , shooterInfo . nameSing ) ;
2022-11-27 20:01:52 +02:00
}
2022-11-17 13:21:03 +02:00
2022-12-01 22:06:42 +02:00
if ( stackUsesRayProjectile ( shooter ) )
{
auto rayProjectile = new ProjectileRay ( ) ;
projectile . reset ( rayProjectile ) ;
2022-11-27 17:24:45 +02:00
2022-12-13 13:58:16 +02:00
rayProjectile - > rayConfig = shooterInfo . animation . projectileRay ;
2022-12-01 22:06:42 +02:00
}
else if ( stackUsesMissileProjectile ( shooter ) )
{
auto missileProjectile = new ProjectileMissile ( ) ;
projectile . reset ( missileProjectile ) ;
missileProjectile - > animation = getProjectileImage ( shooter ) ;
2022-12-13 13:58:16 +02:00
missileProjectile - > reverse = ! owner . stacksController - > facingRight ( shooter ) ;
2022-12-01 22:06:42 +02:00
missileProjectile - > frameNum = computeProjectileFrameID ( from , dest , shooter ) ;
}
2022-11-17 13:21:03 +02:00
2022-12-01 23:40:03 +02:00
projectile - > steps = computeProjectileFlightTime ( from , dest , AnimationControls : : getProjectileSpeed ( ) ) ;
projectile - > from = from ;
projectile - > dest = dest ;
2022-11-25 16:32:23 +02:00
projectile - > shooterID = shooter - > ID ;
2022-12-01 23:40:03 +02:00
projectile - > step = 0 ;
projectile - > playing = false ;
2022-11-25 16:32:23 +02:00
projectiles . push_back ( projectile ) ;
2022-11-17 13:21:03 +02:00
}
2022-12-01 22:06:42 +02:00
2022-12-11 23:16:23 +02:00
void BattleProjectileController : : createSpellProjectile ( const CStack * shooter , Point from , Point dest , const CSpell * spell )
2022-12-01 22:06:42 +02:00
{
double projectileAngle = std : : abs ( atan2 ( dest . x - from . x , dest . y - from . y ) ) ;
std : : string animToDisplay = spell - > animationInfo . selectProjectile ( projectileAngle ) ;
assert ( ! animToDisplay . empty ( ) ) ;
if ( ! animToDisplay . empty ( ) )
{
auto projectile = new ProjectileAnimatedMissile ( ) ;
projectile - > animation = createProjectileImage ( animToDisplay ) ;
projectile - > frameProgress = 0 ;
projectile - > frameNum = 0 ;
projectile - > reverse = from . x > dest . x ;
projectile - > from = from ;
projectile - > dest = dest ;
projectile - > shooterID = shooter ? shooter - > ID : - 1 ;
projectile - > step = 0 ;
projectile - > steps = computeProjectileFlightTime ( from , dest , AnimationControls : : getSpellEffectSpeed ( ) ) ;
projectile - > playing = false ;
projectiles . push_back ( std : : shared_ptr < ProjectileBase > ( projectile ) ) ;
}
}