瀏覽代碼

Battle Interface refactoring: separated projectile handling into a
separate class (untested)

Ivan Savenko 2 年之前
父節點
當前提交
bd1b2e4977

+ 6 - 0
client/CMakeLists.txt

@@ -5,6 +5,9 @@ set(client_SRCS
 		battle/CBattleAnimations.cpp
 		battle/CBattleInterfaceClasses.cpp
 		battle/CBattleInterface.cpp
+		battle/CBattleObstacleController.cpp
+		battle/CBattleProjectileController.cpp
+		battle/CBattleSiegeController.cpp
 		battle/CCreatureAnimation.cpp
 
 		gui/CAnimation.cpp
@@ -78,6 +81,9 @@ set(client_HEADERS
 		battle/CBattleAnimations.h
 		battle/CBattleInterfaceClasses.h
 		battle/CBattleInterface.h
+		battle/CBattleObstacleController.h
+		battle/CBattleProjectileController.h
+		battle/CBattleSiegeController.h
 		battle/CCreatureAnimation.h
 
 		gui/CAnimation.h

+ 28 - 126
client/battle/CBattleAnimations.cpp

@@ -14,6 +14,7 @@
 
 #include "CBattleInterfaceClasses.h"
 #include "CBattleInterface.h"
+#include "CBattleProjectileController.h"
 #include "CCreatureAnimation.h"
 
 #include "../CGameInfo.h"
@@ -196,15 +197,9 @@ bool CDefenceAnimation::init()
 	}
 	//unit reversed
 
-	if(rangedAttack && attacker != nullptr) //delay hit animation
+	if(rangedAttack && attacker != nullptr && owner->projectilesController->hasActiveProjectile(attacker)) //delay hit animation
 	{
-		for(std::list<ProjectileInfo>::const_iterator it = owner->projectiles.begin(); it != owner->projectiles.end(); ++it)
-		{
-			if(it->creID == attacker->getCreature()->idNumber)
-			{
-				return false;
-			}
-		}
+		return false;
 	}
 
 	// synchronize animation with attacker, unless defending or attacked by shooter:
@@ -725,14 +720,10 @@ bool CShootingAnimation::init()
 		return false;
 	}
 
-	// opponent must face attacker ( = different directions) before he can be attacked
 	//FIXME: this cause freeze
-
-//	if (attackingStack && attackedStack &&
-//	    owner->creDir[attackingStack->ID] == owner->creDir[attackedStack->ID])
-//		return false;
-
-	// Create the projectile animation
+	// opponent must face attacker ( = different directions) before he can be attacked
+	//if (attackingStack && attackedStack && owner->creDir[attackingStack->ID] == owner->creDir[attackedStack->ID])
+	//	return false;
 
 	//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;
@@ -744,40 +735,31 @@ bool CShootingAnimation::init()
 
 	if(shooterInfo->idNumber == CreatureID::ARROW_TOWERS)
 	{
-		int creID = owner->siegeH->town->town->clientInfo.siegeShooter;
-		shooterInfo = CGI->creh->operator[](creID);
+		CreatureID creID = owner->siegeH->town->town->clientInfo.siegeShooter;
+		shooterInfo = CGI->creh->objects[creID];
 	}
-	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;
-
-	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 = attackingStack ? !owner->creDir[attackingStack->ID] : shooter->getCreature()->idNumber != CreatureID::ARROW_TOWERS ;
-
-	spi.step = 0;
-	spi.frameNum = 0;
 
-	Point fromPos;
+	Point shooterPos;
+	Point shotPos;
 	Point destPos;
 
 	// NOTE: two lines below return different positions (very notable with 2-hex creatures). Obtaining via creanims seems to be more precise
-	fromPos = owner->creAnims[spi.stackID]->pos.topLeft();
+	shooterPos = owner->creAnims[shooter->ID]->pos.topLeft();
 	//xycoord = CClickableHex::getXYUnitAnim(shooter->position, true, shooter, owner);
 
-	destPos = CClickableHex::getXYUnitAnim(dest, attackedStack, owner);
+	destPos = CClickableHex::getXYUnitAnim(dest, attackedStack, owner) + Point(225, 225);
 
 	// to properly translate coordinates when shooter is rotated
-	int multiplier = spi.reverse ? -1 : 1;
+	int multiplier = 0;
+	if (shooter)
+		multiplier = owner->creDir[shooter->ID] ? 1 : -1;
+	else
+	{
+		assert(false); // unreachable?
+		multiplier = shooter->getCreature()->idNumber == CreatureID::ARROW_TOWERS ? -1 : 1;
+	}
 
-	double projectileAngle = atan2(fabs((double)destPos.y - fromPos.y), fabs((double)destPos.x - fromPos.x));
+	double projectileAngle = atan2(fabs((double)destPos.y - shooterPos.y), fabs((double)destPos.x - shooterPos.x));
 	if(shooter->getPosition() < dest)
 		projectileAngle = -projectileAngle;
 
@@ -785,103 +767,23 @@ bool CShootingAnimation::init()
 	if (projectileAngle > straightAngle)
 	{
 		//upper shot
-		spi.x0 = fromPos.x + 222 + ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier;
-		spi.y0 = fromPos.y + 265 + shooterInfo->animation.upperRightMissleOffsetY;
+		shotPos.x = shooterPos.x + 222 + ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier;
+		shotPos.y = shooterPos.y + 265 + shooterInfo->animation.upperRightMissleOffsetY;
 	}
 	else if (projectileAngle < -straightAngle)
 	{
 		//lower shot
-		spi.x0 = fromPos.x + 222 + ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier;
-		spi.y0 = fromPos.y + 265 + shooterInfo->animation.lowerRightMissleOffsetY;
+		shotPos.x = shooterPos.x + 222 + ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier;
+		shotPos.y = shooterPos.y + 265 + shooterInfo->animation.lowerRightMissleOffsetY;
 	}
 	else
 	{
 		//straight shot
-		spi.x0 = fromPos.x + 222 + ( -25 + shooterInfo->animation.rightMissleOffsetX ) * multiplier;
-		spi.y0 = fromPos.y + 265 + shooterInfo->animation.rightMissleOffsetY;
-	}
-
-	spi.x = spi.x0;
-	spi.y = spi.y0;
-
-	destPos += Point(225, 225);
-
-	// recalculate angle taking in account offsets
-	//projectileAngle = atan2(fabs(destPos.y - spi.y), fabs(destPos.x - spi.x));
-	//if(shooter->position < dest)
-	//	projectileAngle = -projectileAngle;
-
-	if (attackedStack)
-	{
-		double animSpeed = AnimationControls::getProjectileSpeed(); // flight speed of projectile
-		double distanceSquared = (destPos.x - spi.x) * (destPos.x - spi.x) + (destPos.y - spi.y) * (destPos.y - spi.y);
-		double distance = sqrt(distanceSquared);
-		spi.lastStep = std::round(distance / animSpeed);
-		if(spi.lastStep == 0)
-			spi.lastStep = 1;
-		spi.dx = (destPos.x - spi.x) / spi.lastStep;
-		spi.dy = (destPos.y - spi.y) / spi.lastStep;
-	}
-	else
-	{
-		// Catapult attack
-		spi.catapultInfo.reset(new CatapultProjectileInfo(Point((int)spi.x, (int)spi.y), destPos));
-
-		double animSpeed = AnimationControls::getProjectileSpeed() / 10;
-		spi.lastStep = static_cast<int>(std::abs((destPos.x - spi.x) / animSpeed));
-		spi.dx = animSpeed;
-		spi.dy = 0;
-
-		auto img = owner->idToProjectile[spi.creID]->getImage(0);
-
-		// Add explosion anim
-		Point animPos(destPos.x - 126 + img->width() / 2,
-					  destPos.y - 105 + img->height() / 2);
-
-		owner->addNewAnim( new CEffectAnimation(owner, catapultDamage ? "SGEXPL.DEF" : "CSGRCK.DEF", animPos.x, animPos.y));
-	}
-	double pi = boost::math::constants::pi<double>();
-
-	//in some cases (known one: hero grants shooter bonus to unit) the shooter stack's projectile may not be properly initialized
-	if (!owner->idToProjectile.count(spi.creID) && !owner->idToRay.count(spi.creID))
-		owner->initStackProjectile(shooter);
-
-	if (owner->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(), owner->idToProjectile.at(spi.creID)->size(0));
-
-		assert(maxFrame > 0);
-
-		// 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 (owner->idToRay.count(spi.creID))
-	{
-		// no-op
-	}
-	else
-	{
-		logGlobal->error("Unable to find valid projectile for shooter %d", spi.creID);
+		shotPos.x = shooterPos.x + 222 + ( -25 + shooterInfo->animation.rightMissleOffsetX ) * multiplier;
+		shotPos.y = shooterPos.y + 265 + shooterInfo->animation.rightMissleOffsetY;
 	}
 
-	// Set projectile animation start delay which is specified in frames
-	spi.animStartDelay = shooterInfo->animation.attackClimaxFrame;
-	owner->projectiles.push_back(spi);
+	owner->projectilesController->createProjectile(attackingStack, attackedStack, shotPos, destPos);
 
 	//attack animation
 

+ 0 - 17
client/battle/CBattleAnimations.h

@@ -186,23 +186,6 @@ public:
 	virtual ~CReverseAnimation(){};
 };
 
-/// Small struct which contains information about the position and the velocity of a projectile
-struct ProjectileInfo
-{
-	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
-	int animStartDelay; //frame of shooter animation when projectile should appear
-	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
-};
-
 class CRangedAttackAnimation : public CAttackAnimation
 {
 public:

+ 5 - 167
client/battle/CBattleInterface.cpp

@@ -13,6 +13,7 @@
 #include "CBattleAnimations.h"
 #include "CBattleInterfaceClasses.h"
 #include "CCreatureAnimation.h"
+#include "CBattleProjectileController.h"
 
 #include "../CBitmapHandler.h"
 #include "../CGameInfo.h"
@@ -113,6 +114,8 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
 {
 	OBJ_CONSTRUCTION;
 
+	projectilesController.reset(new CBattleProjectileController(this));
+
 	if(spectatorInt)
 	{
 		curInt = spectatorInt;
@@ -1008,33 +1011,7 @@ void CBattleInterface::unitAdded(const CStack * stack)
 	//loading projectiles for units
 	if(stack->isShooter())
 	{
-		initStackProjectile(stack);
-	}
-}
-
-void CBattleInterface::initStackProjectile(const CStack * stack)
-{
-	const CCreature * creature;//creature whose shots should be loaded
-	if(stack->getCreature()->idNumber == CreatureID::ARROW_TOWERS)
-		creature = CGI->creh->objects[siegeH->town->town->clientInfo.siegeShooter];
-	else
-		creature = stack->getCreature();
-
-	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;
+		projectilesController->initStackProjectile(stack);
 	}
 }
 
@@ -2990,35 +2967,6 @@ void CBattleInterface::SiegeHelper::printPartOfWall(SDL_Surface *to, int what)
 	}
 }
 
-CatapultProjectileInfo::CatapultProjectileInfo(Point from, Point dest)
-{
-	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];
-
-	facB = detB / det;
-	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);
-}
-
-double CatapultProjectileInfo::calculateY(double x)
-{
-	return facA *pow(x, 2.0) + facB *x + facC;
-}
-
 void CBattleInterface::showAll(SDL_Surface *to)
 {
 	show(to);
@@ -3036,7 +2984,7 @@ void CBattleInterface::show(SDL_Surface *to)
 
 	showBackground(to);
 	showBattlefieldObjects(to);
-	showProjectiles(to);
+	projectilesController->showProjectiles(to);
 
 	if(battleActionsStarted)
 		updateBattleAnimations();
@@ -3191,116 +3139,6 @@ void CBattleInterface::showHighlightedHex(SDL_Surface *to, BattleHex hex, bool d
 		CSDL_Ext::blit8bppAlphaTo24bpp(cellBorder, nullptr, to, &temp_rect); //redraw border to make it light green instead of shaded
 }
 
-void CBattleInterface::showProjectiles(SDL_Surface *to)
-{
-	assert(to);
-
-	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)
-		{
-			// frame we're waiting for is reached OR animation has already finished
-			if (creAnims[it->stackID]->getCurrentFrame() >= it->animStartDelay ||
-				creAnims[it->stackID]->isShooting() == false)
-			{
-				//at this point projectile should become visible
-				creAnims[it->stackID]->pause(); // pause animation
-				it->shotDone = true;
-			}
-			else
-				continue; // wait...
-		}
-
-		if (idToProjectile.count(it->creID))
-		{
-			size_t group = it->reverse ? 1 : 0;
-			auto image = idToProjectile[it->creID]->getImage(it->frameNum, group, true);
-
-			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;
-			}
-		}
-	}
-
-	for (auto & elem : toBeDeleted)
-	{
-		// resume animation
-		creAnims[elem->stackID]->play();
-		projectiles.erase(elem);
-	}
-}
-
 void CBattleInterface::showBattlefieldObjects(SDL_Surface *to)
 {
 	auto showHexEntry = [&](BattleObjectsByHex::HexData & hex)

+ 5 - 18
client/battle/CBattleInterface.h

@@ -47,7 +47,6 @@ class CCallback;
 class CButton;
 class CToggleButton;
 class CToggleGroup;
-struct CatapultProjectileInfo;
 class CBattleAnimation;
 class CBattleHero;
 class CBattleConsole;
@@ -55,11 +54,12 @@ class CBattleResultWindow;
 class CStackQueue;
 class CPlayerInterface;
 class CCreatureAnimation;
-struct ProjectileInfo;
 class CClickableHex;
 class CAnimation;
 class IImage;
 
+class CBattleProjectileController;
+
 /// Small struct which contains information about the id of the attacked stack, the damage dealt,...
 struct StackAttackedInfo
 {
@@ -104,16 +104,6 @@ struct BattleObjectsByHex
 	std::array<HexData, GameConstants::BFIELD_SIZE> hex;
 };
 
-/// Small struct which is needed for drawing the parabolic trajectory of the catapult cannon
-struct CatapultProjectileInfo
-{
-	CatapultProjectileInfo(Point from, Point dest);
-
-	double facA, facB, facC;
-
-	double calculateY(double x);
-};
-
 enum class MouseHoveredHexContext
 {
 	UNOCCUPIED_HEX,
@@ -148,9 +138,6 @@ private:
 	const CGHeroInstance *attackingHeroInstance, *defendingHeroInstance;
 	std::map<int32_t, std::shared_ptr<CCreatureAnimation>> creAnims; //animations of creatures from fighting armies (order by BattleInfo's stacks' ID)
 
-	std::map<int, std::shared_ptr<CAnimation>> idToProjectile;
-	std::map<int, std::vector<CCreature::CreatureAnimation::RayColor>> idToRay;
-
 	std::map<std::string, std::shared_ptr<CAnimation>> animationsCache;
 	std::map<si32, std::shared_ptr<CAnimation>> obstacleAnimations;
 
@@ -197,7 +184,6 @@ private:
 	//force active stack to cast a spell if possible
 	void enterCreatureCastingMode();
 
-	std::list<ProjectileInfo> projectiles; //projectiles flying on battlefield
 	void giveCommand(EActionType action, BattleHex tile = BattleHex(), si32 additional = -1);
 	void sendCommand(BattleAction *& command, const CStack * actor = nullptr);
 
@@ -269,7 +255,6 @@ private:
 	void showPiecesOfWall(SDL_Surface *to, std::vector<int> pieces);
 
 	void showBattleEffects(SDL_Surface *to, const std::vector<const BattleEffect *> &battleEffects);
-	void showProjectiles(SDL_Surface *to);
 
 	BattleObjectsByHex sortObjectsByHex();
 	void updateBattleAnimations();
@@ -283,6 +268,8 @@ private:
 
 	void setHeroAnimation(ui8 side, int phase);
 public:
+	std::unique_ptr<CBattleProjectileController> projectilesController;
+
 	static CondSh<bool> animsAreDisplayed; //for waiting with the end of battle for end of anims
 	static CondSh<BattleAction *> givenCommand; //data != nullptr if we have i.e. moved current unit
 
@@ -388,7 +375,6 @@ public:
 
 	void gateStateChanged(const EGateState state);
 
-	void initStackProjectile(const CStack * stack);
 
 	const CGHeroInstance *currentHero() const;
 	InfoAboutHero enemyHero() const;
@@ -410,4 +396,5 @@ public:
 	friend class CShootingAnimation;
 	friend class CCastAnimation;
 	friend class CClickableHex;
+	friend class CBattleProjectileController;
 };

+ 12 - 0
client/battle/CBattleObstacleController.cpp

@@ -0,0 +1,12 @@
+/*
+ * CBattleObstacleController.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 "CBattleObstacleController.h"
+

+ 15 - 0
client/battle/CBattleObstacleController.h

@@ -0,0 +1,15 @@
+/*
+ * CBattleObstacleController.h, 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
+ *
+ */
+#pragma once
+
+class CBattleObstacleController
+{
+
+};

+ 320 - 0
client/battle/CBattleProjectileController.cpp

@@ -0,0 +1,320 @@
+/*
+ * CBattleProjectileController.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 "CBattleProjectileController.h"
+#include "../gui/Geometries.h"
+#include "../../lib/CStack.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../CGameInfo.h"
+#include "../gui/CAnimation.h"
+#include "CBattleInterface.h"
+#include "CCreatureAnimation.h"
+
+CatapultProjectileInfo::CatapultProjectileInfo(const Point &from, const Point &dest)
+{
+	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];
+
+	facB = detB / det;
+	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);
+}
+
+double CatapultProjectileInfo::calculateY(double x)
+{
+	return facA *pow(x, 2.0) + facB *x + facC;
+}
+
+CBattleProjectileController::CBattleProjectileController(CBattleInterface * owner):
+	owner(owner)
+{
+
+}
+
+void CBattleProjectileController::initStackProjectile(const CStack * stack)
+{
+	const CCreature * creature;//creature whose shots should be loaded
+	if(stack->getCreature()->idNumber == CreatureID::ARROW_TOWERS)
+		creature = CGI->creh->objects[owner->siegeH->town->town->clientInfo.siegeShooter];
+	else
+		creature = stack->getCreature();
+
+	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;
+	}
+}
+
+
+void CBattleProjectileController::showProjectiles(SDL_Surface *to)
+{
+	assert(to);
+
+	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)
+		{
+			// frame we're waiting for is reached OR animation has already finished
+			if (owner->creAnims[it->stackID]->getCurrentFrame() >= it->animStartDelay ||
+				owner->creAnims[it->stackID]->isShooting() == false)
+			{
+				//at this point projectile should become visible
+				owner->creAnims[it->stackID]->pause(); // pause animation
+				it->shotDone = true;
+			}
+			else
+				continue; // wait...
+		}
+
+		if (idToProjectile.count(it->creID))
+		{
+			size_t group = it->reverse ? 1 : 0;
+			auto image = idToProjectile[it->creID]->getImage(it->frameNum, group, true);
+
+			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;
+			}
+		}
+	}
+
+	for (auto & elem : toBeDeleted)
+	{
+		// resume animation
+		owner->creAnims[elem->stackID]->play();
+		projectiles.erase(elem);
+	}
+}
+
+bool CBattleProjectileController::hasActiveProjectile(const CStack * stack)
+{
+	for(auto const & instance : projectiles)
+	{
+		if(instance.creID == stack->getCreature()->idNumber)
+		{
+			return true;
+		}
+	}
+	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)
+	{
+		int creID = owner->siegeH->town->town->clientInfo.siegeShooter;
+		shooterInfo = CGI->creh->operator[](creID);
+	}
+	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->creDir[shooter->ID] : 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)
+	{
+		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));
+
+		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
+	}
+	else
+	{
+		logGlobal->error("Unable to find valid projectile for shooter %d", spi.creID);
+	}
+
+	// Set projectile animation start delay which is specified in frames
+	spi.animStartDelay = shooterInfo->animation.attackClimaxFrame;
+	projectiles.push_back(spi);
+}

+ 65 - 0
client/battle/CBattleProjectileController.h

@@ -0,0 +1,65 @@
+/*
+ * CBattleSiegeController.h, 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
+ *
+ */
+#pragma once
+
+#include "../../lib/CCreatureHandler.h"
+
+struct Point;
+struct SDL_Surface;
+class CAnimation;
+class CStack;
+class CBattleInterface;
+
+/// Small struct which is needed for drawing the parabolic trajectory of the catapult cannon
+struct CatapultProjectileInfo
+{
+	CatapultProjectileInfo(const Point &from, const Point &dest);
+
+	double facA, facB, facC;
+
+	double calculateY(double x);
+};
+
+/// Small struct which contains information about the position and the velocity of a projectile
+struct ProjectileInfo
+{
+	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
+	int animStartDelay; //frame of shooter animation when projectile should appear
+	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
+};
+
+class CBattleProjectileController
+{
+	CBattleInterface * owner;
+
+	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
+
+public:
+	CBattleProjectileController(CBattleInterface * owner);
+
+	void showProjectiles(SDL_Surface *to);
+	void initStackProjectile(const CStack * stack);
+
+	bool hasActiveProjectile(const CStack * stack);
+
+	void createProjectile(const CStack * shooter, const CStack * target, Point from, Point dest);
+};

+ 12 - 0
client/battle/CBattleSiegeController.cpp

@@ -0,0 +1,12 @@
+/*
+ * CBattleSiegeController.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 "CBattleSiegeController.h"
+

+ 17 - 0
client/battle/CBattleSiegeController.h

@@ -0,0 +1,17 @@
+/*
+ * CBattleObstacleController.h, 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
+ *
+ */
+#pragma once
+
+
+
+class CBattleSiegeController
+{
+
+};

+ 4 - 0
lib/VCMIDirs.cpp

@@ -701,6 +701,10 @@ namespace VCMIDirs
 			#ifdef VCMI_WINDOWS
 			std::locale::global(boost::locale::generator().generate("en_US.UTF-8"));
 			#endif
+
+			#ifdef VCMI_XDG
+			setenv("LC_ALL", "C", 1);
+			#endif
 			boost::filesystem::path::imbue(std::locale());
 
 			singleton.init();