commit
2007fb7edc
200 files changed
+27710
No files matched your search
+256
@@ -0,0 +1,256 @@
|
||||
#include "effects.h"
|
||||
#include "game.h"
|
||||
#include <algorithm>
|
||||
|
||||
constexpr int AFTERIMAGE_INTERVAL = 2;
|
||||
constexpr size_t EFFECTS_MAX = 1000;
|
||||
constexpr int NUMBER_LENGTH = 16;
|
||||
constexpr float NUMBER_MERGE_TIME = 0.25f;
|
||||
constexpr float AFTERIMAGE_ALPHA = 0.6f;
|
||||
constexpr float DPS_WINDOW = 3;
|
||||
constexpr float FLASH_CROSS = 0.5f;
|
||||
constexpr float FLASH_LENGTH = 16;
|
||||
constexpr float FLASH_LIFE = 0.05f;
|
||||
constexpr float FLASH_WIDTH = 6;
|
||||
constexpr float AFTERIMAGE_LIFE = 0.12f;
|
||||
constexpr float NUMBER_GAP = 8;
|
||||
constexpr float POPUP_FADE = 0.4f;
|
||||
constexpr float POPUP_JITTER_SCALE = 2;
|
||||
constexpr float POPUP_JITTER_START = 4;
|
||||
constexpr float POPUP_GRAVITY = 500;
|
||||
constexpr float POPUP_JUMP = 180;
|
||||
constexpr float POPUP_LIFE = 0.9f;
|
||||
constexpr float POPUP_SCALE = 2;
|
||||
constexpr float POPUP_SCALE_MAX = 5;
|
||||
constexpr float POPUP_SCALE_START = 2.5f;
|
||||
constexpr float POPUP_SCALE_STEP = 1.5f;
|
||||
constexpr float POPUP_SPREAD = 40;
|
||||
constexpr float PARTICLE_OUTLINE = 1;
|
||||
constexpr float PARTICLE_RANDOM_MIN = 0.4f;
|
||||
constexpr float SHAKE_DECAY = 2.5f;
|
||||
constexpr float SHAKE_LIMIT = 12;
|
||||
constexpr float SHAKE_MAX = 10;
|
||||
constexpr float SHAKE_TRAUMA_MAX = 3;
|
||||
|
||||
// Adds to one of the effect lists, dropping its oldest once it holds EFFECTS_MAX, so a busy fight can't flood the
|
||||
// screen (or the renderer) with leftovers.
|
||||
template <typename T> static void effect_add(std::vector<T>& list, const T& effect)
|
||||
{
|
||||
if (list.size() >= EFFECTS_MAX)
|
||||
list.erase(list.begin());
|
||||
list.push_back(effect);
|
||||
}
|
||||
|
||||
// Sprays particles at position, rotated by angle on top of the burst's own angle.
|
||||
void Effects::burst(Burst burst, Vec2 position, float angle, Color color)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
for (int i = 0; i < burst.count; i++)
|
||||
{
|
||||
float direction = angle + burst.angle + random_get(-burst.spread, burst.spread);
|
||||
float speed = burst.speed * random_get(PARTICLE_RANDOM_MIN, 1);
|
||||
float life = burst.life * random_get(PARTICLE_RANDOM_MIN, 1);
|
||||
effect_add(particles, Particle{color, position, vec2_polar(direction, speed), burst.fill, burst.gravity, life,
|
||||
life, burst.size});
|
||||
}
|
||||
}
|
||||
|
||||
// Callers may ask every tick. Copies are only kept every few ticks so trails stay spaced out.
|
||||
void Effects::afterimage_spawn(Pose pose)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
if (ticks % AFTERIMAGE_INTERVAL == 0)
|
||||
effect_add(afterimages, Afterimage{pose, AFTERIMAGE_LIFE});
|
||||
}
|
||||
|
||||
// Drawn before the player and attacks so trails sit behind what made them.
|
||||
void Effects::afterimage_draw(Game& game)
|
||||
{
|
||||
for (Afterimage& afterimage : afterimages)
|
||||
{
|
||||
Pose pose = afterimage.pose;
|
||||
pose.placement.color = color_fade(pose.placement.color, AFTERIMAGE_ALPHA * afterimage.life / AFTERIMAGE_LIFE);
|
||||
pose.draw(game.renderer);
|
||||
}
|
||||
}
|
||||
|
||||
// Damage numbers are popups whose whole-number scale and shake both grow with the logarithm of the damage.
|
||||
// Numbers take the attacker's colour and start just above the hit (bigger ones higher), so they don't cover it. A hit
|
||||
// on a target whose last number from the same attacker appeared moments ago folds into it, respawning it with the
|
||||
// total.
|
||||
void Effects::number_spawn(Vec2 position, int value, int target, Color color)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
for (int i = (int)popups.size() - 1; i >= 0; i--)
|
||||
{
|
||||
Popup& popup = popups[i];
|
||||
bool isSameColor = popup.color.r == color.r && popup.color.g == color.g && popup.color.b == color.b;
|
||||
if (popup.value <= 0 || popup.target != target || !isSameColor || POPUP_LIFE - popup.life > NUMBER_MERGE_TIME)
|
||||
continue;
|
||||
value += popup.value;
|
||||
popups.erase(popups.begin() + i);
|
||||
break;
|
||||
}
|
||||
|
||||
// Its bottom edge starts a small gap above the hit, however big the number is drawn.
|
||||
char text[NUMBER_LENGTH];
|
||||
SDL_snprintf(text, sizeof(text), "%d", value);
|
||||
float magnitude = math_log2((float)std::max(1, value));
|
||||
float scale = std::clamp(1 + std::floor((magnitude - POPUP_SCALE_START) / POPUP_SCALE_STEP), 1.0f, POPUP_SCALE_MAX);
|
||||
popup_spawn(position - Vec2{0, LINE_HEIGHT * scale / 2 + NUMBER_GAP}, text, color);
|
||||
popups.back().scale = scale;
|
||||
popups.back().jitter = std::max(0.0f, std::floor(magnitude - POPUP_JITTER_START)) * POPUP_JITTER_SCALE;
|
||||
popups.back().target = target;
|
||||
popups.back().value = value;
|
||||
}
|
||||
|
||||
void Effects::popup_spawn(Vec2 position, const char* text, Color color)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
Popup popup = {
|
||||
.color = color,
|
||||
.position = position,
|
||||
.velocity = {random_get(-POPUP_SPREAD, POPUP_SPREAD), -POPUP_JUMP},
|
||||
.jitter = 0,
|
||||
.life = POPUP_LIFE,
|
||||
.scale = POPUP_SCALE,
|
||||
};
|
||||
SDL_strlcpy(popup.text, text, sizeof(popup.text));
|
||||
effect_add(popups, popup);
|
||||
}
|
||||
|
||||
void Effects::flash_spawn(Vec2 position, float angle, float size, Color color)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
effect_add(flashes, Flash{color, position, angle, FLASH_LIFE, size});
|
||||
}
|
||||
|
||||
void Effects::dps_record(int amount)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
hits.push_back({ticks * TICK, amount});
|
||||
}
|
||||
|
||||
// Damage dealt over the last few seconds, averaged per second.
|
||||
float Effects::dps_get()
|
||||
{
|
||||
int total = 0;
|
||||
for (Hit& hit : hits)
|
||||
total += hit.amount;
|
||||
return total / DPS_WINDOW;
|
||||
}
|
||||
|
||||
void Effects::shake_add(float amount)
|
||||
{
|
||||
if (isMuted)
|
||||
return;
|
||||
shake = std::min(SHAKE_TRAUMA_MAX, shake + amount);
|
||||
}
|
||||
|
||||
// Shake grows with the square of its trauma so small hits stay subtle, up to a limit in pixels. Big trauma
|
||||
// still lasts longer at that limit.
|
||||
Vec2 Effects::shake_get()
|
||||
{
|
||||
float strength = std::min(SHAKE_LIMIT, shake * shake * SHAKE_MAX);
|
||||
return {random_get(-strength, strength), random_get(-strength, strength)};
|
||||
}
|
||||
|
||||
void Effects::update()
|
||||
{
|
||||
shake = std::max(0.0f, shake - SHAKE_DECAY * TICK);
|
||||
ticks++;
|
||||
std::erase_if(hits, [&](Hit& hit) { return ticks * TICK - hit.time > DPS_WINDOW; });
|
||||
|
||||
for (Flash& flash : flashes)
|
||||
flash.life -= TICK;
|
||||
std::erase_if(flashes, [](Flash& flash) { return flash.life <= 0; });
|
||||
|
||||
for (Afterimage& afterimage : afterimages)
|
||||
afterimage.life -= TICK;
|
||||
std::erase_if(afterimages, [](Afterimage& afterimage) { return afterimage.life <= 0; });
|
||||
|
||||
for (Particle& particle : particles)
|
||||
{
|
||||
particle.velocity.y += particle.gravity * TICK;
|
||||
particle.position = particle.position + particle.velocity * TICK;
|
||||
particle.life -= TICK;
|
||||
}
|
||||
std::erase_if(particles, [](Particle& particle) { return particle.life <= 0; });
|
||||
|
||||
for (Popup& popup : popups)
|
||||
{
|
||||
popup.velocity.y += POPUP_GRAVITY * TICK;
|
||||
popup.position = popup.position + popup.velocity * TICK;
|
||||
popup.life -= TICK;
|
||||
}
|
||||
std::erase_if(popups, [](Popup& popup) { return popup.life <= 0; });
|
||||
}
|
||||
|
||||
// Particles and popups have no stored history, so they step back along their velocity
|
||||
// by however much of the tick drawing is behind.
|
||||
void Effects::draw(Game& game)
|
||||
{
|
||||
Renderer& renderer = game.renderer;
|
||||
|
||||
float behind = (1 - game.alpha) * TICK;
|
||||
|
||||
// Each flash is a long streak along the shot crossed by a short one, shrinking as it fades.
|
||||
for (Flash& flash : flashes)
|
||||
{
|
||||
float fade = flash.life / FLASH_LIFE;
|
||||
float length = FLASH_LENGTH * flash.size * fade;
|
||||
float width = FLASH_WIDTH * flash.size * fade;
|
||||
Vec2 along = vec2_polar(flash.angle, length / 2);
|
||||
Vec2 across = vec2_polar(flash.angle + PI / 2, length * FLASH_CROSS / 2);
|
||||
renderer.line_draw(flash.position - along * 0.2f, flash.position + along * 1.8f, width, flash.color);
|
||||
renderer.line_draw(flash.position - across, flash.position + across, width * FLASH_CROSS, flash.color);
|
||||
}
|
||||
|
||||
// Each particle shrinks and fades out. The outline is four strips so the fading core never shows it through.
|
||||
for (Particle& particle : particles)
|
||||
{
|
||||
float fade = particle.life / particle.lifeMax;
|
||||
float size = std::ceil(particle.size * fade);
|
||||
float outer = size + PARTICLE_OUTLINE * 2;
|
||||
Vec2 position = particle.position - particle.velocity * behind;
|
||||
Vec2 corner = {std::round(position.x - size / 2), std::round(position.y - size / 2)};
|
||||
Vec2 edge = corner - Vec2{PARTICLE_OUTLINE, PARTICLE_OUTLINE};
|
||||
Color color = color_fade(particle.color, fade);
|
||||
renderer.rect_draw({edge.x, edge.y, outer, PARTICLE_OUTLINE}, color);
|
||||
renderer.rect_draw({edge.x, corner.y + size, outer, PARTICLE_OUTLINE}, color);
|
||||
renderer.rect_draw({edge.x, corner.y, PARTICLE_OUTLINE, size}, color);
|
||||
renderer.rect_draw({corner.x + size, corner.y, PARTICLE_OUTLINE, size}, color);
|
||||
renderer.rect_draw({corner.x, corner.y, size, size},
|
||||
color_fade(color_shade(particle.color, particle.fill), fade));
|
||||
}
|
||||
|
||||
for (Popup& popup : popups)
|
||||
{
|
||||
Vec2 size = game.text.size_get(popup.text, FONT_SMALL, popup.scale);
|
||||
float jitter = popup.jitter * popup.life / POPUP_LIFE;
|
||||
Vec2 shake = {random_get(-jitter, jitter), random_get(-jitter, jitter)};
|
||||
Vec2 position = popup.position - popup.velocity * behind + shake;
|
||||
Vec2 corner = {std::round(position.x - size.x / 2), std::round(position.y - size.y / 2)};
|
||||
float alpha = std::min(1.0f, popup.life / POPUP_FADE);
|
||||
game.text.draw(
|
||||
renderer, popup.text,
|
||||
{.color = color_fade(popup.color, alpha), .position = corner, .scale = {popup.scale, popup.scale}});
|
||||
}
|
||||
}
|
||||
|
||||
// Everything showing goes, as when this screen starts showing another game.
|
||||
void Effects::clear()
|
||||
{
|
||||
afterimages.clear();
|
||||
flashes.clear();
|
||||
hits.clear();
|
||||
particles.clear();
|
||||
popups.clear();
|
||||
shake = 0;
|
||||
}
|
||||
Reference in new issue
Block a user