Jelajahi Sumber

Merge pull request #1146 from IvanSavenko/ray_projectile

Implemented ray-like projectiles for shooters
Nordsoft91 2 tahun lalu
induk
melakukan
1893212abb

+ 40 - 23
client/battle/CBattleAnimations.cpp

@@ -785,22 +785,25 @@ bool CShootingAnimation::init()
 	if (projectileAngle > straightAngle)
 	{
 		//upper shot
-		spi.x = fromPos.x + 222 + ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier;
-		spi.y = fromPos.y + 265 + shooterInfo->animation.upperRightMissleOffsetY;
+		spi.x0 = fromPos.x + 222 + ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier;
+		spi.y0 = fromPos.y + 265 + shooterInfo->animation.upperRightMissleOffsetY;
 	}
 	else if (projectileAngle < -straightAngle)
 	{
 		//lower shot
-		spi.x = fromPos.x + 222 + ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier;
-		spi.y = fromPos.y + 265 + shooterInfo->animation.lowerRightMissleOffsetY;
+		spi.x0 = fromPos.x + 222 + ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier;
+		spi.y0 = fromPos.y + 265 + shooterInfo->animation.lowerRightMissleOffsetY;
 	}
 	else
 	{
 		//straight shot
-		spi.x = fromPos.x + 222 + ( -25 + shooterInfo->animation.rightMissleOffsetX ) * multiplier;
-		spi.y = fromPos.y + 265 + shooterInfo->animation.rightMissleOffsetY;
+		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
@@ -811,7 +814,9 @@ bool CShootingAnimation::init()
 	if (attackedStack)
 	{
 		double animSpeed = AnimationControls::getProjectileSpeed(); // flight speed of projectile
-		spi.lastStep = static_cast<int>(sqrt(static_cast<double>((destPos.x - spi.x) * (destPos.x - spi.x) + (destPos.y - spi.y) * (destPos.y - spi.y))) / animSpeed);
+		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;
@@ -837,30 +842,42 @@ bool CShootingAnimation::init()
 	}
 	double pi = boost::math::constants::pi<double>();
 
-	if (owner->idToProjectile.count(spi.creID) == 0) //in some cases (known one: hero grants shooter bonus to unit) the shooter stack's projectile may not be properly initialized
+	//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);
 
-	// 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));
+	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);
+		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 );
+		// 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)
+		for (size_t i=1; i<maxFrame; i++)
 		{
-			bestID = i;
-			bestDiff = currentDiff;
+			double currentDiff = fabs( angles[i] / 180 * pi - projectileAngle );
+			if (currentDiff < bestDiff)
+			{
+				bestID = i;
+				bestDiff = currentDiff;
+			}
 		}
-	}
 
-	spi.frameNum = static_cast<int>(bestID);
+		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);
+	}
 
 	// Set projectile animation start delay which is specified in frames
 	spi.animStartDelay = shooterInfo->animation.attackClimaxFrame;

+ 1 - 0
client/battle/CBattleAnimations.h

@@ -189,6 +189,7 @@ public:
 /// 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

+ 64 - 17
client/battle/CBattleInterface.cpp

@@ -1020,15 +1020,22 @@ void CBattleInterface::initStackProjectile(const CStack * stack)
 	else
 		creature = stack->getCreature();
 
-	std::shared_ptr<CAnimation> projectile = std::make_shared<CAnimation>(creature->animation.projectileImageName);
-	projectile->preload();
+	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);
+		if(projectile->size(1) != 0)
+			logAnim->error("Expected empty group 1 in stack projectile");
+		else
+			projectile->createFlippedGroup(0, 1);
 
-	idToProjectile[stack->getCreature()->idNumber] = projectile;
+		idToProjectile[stack->getCreature()->idNumber] = projectile;
+	}
+	else
+	{
+		idToRay[stack->getCreature()->idNumber] = creature->animation.projectileRay;
+	}
 }
 
 void CBattleInterface::stackRemoved(uint32_t stackID)
@@ -3206,23 +3213,63 @@ void CBattleInterface::showProjectiles(SDL_Surface *to)
 				continue; // wait...
 		}
 
-		size_t group = it->reverse ? 1 : 0;
-		auto image = idToProjectile[it->creID]->getImage(it->frameNum, group, true);
+		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);
 
-		if(image)
+				image->draw(to, &dst, nullptr);
+			}
+		}
+		if (idToRay.count(it->creID))
 		{
-			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);
+			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};
 
-			image->draw(to, &dst, nullptr);
+					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)
+		if (it->step > it->lastStep)
 		{
 			toBeDeleted.insert(toBeDeleted.end(), it);
 		}

+ 2 - 0
client/battle/CBattleInterface.h

@@ -17,6 +17,7 @@
 #include "CBattleAnimations.h"
 
 #include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation
+#include "../../lib/CCreatureHandler.h"
 #include "../../lib/battle/CBattleInfoCallback.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -148,6 +149,7 @@ private:
 	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;

+ 69 - 0
client/gui/SDL_Extensions.cpp

@@ -361,6 +361,75 @@ void CSDL_Ext::update(SDL_Surface * what)
 	if(0 !=SDL_UpdateTexture(screenTexture, nullptr, what->pixels, what->pitch))
 		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);
+
+		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 *p = CSDL_Ext::getPxPtr(sur, x, y);
+		ColorPutter<4, 0>::PutColor(p, r,g,b,a);
+	}
+}
+
+static void drawLineY(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color1, const SDL_Color & color2)
+{
+	for(int y = y1; y <= y2; y++)
+	{
+		float f = float(y - y1) / float(y2 - y1);
+		int x = 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 *p = CSDL_Ext::getPxPtr(sur, x, y);
+		ColorPutter<4, 0>::PutColor(p, r,g,b,a);
+	}
+}
+
+void CSDL_Ext::drawLine(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color1, const SDL_Color & color2)
+{
+	int width  = std::abs(x1-x2);
+	int height = std::abs(y1-y2);
+
+	if ( width == 0 && height == 0)
+	{
+		Uint8 *p = CSDL_Ext::getPxPtr(sur, x1, y1);
+		ColorPutter<4, 0>::PutColorAlpha(p, color1);
+		return;
+	}
+
+	if (width > height)
+	{
+		if ( x1 < x2)
+			drawLineX(sur, x1,y1,x2,y2, color1, color2);
+		else
+			drawLineX(sur, x2,y2,x1,y1, color2, color1);
+	}
+	else
+	{
+		if ( y1 < y2)
+			drawLineY(sur, x1,y1,x2,y2, color1, color2);
+		else
+			drawLineY(sur, x2,y2,x1,y1, color2, color1);
+	}
+}
+
 void CSDL_Ext::drawBorder(SDL_Surface * sur, int x, int y, int w, int h, const int3 &color)
 {
 	for(int i = 0; i < w; i++)

+ 1 - 0
client/gui/SDL_Extensions.h

@@ -236,6 +236,7 @@ namespace CSDL_Ext
 	SDL_Color makeColor(ui8 r, ui8 g, ui8 b, ui8 a);
 
 	void update(SDL_Surface * what = screen); //updates whole surface (default - main screen)
+	void drawLine(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color1, const SDL_Color & color2);
 	void drawBorder(SDL_Surface * sur, int x, int y, int w, int h, const int3 &color);
 	void drawBorder(SDL_Surface * sur, const SDL_Rect &r, const int3 &color);
 	void drawDashedBorder(SDL_Surface * sur, const Rect &r, const int3 &color);

+ 16 - 2
config/creatures/dungeon.json

@@ -135,7 +135,14 @@
 			"animation": "CBEHOL.DEF",
 			"missile" :
 			{
-				"projectile": "SMBALX.DEF"
+				"ray" :
+				[
+					{ "start" : [ 160, 160, 160, 255 ], "end" : [ 160, 160, 160,  64 ] },
+					{ "start" : [ 192, 192, 192, 255 ], "end" : [ 192, 192, 192, 128 ] },
+					{ "start" : [ 224, 224, 224, 255 ], "end" : [ 224, 224, 224, 255 ] },
+					{ "start" : [ 192, 192, 192, 255 ], "end" : [ 192, 192, 192, 128 ] },
+					{ "start" : [ 160, 160, 160, 255 ], "end" : [ 160, 160, 160,  64 ] }
+				]
 			}
 		},
 		"sound" :
@@ -158,7 +165,14 @@
 			"animation": "CEVEYE.DEF",
 			"missile" :
 			{
-				"projectile": "SMBALX.DEF"
+				"ray" :
+				[
+					{ "start" : [ 160, 160, 160, 255 ], "end" : [ 160, 160, 160,  64 ] },
+					{ "start" : [ 192, 192, 192, 255 ], "end" : [ 192, 192, 192, 128 ] },
+					{ "start" : [ 224, 224, 224, 255 ], "end" : [ 224, 224, 224, 255 ] },
+					{ "start" : [ 192, 192, 192, 255 ], "end" : [ 192, 192, 192, 128 ] },
+					{ "start" : [ 160, 160, 160, 255 ], "end" : [ 160, 160, 160,  64 ] }
+				]
 			}
 		},
 		"sound" :

+ 9 - 1
config/creatures/tower.json

@@ -208,7 +208,15 @@
 			"animation": "CAMAGE.DEF",
 			"missile" :
 			{
-				"projectile": "PMAGEX.DEF"
+				"attackClimaxFrame" : 8,
+				"ray" :
+				[
+					{ "start" : [ 160, 192,   0, 255 ], "end" : [ 160, 192,   0,  64 ] },
+					{ "start" : [ 128, 224, 128, 255 ], "end" : [ 128, 224, 128, 128 ] },
+					{ "start" : [  32, 176,  32, 255 ], "end" : [  32, 176,  32, 255 ] },
+					{ "start" : [ 128, 224, 128, 255 ], "end" : [ 128, 224, 128, 128 ] },
+					{ "start" : [ 160, 192,   0, 255 ], "end" : [ 160, 192,   0,  64 ] }
+				]
 			}
 		},
 		"sound" :

+ 32 - 1
config/schemas/creature.json

@@ -217,7 +217,7 @@
 				"missile": {
 					"type":"object",
 					"additionalProperties" : false,
-					"required" : [ "projectile", "frameAngles", "offset", "attackClimaxFrame" ],
+					"required" : [ "frameAngles", "offset", "attackClimaxFrame" ],
 					"description": "Missile description for archers",
 					"properties":{
 						"projectile": {
@@ -225,6 +225,37 @@
 							"description": "Path to projectile animation",
 							"format" : "defFile"
 						},
+						"ray": {
+							"type":"array",
+							"description": "Colors of ray projectile animation",
+							"minItems" : 1,
+							"items": {
+								"type":"object",
+								"required" : [ "start", "end" ],
+								"properties":{
+									"start":  {
+										"type":"array",
+										"minItems" : 4,
+										"maxItems" : 4,
+										"items": {
+											"minimum" : 0,
+											"maximum" : 255,
+											"type":"number"
+										}
+									 },
+									"end":  {
+										"type":"array",
+										"minItems" : 4,
+										"maxItems" : 4,
+										"items": {
+											"minimum" : 0,
+											"maximum" : 255,
+											"type":"number"
+										}
+									}
+								}
+							}
+						},
 						"frameAngles": {
 							"type":"array",
 							"description": "Angles of missile images, should go from 90 to -90",

+ 17 - 0
lib/CCreatureHandler.cpp

@@ -907,6 +907,23 @@ void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & c
 
 	creature->animation.projectileImageName = config["graphics"]["missile"]["projectile"].String();
 
+	for(const JsonNode & value : config["graphics"]["missile"]["ray"].Vector())
+	{
+		CCreature::CreatureAnimation::RayColor color;
+
+		color.r1 = value["start"].Vector()[0].Integer();
+		color.g1 = value["start"].Vector()[1].Integer();
+		color.b1 = value["start"].Vector()[2].Integer();
+		color.a1 = value["start"].Vector()[3].Integer();
+
+		color.r2 = value["end"].Vector()[0].Integer();
+		color.g2 = value["end"].Vector()[1].Integer();
+		color.b2 = value["end"].Vector()[2].Integer();
+		color.a2 = value["end"].Vector()[3].Integer();
+
+		creature->animation.projectileRay.push_back(color);
+	}
+
 	creature->special = config["special"].Bool() || config["disabled"].Bool();
 
 	const JsonNode & sounds = config["sound"];

+ 12 - 0
lib/CCreatureHandler.h

@@ -63,6 +63,16 @@ public:
 
 	struct CreatureAnimation
 	{
+		struct RayColor {
+			uint8_t r1, g1, b1, a1;
+			uint8_t r2, g2, b2, a2;
+
+			template <typename Handler> void serialize(Handler &h, const int version)
+			{
+				h & r1 & g1 & b1 & a1 & r2 & g2 & b2 & a2;
+			}
+		};
+
 		double timeBetweenFidgets, idleAnimationTime,
 			   walkAnimationTime, attackAnimationTime, flightAnimationDistance;
 		int upperRightMissleOffsetX, rightMissleOffsetX, lowerRightMissleOffsetX,
@@ -72,6 +82,7 @@ public:
 		int troopCountLocationOffset, attackClimaxFrame;
 
 		std::string projectileImageName;
+		std::vector<RayColor> projectileRay;
 		//bool projectileSpin; //if true, appropriate projectile is spinning during flight
 
 		template <typename Handler> void serialize(Handler &h, const int version)
@@ -91,6 +102,7 @@ public:
 			h & troopCountLocationOffset;
 			h & attackClimaxFrame;
 			h & projectileImageName;
+			h & projectileRay;
 		}
 	} animation;