1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-11-28 08:48:48 +02:00

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
This commit is contained in:
Ivan Savenko 2022-11-15 21:42:16 +02:00
parent e4f3d2a685
commit 6678a747bb
11 changed files with 237 additions and 47 deletions

View File

@ -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));
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++)
if (owner->idToProjectile.count(spi.creID) != 0)
{
double currentDiff = fabs( angles[i] / 180 * pi - projectileAngle );
if (currentDiff < bestDiff)
{
bestID = i;
bestDiff = currentDiff;
}
}
// 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));
spi.frameNum = static_cast<int>(bestID);
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) != 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;

View File

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

View File

@ -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");
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
projectile->createFlippedGroup(0, 1);
idToProjectile[stack->getCreature()->idNumber] = projectile;
{
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(image)
if ( idToProjectile.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);
size_t group = it->reverse ? 1 : 0;
auto image = idToProjectile[it->creID]->getImage(it->frameNum, group, true);
image->draw(to, &dst, nullptr);
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) != 0)
{
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

View File

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

View File

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

View File

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

View File

@ -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" :

View File

@ -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" :

View File

@ -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",

View File

@ -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"];

View File

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