Sfoglia il codice sorgente

Refactoring of projectile controller

- projectiles now separated based on type
- each type has its own rendering method
- refactoring of CShootingAnimation
Ivan Savenko 2 anni fa
parent
commit
4f91b062db

+ 66 - 64
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));
 
-	if(shooterInfo->idNumber == CreatureID::ARROW_TOWERS)
-		shooterInfo = owner->siegeController->getTurretCreature();
+	// 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;
+}
 
-	Point shooterPos;
-	Point shotPos;
-	Point destPos;
+void CShootingAnimation::initializeProjectile()
+{
+	const CCreature *shooterInfo = attackingStack->getCreature();
 
-	// 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);
+	if(shooterInfo->idNumber == CreatureID::ARROW_TOWERS)
+		shooterInfo = owner->siegeController->getTurretCreature();
 
-	destPos = CClickableHex::getXYUnitAnim(dest, attackedStack, owner) + Point(225, 225);
+	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;
 
-	// 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);
-
-	//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;
+	owner->projectilesController->createProjectile(attackingStack, attackedStack, shotOrigin, shotTarget);
+}
 
-	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)

+ 5 - 0
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;

+ 4 - 1
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<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();

+ 213 - 200
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<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;
 }
 
-CBattleProjectileController::CBattleProjectileController(CBattleInterface * owner):
-	owner(owner)
+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 CBattleProjectileController::initStackProjectile(const CStack * stack)
+void ProjectileRay::show(std::shared_ptr<CCanvas> canvas)
 {
-	const CCreature * creature;//creature whose shots should be loaded
-	if(stack->getCreature()->idNumber == CreatureID::ARROW_TOWERS)
-		creature = owner->siegeController->getTurretCreature();
-	else
-		creature = stack->getCreature();
+	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;
 
-	if (creature->animation.projectileRay.empty())
+	//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
 	{
-		std::shared_ptr<CAnimation> projectile = std::make_shared<CAnimation>(creature->animation.projectileImageName);
-		projectile->preload();
+		int y1 =  from.y - rayConfig.size() / 2;
+		int y2 =  curr.y - rayConfig.size() / 2;
+
+		int x1 = from.x;
+		int x2 = curr.x;
 
-		if(projectile->size(1) != 0)
-			logAnim->error("Expected empty group 1 in stack projectile");
-		else
-			projectile->createFlippedGroup(0, 1);
+		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};
 
-		idToProjectile[stack->getCreature()->idNumber] = projectile;
+			canvas->drawLine(Point(x1, y1 + i), Point(x2, y2+i), beginColor, endColor);
+		}
 	}
-	else
+	else // draw in vertical axis
 	{
-		idToRay[stack->getCreature()->idNumber] = creature->animation.projectileRay;
+		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);
+		}
 	}
 }
 
-void CBattleProjectileController::fireStackProjectile(const CStack * stack)
+CBattleProjectileController::CBattleProjectileController(CBattleInterface * owner):
+	owner(owner)
+{}
+
+const CCreature * CBattleProjectileController::getShooter(const CStack * stack)
 {
-	for (auto it = projectiles.begin(); it!=projectiles.end(); ++it)
+	const CCreature * creature = stack->getCreature();
+
+	if(creature->idNumber == CreatureID::ARROW_TOWERS)
+		creature = owner->siegeController->getTurretCreature();
+
+	if(creature->animation.missleFrameAngles.empty())
 	{
-		if ( !it->shotDone && it->stackID == stack->ID)
-		{
-			it->shotDone = true;
-			return;
-		}
+		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;
 }
 
-void CBattleProjectileController::showProjectiles(SDL_Surface *to)
+bool CBattleProjectileController::stackUsesRayProjectile(const CStack * stack)
 {
-	assert(to);
+	return !getShooter(stack)->animation.projectileRay.empty();
+}
 
-	std::list< std::list<ProjectileInfo>::iterator > toBeDeleted;
-	for (auto it = projectiles.begin(); it!=projectiles.end(); ++it)
-	{
-		// Check if projectile is already visible (shooter animation did the shot)
-		if (!it->shotDone)
-			continue;
+bool CBattleProjectileController::stackUsesMissileProjectile(const CStack * stack)
+{
+	return !getShooter(stack)->animation.projectileImageName.empty();
+}
 
-		if (idToProjectile.count(it->creID))
-		{
-			size_t group = it->reverse ? 1 : 0;
-			auto image = idToProjectile[it->creID]->getImage(it->frameNum, group, true);
+void CBattleProjectileController::initStackProjectile(const CStack * stack)
+{
+	if (!stackUsesMissileProjectile(stack))
+		return;
 
-			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);
+	const CCreature * creature = getShooter(stack);
 
-				image->draw(to, &dst, nullptr);
-			}
-		}
-		if (idToRay.count(it->creID))
-		{
-			auto const & ray = idToRay[it->creID];
+	std::shared_ptr<CAnimation> projectile = std::make_shared<CAnimation>(creature->animation.projectileImageName);
+	projectile->preload();
 
-			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;
+	if(projectile->size(1) != 0)
+		logAnim->error("Expected empty group 1 in stack projectile");
+	else
+		projectile->createFlippedGroup(0, 1);
 
-				int x1 = it->x0;
-				int x2 = it->x;
+	projectilesCache[creature->animation.projectileImageName] = projectile;
+}
 
-				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};
+std::shared_ptr<CAnimation> CBattleProjectileController::getProjectileImage(const CStack * stack)
+{
+	const CCreature * creature = getShooter(stack);
+	std::string imageName = creature->animation.projectileImageName;
 
-					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;
+	if (!projectilesCache.count(imageName))
+		initStackProjectile(stack);
 
-				int y1 =  it->y0;
-				int y2 =  it->y;
+	return projectilesCache[imageName];
+}
 
-				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};
+//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;
+//		}
+//	}
+//}
 
-					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);
+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;
 
-				++(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;
-			}
-		}
+		projectile->show(canvas);
+
+		// 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();
-
-	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;
+	std::shared_ptr<ProjectileBase> projectile;
 
-	// 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;
+	if (!target)
+	{
+		auto catapultProjectile= new ProjectileCatapult();
+		projectile.reset(catapultProjectile);
 
-	spi.x = from.x;
-	spi.y = from.y;
+		catapultProjectile->animation = getProjectileImage(shooter);
+		catapultProjectile->wallDamageAmount = 0; //FIXME - receive from caller
+		catapultProjectile->frameNum = 0;
+		catapultProjectile->reverse = false;
+		catapultProjectile->step = 0;
+		catapultProjectile->steps = 0;
 
-	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;
+		double animSpeed = AnimationControls::getProjectileSpeed() / 10;
+		catapultProjectile->steps = std::round(std::abs((dest.x - from.x) / animSpeed));
 	}
 	else
 	{
-		// Catapult attack
-		spi.catapultInfo.reset(new CatapultProjectileInfo(Point((int)spi.x, (int)spi.y), dest));
+		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);
+		}
 
-		double animSpeed = AnimationControls::getProjectileSpeed() / 10;
-		spi.lastStep = static_cast<int>(std::abs((dest.x - spi.x) / animSpeed));
-		spi.dx = animSpeed;
-		spi.dy = 0;
+		if (stackUsesRayProjectile(shooter))
+		{
+			auto rayProjectile = new ProjectileRay();
+			projectile.reset(rayProjectile);
 
-		auto img = idToProjectile[spi.creID]->getImage(0);
+			rayProjectile->rayConfig = shooterInfo->animation.projectileRay;
+		}
+		else if (stackUsesMissileProjectile(shooter))
+		{
+			auto missileProjectile = new ProjectileMissile();
+			projectile.reset(missileProjectile);
 
-		// Add explosion anim
-		Point animPos(dest.x - 126 + img->width() / 2,
-					  dest.y - 105 + img->height() / 2);
+			auto & angles = shooterInfo->animation.missleFrameAngles;
 
-		//owner->addNewAnim( new CEffectAnimation(owner, catapultDamage ? "SGEXPL.DEF" : "CSGRCK.DEF", animPos.x, animPos.y));
-	}
-	double pi = std::atan(1)*4;
+			missileProjectile->animation = getProjectileImage(shooter);
+			missileProjectile->reverse  = owner->stacksController->facingRight(shooter);
 
-	//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));
+			// 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(fabs((double)dest.y - from.y), fabs((double)dest.x - from.x));
-		//if(shooter->getPosition() < dest)
-		//	projectileAngle = -projectileAngle;
+			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
-		size_t bestID = 0;
-		double bestDiff = fabs( angles[0] / 180 * pi - 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
+			int bestID = 0;
+			double bestDiff = fabs( angles[0] / 180 * M_PI - projectileAngle );
 
-		for (size_t i=1; i<maxFrame; i++)
-		{
-			double currentDiff = fabs( angles[i] / 180 * pi - projectileAngle );
-			if (currentDiff < bestDiff)
+			for (int i=1; i<maxFrame; i++)
 			{
-				bestID = i;
-				bestDiff = currentDiff;
+				double currentDiff = fabs( angles[i] / 180 * M_PI - projectileAngle );
+				if (currentDiff < bestDiff)
+				{
+					bestID = i;
+					bestDiff = currentDiff;
+				}
 			}
+			missileProjectile->frameNum = bestID;
 		}
 
-		spi.frameNum = static_cast<int>(bestID);
-	}
-	else if (idToRay.count(spi.creID))
-	{
-		// no-op
-	}
-	else
-	{
-		logGlobal->error("Unable to find valid projectile for shooter %d", spi.creID);
+		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);
 }

+ 49 - 25
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<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
+{
+	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
 {
-	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::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::map<int, std::shared_ptr<CAnimation>> idToProjectile;
+//	std::map<int, std::vector<CCreature::CreatureAnimation::RayColor>> idToRay;
 
-	std::list<ProjectileInfo> projectiles; //projectiles flying on battlefield
+	/// 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);
 };

+ 0 - 6
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)

+ 5 - 0
client/gui/CCanvas.cpp

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

+ 4 - 0
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<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);
 };

+ 10 - 16
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<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);

+ 6 - 0
client/gui/SDL_Extensions.h

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