Merge branch 'feat/ship-trail': estela de partícules daurada/vermella darrere la nau
This commit is contained in:
@@ -25,6 +25,7 @@
|
|||||||
#include "core/defaults/rendering.hpp"
|
#include "core/defaults/rendering.hpp"
|
||||||
#include "core/defaults/ship.hpp"
|
#include "core/defaults/ship.hpp"
|
||||||
#include "core/defaults/title.hpp"
|
#include "core/defaults/title.hpp"
|
||||||
|
#include "core/defaults/trail.hpp"
|
||||||
#include "core/defaults/window.hpp"
|
#include "core/defaults/window.hpp"
|
||||||
#include "core/defaults/zones.hpp"
|
#include "core/defaults/zones.hpp"
|
||||||
// IWYU pragma: end_exports
|
// IWYU pragma: end_exports
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// trail.hpp - Configuració de l'estela de partícules de la nau
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Defaults::Trail {
|
||||||
|
|
||||||
|
constexpr int POOL_SIZE = 200;
|
||||||
|
|
||||||
|
constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de Physics::MAX_VELOCITY (180)
|
||||||
|
constexpr float EMIT_INTERVAL_S = 0.04F; // ~25 Hz nominal
|
||||||
|
constexpr float EMIT_JITTER_S = 0.015F; // ±15 ms al cooldown
|
||||||
|
constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement
|
||||||
|
constexpr float REAR_OFFSET_PX = 12.0F; // distància darrere center_ (cua)
|
||||||
|
|
||||||
|
constexpr float LIFETIME_BASE_S = 1.3F;
|
||||||
|
constexpr float LIFETIME_JITTER_S = 0.3F;
|
||||||
|
|
||||||
|
constexpr float SCALE_MIN = 0.7F; // × estrella starfield (3 px punta)
|
||||||
|
constexpr float SCALE_MAX = 1.2F;
|
||||||
|
|
||||||
|
constexpr float OSCILLATION_AMP_PX = 1.8F;
|
||||||
|
constexpr float OSCILLATION_FREQ_HZ = 6.0F;
|
||||||
|
|
||||||
|
constexpr float PULSE_FREQ_HZ = 2.5F;
|
||||||
|
|
||||||
|
// Colors del pulse (interpolats sinusoïdalment per partícula)
|
||||||
|
// P1: groc viu ↔ daurat clàssic
|
||||||
|
constexpr unsigned char COLOR_A_R = 255;
|
||||||
|
constexpr unsigned char COLOR_A_G = 255;
|
||||||
|
constexpr unsigned char COLOR_A_B = 0; // #FFFF00
|
||||||
|
constexpr unsigned char COLOR_B_R = 218;
|
||||||
|
constexpr unsigned char COLOR_B_G = 165;
|
||||||
|
constexpr unsigned char COLOR_B_B = 32; // #DAA520
|
||||||
|
|
||||||
|
// P2: roig viu ↔ rosa
|
||||||
|
constexpr unsigned char COLOR_P2_A_R = 255;
|
||||||
|
constexpr unsigned char COLOR_P2_A_G = 31;
|
||||||
|
constexpr unsigned char COLOR_P2_A_B = 31; // #FF1F1F
|
||||||
|
constexpr unsigned char COLOR_P2_B_R = 255;
|
||||||
|
constexpr unsigned char COLOR_P2_B_G = 105;
|
||||||
|
constexpr unsigned char COLOR_P2_B_B = 180; // #FF69B4
|
||||||
|
|
||||||
|
} // namespace Defaults::Trail
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
// 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 i = 0; i < ships.size(); i++) {
|
||||||
|
tryEmitFromShip(ships[i], static_cast<std::uint8_t>(i), delta_time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TrailManager::tryEmitFromShip(const Ship& ship, std::uint8_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, player_id);
|
||||||
|
|
||||||
|
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, std::uint8_t player_id) {
|
||||||
|
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.player_id = player_id;
|
||||||
|
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 bool IS_P2 = particle.player_id == 1;
|
||||||
|
const unsigned char A_R = IS_P2 ? Defaults::Trail::COLOR_P2_A_R : Defaults::Trail::COLOR_A_R;
|
||||||
|
const unsigned char A_G = IS_P2 ? Defaults::Trail::COLOR_P2_A_G : Defaults::Trail::COLOR_A_G;
|
||||||
|
const unsigned char A_B = IS_P2 ? Defaults::Trail::COLOR_P2_A_B : Defaults::Trail::COLOR_A_B;
|
||||||
|
const unsigned char B_R = IS_P2 ? Defaults::Trail::COLOR_P2_B_R : Defaults::Trail::COLOR_B_R;
|
||||||
|
const unsigned char B_G = IS_P2 ? Defaults::Trail::COLOR_P2_B_G : Defaults::Trail::COLOR_B_G;
|
||||||
|
const unsigned char B_B = IS_P2 ? Defaults::Trail::COLOR_P2_B_B : Defaults::Trail::COLOR_B_B;
|
||||||
|
|
||||||
|
const SDL_Color COLOR = {
|
||||||
|
.r = lerpU8(A_R, B_R, MIX),
|
||||||
|
.g = lerpU8(A_G, B_G, MIX),
|
||||||
|
.b = lerpU8(A_B, 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
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// trail_manager.hpp - Estela de partícules daurades darrere la nau
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "core/defaults/trail.hpp"
|
||||||
|
#include "core/graphics/shape.hpp"
|
||||||
|
#include "core/rendering/render_context.hpp"
|
||||||
|
#include "core/types.hpp"
|
||||||
|
#include "game/entities/ship.hpp"
|
||||||
|
|
||||||
|
namespace Effects {
|
||||||
|
|
||||||
|
// Manté un pool fixe de partícules amb forma d'estrella (star.shp) que
|
||||||
|
// pulsen entre groc i daurat, oscil·len al voltant del seu naixement i
|
||||||
|
// encongeixen fins a desaparèixer. Les naus actives emeten quan superen
|
||||||
|
// SPEED_THRESHOLD_PX_S.
|
||||||
|
class TrailManager {
|
||||||
|
public:
|
||||||
|
explicit TrailManager(Rendering::Renderer* renderer);
|
||||||
|
|
||||||
|
void update(float delta_time, const std::array<Ship, 2>& ships);
|
||||||
|
void draw() const;
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
[[nodiscard]] auto getActiveCount() const -> int;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Particle {
|
||||||
|
bool active{false};
|
||||||
|
std::uint8_t player_id{0}; // 0=P1, 1=P2 — selecciona paleta de colors
|
||||||
|
Vec2 origin{}; // punt de naixement (no es desplaça)
|
||||||
|
float phase_x{0.0F}; // fase oscil·lació horitzontal
|
||||||
|
float phase_y{0.0F};
|
||||||
|
float phase_pulse{0.0F};
|
||||||
|
float age{0.0F};
|
||||||
|
float lifetime{0.0F};
|
||||||
|
float scale{1.0F};
|
||||||
|
};
|
||||||
|
|
||||||
|
void tryEmitFromShip(const Ship& ship, std::uint8_t player_id, float delta_time);
|
||||||
|
void emitAt(Vec2 pos, std::uint8_t player_id);
|
||||||
|
auto findFreeSlot() -> int;
|
||||||
|
void drawParticle(const Particle& particle) const;
|
||||||
|
|
||||||
|
Rendering::Renderer* renderer_;
|
||||||
|
std::shared_ptr<Graphics::Shape> star_shape_;
|
||||||
|
std::array<Particle, Defaults::Trail::POOL_SIZE> pool_{};
|
||||||
|
std::array<float, 2> emit_cooldown_{0.0F, 0.0F};
|
||||||
|
float time_accumulator_{0.0F};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Effects
|
||||||
@@ -29,6 +29,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
|||||||
debris_manager_(sdl.getRenderer()),
|
debris_manager_(sdl.getRenderer()),
|
||||||
firework_manager_(sdl.getRenderer()),
|
firework_manager_(sdl.getRenderer()),
|
||||||
floating_score_manager_(sdl.getRenderer()),
|
floating_score_manager_(sdl.getRenderer()),
|
||||||
|
trail_manager_(sdl.getRenderer()),
|
||||||
text_(sdl.getRenderer()) {
|
text_(sdl.getRenderer()) {
|
||||||
// Recuperar configuración de match des del context
|
// Recuperar configuración de match des del context
|
||||||
match_config_ = context_.getMatchConfig();
|
match_config_ = context_.getMatchConfig();
|
||||||
@@ -179,6 +180,7 @@ void GameScene::stepPhysics(float delta_time) {
|
|||||||
for (auto& bullet : bullets_) {
|
for (auto& bullet : bullets_) {
|
||||||
bullet.postUpdate(delta_time);
|
bullet.postUpdate(delta_time);
|
||||||
}
|
}
|
||||||
|
trail_manager_.update(delta_time, ships_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::stepShootingInput() {
|
void GameScene::stepShootingInput() {
|
||||||
@@ -594,6 +596,7 @@ void GameScene::drawInitHudState() {
|
|||||||
|
|
||||||
void GameScene::drawLevelStartState() {
|
void GameScene::drawLevelStartState() {
|
||||||
drawMargins();
|
drawMargins();
|
||||||
|
trail_manager_.draw();
|
||||||
drawActiveShipsAlive();
|
drawActiveShipsAlive();
|
||||||
drawBullets();
|
drawBullets();
|
||||||
debris_manager_.draw();
|
debris_manager_.draw();
|
||||||
@@ -605,6 +608,7 @@ void GameScene::drawLevelStartState() {
|
|||||||
|
|
||||||
void GameScene::drawPlayingState() {
|
void GameScene::drawPlayingState() {
|
||||||
drawMargins();
|
drawMargins();
|
||||||
|
trail_manager_.draw();
|
||||||
drawActiveShipsAlive();
|
drawActiveShipsAlive();
|
||||||
drawEnemies();
|
drawEnemies();
|
||||||
drawBullets();
|
drawBullets();
|
||||||
@@ -616,6 +620,7 @@ void GameScene::drawPlayingState() {
|
|||||||
|
|
||||||
void GameScene::drawLevelCompletedState() {
|
void GameScene::drawLevelCompletedState() {
|
||||||
drawMargins();
|
drawMargins();
|
||||||
|
trail_manager_.draw();
|
||||||
drawActiveShipsAlive();
|
drawActiveShipsAlive();
|
||||||
drawBullets();
|
drawBullets();
|
||||||
debris_manager_.draw();
|
debris_manager_.draw();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include "game/effects/debris_manager.hpp"
|
#include "game/effects/debris_manager.hpp"
|
||||||
#include "game/effects/firework_manager.hpp"
|
#include "game/effects/firework_manager.hpp"
|
||||||
#include "game/effects/floating_score_manager.hpp"
|
#include "game/effects/floating_score_manager.hpp"
|
||||||
|
#include "game/effects/trail_manager.hpp"
|
||||||
#include "game/entities/bullet.hpp"
|
#include "game/entities/bullet.hpp"
|
||||||
#include "game/entities/enemy.hpp"
|
#include "game/entities/enemy.hpp"
|
||||||
#include "game/entities/ship.hpp"
|
#include "game/entities/ship.hpp"
|
||||||
@@ -56,6 +57,7 @@ class GameScene final : public Scene {
|
|||||||
Effects::DebrisManager debris_manager_;
|
Effects::DebrisManager debris_manager_;
|
||||||
Effects::FireworkManager firework_manager_;
|
Effects::FireworkManager firework_manager_;
|
||||||
Effects::FloatingScoreManager floating_score_manager_;
|
Effects::FloatingScoreManager floating_score_manager_;
|
||||||
|
Effects::TrailManager trail_manager_;
|
||||||
|
|
||||||
// Estat del juego
|
// Estat del juego
|
||||||
std::array<Ship, 2> ships_; // [0]=P1, [1]=P2
|
std::array<Ship, 2> ships_; // [0]=P1, [1]=P2
|
||||||
|
|||||||
Reference in New Issue
Block a user