瀏覽代碼

Implemented ray-like projectiles for shooters

- Added missing support for ray-like projectiles
- Archmages, Evil Eyes and Beholders now use ray for shooting
- New method to draw 1 pixel-wide line with color gradient at arbitrary
angle
- fixed incorrect attackClimaxFrame field for Archmages
Ivan Savenko 2 年之前
父節點
當前提交
6678a747bb

+ 37 - 22
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
@@ -837,30 +840,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) == 0 && owner->idToRay.count(spi.creID) == 0)
 		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) != 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(), 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) != 0)
+	{
+		// 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

+ 63 - 16
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,18 +3213,58 @@ 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) != 0)
+		{
+			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) != 0)
 		{
-			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

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

+ 68 - 0
client/gui/SDL_Extensions.cpp

@@ -361,6 +361,74 @@ 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());
 }
+
+uint8_t lerp(uint8_t a, uint8_t 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 = y1 + std::round((y2-y1)*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 = x1 + std::round((x2-x1)*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, color1, color2);
+	}
+	else
+	{
+		if ( y1 < y2)
+			drawLineY(sur, x1,y1,x2,y2, color1, color2);
+		else
+			drawLineY(sur, x2,y2,x1,y1, color1, color2);
+	}
+}
+
 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" :

+ 5 - 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,10 @@
 							"description": "Path to projectile animation",
 							"format" : "defFile"
 						},
+						"ray": {
+							"type":"array",
+							"description": "Colors of ray projectile animation"
+						},
 						"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"];

+ 13 - 0
lib/CCreatureHandler.h

@@ -11,6 +11,7 @@
 
 #include <vcmi/Creature.h>
 #include <vcmi/CreatureService.h>
+#include <int3.h>
 
 #include "HeroBonus.h"
 #include "ConstTransitivePtr.h"
@@ -63,6 +64,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 +83,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 +103,7 @@ public:
 			h & troopCountLocationOffset;
 			h & attackClimaxFrame;
 			h & projectileImageName;
+			h & projectileRay;
 		}
 	} animation;