2023-10-19 16:19:09 +02:00
/*
* BattleProjectileController . cpp , part of VCMI engine
*
* 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"
# include "BattleProjectileController.h"
# include "BattleInterface.h"
# include "BattleSiegeController.h"
# include "BattleStacksController.h"
# include "CreatureAnimation.h"
2024-06-02 17:43:56 +02:00
# include "../render/CAnimation.h"
2023-10-19 16:19:09 +02:00
# include "../render/Canvas.h"
# include "../render/IRenderHandler.h"
# include "../gui/CGuiHandler.h"
# include "../CGameInfo.h"
# include "../../lib/CStack.h"
# include "../../lib/mapObjects/CGTownInstance.h"
static double calculateCatapultParabolaY ( const Point & from , const Point & dest , int x )
{
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
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 ] ;
double facB = detB / det ;
double facC = detC / det ;
return facA * pow ( x , 2.0 ) + facB * x + facC ;
}
void ProjectileMissile : : show ( Canvas & canvas )
{
size_t group = reverse ? 1 : 0 ;
auto image = animation - > getImage ( frameNum , group , true ) ;
if ( image )
{
Point pos {
vstd : : lerp ( from . x , dest . x , progress ) - image - > width ( ) / 2 ,
vstd : : lerp ( from . y , dest . y , progress ) - image - > height ( ) / 2 ,
} ;
canvas . draw ( image , pos ) ;
}
}
void ProjectileMissile : : tick ( uint32_t msPassed )
{
float timePassed = msPassed / 1000.f ;
progress + = timePassed * speed ;
}
void ProjectileAnimatedMissile : : tick ( uint32_t msPassed )
{
ProjectileMissile : : tick ( msPassed ) ;
frameProgress + = AnimationControls : : getSpellEffectSpeed ( ) * msPassed / 1000 ;
size_t animationSize = animation - > size ( reverse ? 1 : 0 ) ;
while ( frameProgress > animationSize )
frameProgress - = animationSize ;
frameNum = std : : floor ( frameProgress ) ;
}
void ProjectileCatapult : : tick ( uint32_t msPassed )
{
frameProgress + = AnimationControls : : getSpellEffectSpeed ( ) * msPassed / 1000 ;
float timePassed = msPassed / 1000.f ;
progress + = timePassed * speed ;
}
void ProjectileCatapult : : show ( Canvas & canvas )
{
int frameCounter = std : : floor ( frameProgress ) ;
int frameIndex = ( frameCounter + 1 ) % animation - > size ( 0 ) ;
auto image = animation - > getImage ( frameIndex , 0 , true ) ;
if ( image )
{
int posX = vstd : : lerp ( from . x , dest . x , progress ) ;
int posY = calculateCatapultParabolaY ( from , dest , posX ) ;
Point pos ( posX , posY ) ;
canvas . draw ( image , pos ) ;
}
}
void ProjectileRay : : show ( Canvas & canvas )
{
Point curr {
vstd : : lerp ( from . x , dest . x , progress ) ,
vstd : : 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 ] ;
canvas . drawLine ( Point ( x1 , y1 + i ) , Point ( x2 , y2 + i ) , ray . start , ray . end ) ;
}
}
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 ] ;
canvas . drawLine ( Point ( x1 + i , y1 ) , Point ( x2 + i , y2 ) , ray . start , ray . end ) ;
}
}
}
void ProjectileRay : : tick ( uint32_t msPassed )
{
float timePassed = msPassed / 1000.f ;
progress + = timePassed * speed ;
}
BattleProjectileController : : BattleProjectileController ( BattleInterface & owner ) :
owner ( owner )
{ }
const CCreature & BattleProjectileController : : getShooter ( const CStack * stack ) const
{
const CCreature * creature = stack - > unitType ( ) ;
if ( creature - > getId ( ) = = CreatureID : : ARROW_TOWERS )
2024-08-28 21:33:56 +02:00
creature = owner . siegeController - > getTurretCreature ( stack - > initialPosition ) ;
2023-10-19 16:19:09 +02:00
2024-06-24 03:23:26 +02:00
if ( creature - > animation . missileFrameAngles . empty ( ) )
2023-10-19 16:19:09 +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 - > getNameSingularTranslated ( ) ) ;
2024-05-17 00:05:51 +02:00
creature = CreatureID ( CreatureID : : ARCHER ) . toCreature ( ) ;
2023-10-19 16:19:09 +02:00
}
return * creature ;
}
bool BattleProjectileController : : stackUsesRayProjectile ( const CStack * stack ) const
{
return ! getShooter ( stack ) . animation . projectileRay . empty ( ) ;
}
bool BattleProjectileController : : stackUsesMissileProjectile ( const CStack * stack ) const
{
return ! getShooter ( stack ) . animation . projectileImageName . empty ( ) ;
}
void BattleProjectileController : : initStackProjectile ( const CStack * stack )
{
if ( ! stackUsesMissileProjectile ( stack ) )
return ;
const CCreature & creature = getShooter ( stack ) ;
projectilesCache [ creature . animation . projectileImageName ] = createProjectileImage ( creature . animation . projectileImageName ) ;
}
std : : shared_ptr < CAnimation > BattleProjectileController : : createProjectileImage ( const AnimationPath & path )
{
2024-06-08 09:35:13 +02:00
std : : shared_ptr < CAnimation > projectile = GH . renderHandler ( ) . loadAnimation ( path , EImageBlitMode : : COLORKEY ) ;
2023-10-19 16:19:09 +02:00
if ( projectile - > size ( 1 ) ! = 0 )
logAnim - > error ( " Expected empty group 1 in stack projectile " ) ;
else
projectile - > createFlippedGroup ( 0 , 1 ) ;
return projectile ;
}
std : : shared_ptr < CAnimation > BattleProjectileController : : getProjectileImage ( const CStack * stack )
{
const CCreature & creature = getShooter ( stack ) ;
AnimationPath imageName = creature . animation . projectileImageName ;
if ( ! projectilesCache . count ( imageName ) )
initStackProjectile ( stack ) ;
return projectilesCache [ imageName ] ;
}
void BattleProjectileController : : emitStackProjectile ( const CStack * stack )
{
int stackID = stack ? stack - > unitId ( ) : - 1 ;
for ( auto projectile : projectiles )
{
if ( ! projectile - > playing & & projectile - > shooterID = = stackID )
{
projectile - > playing = true ;
return ;
}
}
}
void BattleProjectileController : : render ( Canvas & canvas )
{
for ( auto projectile : projectiles )
{
if ( projectile - > playing )
projectile - > show ( canvas ) ;
}
}
void BattleProjectileController : : tick ( uint32_t msPassed )
{
for ( auto projectile : projectiles )
{
if ( projectile - > playing )
projectile - > tick ( msPassed ) ;
}
vstd : : erase_if ( projectiles , [ & ] ( const std : : shared_ptr < ProjectileBase > & projectile ) {
return projectile - > progress > 1.0f ;
} ) ;
}
bool BattleProjectileController : : hasActiveProjectile ( const CStack * stack , bool emittedOnly ) const
{
int stackID = stack ? stack - > unitId ( ) : - 1 ;
for ( auto const & instance : projectiles )
{
if ( instance - > shooterID = = stackID & & ( instance - > playing | | ! emittedOnly ) )
{
return true ;
}
}
return false ;
}
float BattleProjectileController : : computeProjectileFlightTime ( Point from , Point dest , double animSpeed )
{
float distanceSquared = ( dest . x - from . x ) * ( dest . x - from . x ) + ( dest . y - from . y ) * ( dest . y - from . y ) ;
float distance = sqrt ( distanceSquared ) ;
assert ( distance > 1.f ) ;
return animSpeed / std : : max ( 1.f , distance ) ;
}
int BattleProjectileController : : computeProjectileFrameID ( Point from , Point dest , const CStack * stack )
{
const CCreature & creature = getShooter ( stack ) ;
2024-06-24 03:23:26 +02:00
auto & angles = creature . animation . missileFrameAngles ;
2023-10-19 16:19:09 +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 + + )
{
double currentDiff = fabs ( angles [ i ] / 180 * M_PI - projectileAngle ) ;
if ( currentDiff < bestDiff )
{
bestID = i ;
bestDiff = currentDiff ;
}
}
return bestID ;
}
void BattleProjectileController : : createCatapultProjectile ( const CStack * shooter , Point from , Point dest )
{
auto catapultProjectile = new ProjectileCatapult ( ) ;
catapultProjectile - > animation = getProjectileImage ( shooter ) ;
catapultProjectile - > progress = 0 ;
catapultProjectile - > speed = computeProjectileFlightTime ( from , dest , AnimationControls : : getCatapultSpeed ( ) ) ;
catapultProjectile - > from = from ;
catapultProjectile - > dest = dest ;
catapultProjectile - > shooterID = shooter - > unitId ( ) ;
catapultProjectile - > playing = false ;
catapultProjectile - > frameProgress = 0.f ;
projectiles . push_back ( std : : shared_ptr < ProjectileBase > ( catapultProjectile ) ) ;
}
void BattleProjectileController : : createProjectile ( const CStack * shooter , Point from , Point dest )
{
const CCreature & shooterInfo = getShooter ( shooter ) ;
std : : shared_ptr < ProjectileBase > projectile ;
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 . getNameSingularTranslated ( ) ) ;
}
if ( stackUsesRayProjectile ( shooter ) )
{
auto rayProjectile = new ProjectileRay ( ) ;
projectile . reset ( rayProjectile ) ;
rayProjectile - > rayConfig = shooterInfo . animation . projectileRay ;
rayProjectile - > speed = computeProjectileFlightTime ( from , dest , AnimationControls : : getRayProjectileSpeed ( ) ) ;
}
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 - > speed = computeProjectileFlightTime ( from , dest , AnimationControls : : getProjectileSpeed ( ) ) ;
}
projectile - > from = from ;
projectile - > dest = dest ;
projectile - > shooterID = shooter - > unitId ( ) ;
projectile - > progress = 0 ;
projectile - > playing = false ;
projectiles . push_back ( projectile ) ;
}
void BattleProjectileController : : createSpellProjectile ( const CStack * shooter , Point from , Point dest , const CSpell * spell )
{
double projectileAngle = std : : abs ( atan2 ( dest . x - from . x , dest . y - from . y ) ) ;
AnimationPath 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 - > unitId ( ) : - 1 ;
projectile - > progress = 0 ;
projectile - > speed = computeProjectileFlightTime ( from , dest , AnimationControls : : getProjectileSpeed ( ) ) ;
projectile - > playing = false ;
projectiles . push_back ( std : : shared_ptr < ProjectileBase > ( projectile ) ) ;
}
}