feat(trail): estela daurada de partícules quan la nau accelera
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
// trail_manager.cpp - Implementació de l'estela de partícules
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "trail_manager.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/graphics/shape_loader.hpp"
|
||||
#include "core/rendering/shape_renderer.hpp"
|
||||
|
||||
namespace Effects {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float TAU = 2.0F * Defaults::Math::PI;
|
||||
|
||||
auto randUniform(float min_v, float max_v) -> float {
|
||||
const float NORM = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||
return min_v + (NORM * (max_v - min_v));
|
||||
}
|
||||
|
||||
auto lerpU8(unsigned char a, unsigned char b, float t) -> unsigned char {
|
||||
const float MIX = std::clamp(t, 0.0F, 1.0F);
|
||||
const float OUT = static_cast<float>(a) + (MIX * (static_cast<float>(b) - static_cast<float>(a)));
|
||||
return static_cast<unsigned char>(std::round(OUT));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TrailManager::TrailManager(Rendering::Renderer* renderer)
|
||||
: renderer_(renderer),
|
||||
star_shape_(Graphics::ShapeLoader::load("star.shp")) {
|
||||
if (!star_shape_ || !star_shape_->isValid()) {
|
||||
std::cerr << "[TrailManager] Warning: no s'ha pogut load star.shp\n";
|
||||
}
|
||||
for (auto& particle : pool_) {
|
||||
particle.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void TrailManager::update(float delta_time, const std::array<Ship, 2>& ships) {
|
||||
time_accumulator_ += delta_time;
|
||||
|
||||
for (auto& particle : pool_) {
|
||||
if (!particle.active) {
|
||||
continue;
|
||||
}
|
||||
particle.age += delta_time;
|
||||
if (particle.age >= particle.lifetime) {
|
||||
particle.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t player_id = 0; player_id < ships.size(); player_id++) {
|
||||
tryEmitFromShip(ships[player_id], player_id, delta_time);
|
||||
}
|
||||
}
|
||||
|
||||
void TrailManager::tryEmitFromShip(const Ship& ship, std::size_t player_id, float delta_time) {
|
||||
if (!ship.isActive()) {
|
||||
emit_cooldown_[player_id] = 0.0F;
|
||||
return;
|
||||
}
|
||||
if (ship.getSpeed() < Defaults::Trail::SPEED_THRESHOLD_PX_S) {
|
||||
emit_cooldown_[player_id] = 0.0F;
|
||||
return;
|
||||
}
|
||||
|
||||
emit_cooldown_[player_id] -= delta_time;
|
||||
if (emit_cooldown_[player_id] > 0.0F) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cua = center - REAR_OFFSET * forward, on forward = (cos(angle-π/2), sin(angle-π/2))
|
||||
const float FORWARD_ANGLE = ship.getAngle() - (Defaults::Math::PI / 2.0F);
|
||||
const float COS_F = std::cos(FORWARD_ANGLE);
|
||||
const float SIN_F = std::sin(FORWARD_ANGLE);
|
||||
const Vec2 CENTER = ship.getCenter();
|
||||
|
||||
const float JITTER_X = randUniform(-Defaults::Trail::POSITION_JITTER_PX, Defaults::Trail::POSITION_JITTER_PX);
|
||||
const float JITTER_Y = randUniform(-Defaults::Trail::POSITION_JITTER_PX, Defaults::Trail::POSITION_JITTER_PX);
|
||||
|
||||
const Vec2 REAR = {
|
||||
.x = CENTER.x - (Defaults::Trail::REAR_OFFSET_PX * COS_F) + JITTER_X,
|
||||
.y = CENTER.y - (Defaults::Trail::REAR_OFFSET_PX * SIN_F) + JITTER_Y};
|
||||
|
||||
emitAt(REAR);
|
||||
|
||||
emit_cooldown_[player_id] = Defaults::Trail::EMIT_INTERVAL_S +
|
||||
randUniform(-Defaults::Trail::EMIT_JITTER_S, Defaults::Trail::EMIT_JITTER_S);
|
||||
}
|
||||
|
||||
void TrailManager::emitAt(Vec2 pos) {
|
||||
const int SLOT = findFreeSlot();
|
||||
if (SLOT < 0) {
|
||||
return; // pool ple — descart silenciós
|
||||
}
|
||||
|
||||
Particle& particle = pool_[static_cast<std::size_t>(SLOT)];
|
||||
particle.active = true;
|
||||
particle.origin = pos;
|
||||
particle.phase_x = randUniform(0.0F, TAU);
|
||||
particle.phase_y = randUniform(0.0F, TAU);
|
||||
particle.phase_pulse = randUniform(0.0F, TAU);
|
||||
particle.age = 0.0F;
|
||||
particle.lifetime = Defaults::Trail::LIFETIME_BASE_S +
|
||||
randUniform(-Defaults::Trail::LIFETIME_JITTER_S, Defaults::Trail::LIFETIME_JITTER_S);
|
||||
particle.scale = randUniform(Defaults::Trail::SCALE_MIN, Defaults::Trail::SCALE_MAX);
|
||||
}
|
||||
|
||||
auto TrailManager::findFreeSlot() -> int {
|
||||
for (std::size_t i = 0; i < pool_.size(); i++) {
|
||||
if (!pool_[i].active) {
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void TrailManager::draw() const {
|
||||
if (!star_shape_ || !star_shape_->isValid()) {
|
||||
return;
|
||||
}
|
||||
for (const auto& particle : pool_) {
|
||||
if (!particle.active) {
|
||||
continue;
|
||||
}
|
||||
drawParticle(particle);
|
||||
}
|
||||
}
|
||||
|
||||
void TrailManager::drawParticle(const Particle& particle) const {
|
||||
const float T_NORM = std::clamp(particle.age / particle.lifetime, 0.0F, 1.0F);
|
||||
const float FADE = 1.0F - T_NORM;
|
||||
|
||||
const float OSC_DX = std::sin((TAU * Defaults::Trail::OSCILLATION_FREQ_HZ * time_accumulator_) + particle.phase_x) *
|
||||
Defaults::Trail::OSCILLATION_AMP_PX * FADE;
|
||||
const float OSC_DY = std::sin((TAU * Defaults::Trail::OSCILLATION_FREQ_HZ * time_accumulator_) + particle.phase_y) *
|
||||
Defaults::Trail::OSCILLATION_AMP_PX * FADE;
|
||||
|
||||
const Vec2 POS = {.x = particle.origin.x + OSC_DX, .y = particle.origin.y + OSC_DY};
|
||||
|
||||
const float MIX = 0.5F + (0.5F * std::sin((TAU * Defaults::Trail::PULSE_FREQ_HZ * time_accumulator_) + particle.phase_pulse));
|
||||
|
||||
const SDL_Color COLOR = {
|
||||
.r = lerpU8(Defaults::Trail::COLOR_A_R, Defaults::Trail::COLOR_B_R, MIX),
|
||||
.g = lerpU8(Defaults::Trail::COLOR_A_G, Defaults::Trail::COLOR_B_G, MIX),
|
||||
.b = lerpU8(Defaults::Trail::COLOR_A_B, Defaults::Trail::COLOR_B_B, MIX),
|
||||
.a = 255};
|
||||
|
||||
const float CURRENT_SCALE = particle.scale * FADE;
|
||||
const float BRIGHTNESS = FADE;
|
||||
|
||||
Rendering::renderShape(
|
||||
renderer_,
|
||||
star_shape_,
|
||||
POS,
|
||||
0.0F, // sense rotació
|
||||
CURRENT_SCALE,
|
||||
1.0F, // progress (totalment visible)
|
||||
BRIGHTNESS,
|
||||
COLOR);
|
||||
}
|
||||
|
||||
void TrailManager::reset() {
|
||||
for (auto& particle : pool_) {
|
||||
particle.active = false;
|
||||
}
|
||||
emit_cooldown_[0] = 0.0F;
|
||||
emit_cooldown_[1] = 0.0F;
|
||||
time_accumulator_ = 0.0F;
|
||||
}
|
||||
|
||||
auto TrailManager::getActiveCount() const -> int {
|
||||
int count = 0;
|
||||
for (const auto& particle : pool_) {
|
||||
if (particle.active) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
} // namespace Effects
|
||||
Reference in New Issue
Block a user