Merge branch 'feat/fireworks': starburst d'explosió d'enemic
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
#include "core/defaults/audio.hpp"
|
||||
#include "core/defaults/brightness.hpp"
|
||||
#include "core/defaults/controls.hpp"
|
||||
#include "core/defaults/effects.hpp"
|
||||
#include "core/defaults/enemies.hpp"
|
||||
#include "core/defaults/entities.hpp"
|
||||
#include "core/defaults/floating_score.hpp"
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// effects.hpp - Constants per a efectes visuals (fireworks, etc.)
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace Defaults::FX::Firework {
|
||||
|
||||
// Color per defecte. La caller pot fer override (p.ex. heretar del pare),
|
||||
// però per defecte no l'heretem — feel més neutre/lluminós.
|
||||
constexpr SDL_Color DEFAULT_COLOR = {.r = 255, .g = 255, .b = 255, .a = 255};
|
||||
|
||||
// Velocitat inicial radial al spawn (px/s) i variació entre punts.
|
||||
constexpr float SPEED = 250.0F;
|
||||
constexpr float SPEED_VARIATION = 30.0F; // ±
|
||||
|
||||
// Quantitat de línies per burst (per defecte).
|
||||
constexpr int N_POINTS = 100;
|
||||
|
||||
// Distribució angular: jitter aleatori sobre el repartiment uniforme.
|
||||
constexpr float ANGULAR_JITTER_DEG = 12.0F;
|
||||
|
||||
// Fase 1 (creixement): la línia neix amb longitud 0 i creix fins a max.
|
||||
constexpr float GROW_DURATION = 0.08F; // s
|
||||
constexpr float MAX_LENGTH = 25.0F; // px
|
||||
|
||||
// Fricció lineal (px/s²). Negativa per frenar.
|
||||
constexpr float FRICTION = -180.0F;
|
||||
|
||||
// Llindar de mort: per sota d'aquesta longitud (px) o brillor, la
|
||||
// partícula es marca inactiva.
|
||||
constexpr float MIN_LENGTH = 0.5F;
|
||||
constexpr float MIN_BRIGHTNESS = 0.02F;
|
||||
|
||||
// Brillor inicial per defecte.
|
||||
constexpr float INITIAL_BRIGHTNESS = 1.0F;
|
||||
|
||||
// Restitució en rebot contra els límits del PLAYAREA (mateix patró que debris).
|
||||
constexpr float RESTITUTION_BOUNDS = 0.7F;
|
||||
|
||||
// Mida del pool. 8 punts × ~25 bursts simultanis.
|
||||
constexpr int POOL_SIZE = 2000;
|
||||
|
||||
} // namespace Defaults::FX::Firework
|
||||
@@ -0,0 +1,41 @@
|
||||
// firework.hpp - Partícula d'efecte starburst (una línia per partícula)
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "core/types.hpp"
|
||||
|
||||
namespace Effects {
|
||||
|
||||
// Una partícula de firework: una línia que neix d'un punt origen, viatja
|
||||
// radialment cap a fora i s'encongeix conforme perd velocitat.
|
||||
//
|
||||
// Geometria (es deriva al draw):
|
||||
// head: extrem davanter (es mou amb velocity).
|
||||
// tail = head − velocity_normalitzada × current_length.
|
||||
//
|
||||
// Cicle de vida:
|
||||
// Fase 1 (temps_vida < grow_duration): current_length creix linealment
|
||||
// de 0 a max_length. Brillor al màxim.
|
||||
// Fase 2: current_length = max_length × (speed/initial_speed) i brillor
|
||||
// amb la mateixa proporció. Mor quan length o brightness cauen sota
|
||||
// llindar.
|
||||
struct Firework {
|
||||
Vec2 head; // Punta davantera (posició actual)
|
||||
Vec2 velocity; // Velocidad en px/s
|
||||
float acceleration; // Fricció lineal (px/s², negativa)
|
||||
|
||||
float current_length; // Longitud actual del segment (px)
|
||||
float max_length; // Longitud màxima (final de la fase de creixement)
|
||||
float grow_duration; // Temps de creixement de 0 a max_length (s)
|
||||
|
||||
float temps_vida; // Acumulador (s)
|
||||
float initial_speed; // Speed inicial per a la proporció de fase 2
|
||||
|
||||
float brightness; // 0..1
|
||||
SDL_Color color{}; // alpha==0 → oscilador global
|
||||
bool active;
|
||||
};
|
||||
|
||||
} // namespace Effects
|
||||
@@ -0,0 +1,219 @@
|
||||
// firework_manager.cpp - Implementació del gestor de fireworks
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "firework_manager.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/rendering/line_renderer.hpp"
|
||||
|
||||
namespace Effects {
|
||||
|
||||
namespace {
|
||||
// Random float in [-1, 1].
|
||||
auto randSigned() -> float {
|
||||
return ((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F) - 1.0F;
|
||||
}
|
||||
|
||||
// Rebot del head contra els límits del PLAYAREA (mateix patró que
|
||||
// DebrisManager::bounceOffPlayArea).
|
||||
void bounceOffPlayArea(Vec2& head, Vec2& velocity) {
|
||||
const auto& bounds = Defaults::Zones::PLAYAREA;
|
||||
constexpr float REST = Defaults::FX::Firework::RESTITUTION_BOUNDS;
|
||||
const float MIN_X = bounds.x;
|
||||
const float MAX_X = bounds.x + bounds.w;
|
||||
const float MIN_Y = bounds.y;
|
||||
const float MAX_Y = bounds.y + bounds.h;
|
||||
if (head.x < MIN_X) {
|
||||
head.x = MIN_X;
|
||||
if (velocity.x < 0.0F) {
|
||||
velocity.x = -velocity.x * REST;
|
||||
}
|
||||
}
|
||||
if (head.x > MAX_X) {
|
||||
head.x = MAX_X;
|
||||
if (velocity.x > 0.0F) {
|
||||
velocity.x = -velocity.x * REST;
|
||||
}
|
||||
}
|
||||
if (head.y < MIN_Y) {
|
||||
head.y = MIN_Y;
|
||||
if (velocity.y < 0.0F) {
|
||||
velocity.y = -velocity.y * REST;
|
||||
}
|
||||
}
|
||||
if (head.y > MAX_Y) {
|
||||
head.y = MAX_Y;
|
||||
if (velocity.y > 0.0F) {
|
||||
velocity.y = -velocity.y * REST;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
FireworkManager::FireworkManager(Rendering::Renderer* renderer)
|
||||
: renderer_(renderer) {
|
||||
for (auto& fw : pool_) {
|
||||
fw.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void FireworkManager::spawn(const Vec2& origen,
|
||||
SDL_Color color,
|
||||
float initial_speed,
|
||||
int n_points,
|
||||
float initial_brightness) {
|
||||
if (n_points <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float ANGLE_STEP = 2.0F * Defaults::Math::PI / static_cast<float>(n_points);
|
||||
const float JITTER_RAD =
|
||||
Defaults::FX::Firework::ANGULAR_JITTER_DEG * Defaults::Math::PI / 180.0F;
|
||||
|
||||
for (int i = 0; i < n_points; i++) {
|
||||
Firework* fw = findFreeSlot();
|
||||
if (fw == nullptr) {
|
||||
return; // Pool ple
|
||||
}
|
||||
|
||||
const float BASE_ANGLE = ANGLE_STEP * static_cast<float>(i);
|
||||
const float JITTER = randSigned() * JITTER_RAD;
|
||||
const float ANGLE = BASE_ANGLE + JITTER;
|
||||
|
||||
const float SPEED =
|
||||
initial_speed + (randSigned() * Defaults::FX::Firework::SPEED_VARIATION);
|
||||
|
||||
fw->head = origen;
|
||||
fw->velocity = {.x = std::cos(ANGLE) * SPEED, .y = std::sin(ANGLE) * SPEED};
|
||||
fw->acceleration = Defaults::FX::Firework::FRICTION;
|
||||
|
||||
fw->current_length = 0.0F;
|
||||
fw->max_length = Defaults::FX::Firework::MAX_LENGTH;
|
||||
fw->grow_duration = Defaults::FX::Firework::GROW_DURATION;
|
||||
|
||||
fw->temps_vida = 0.0F;
|
||||
fw->initial_speed = SPEED;
|
||||
|
||||
fw->brightness = initial_brightness;
|
||||
fw->color = color;
|
||||
fw->active = true;
|
||||
}
|
||||
}
|
||||
|
||||
void FireworkManager::update(float delta_time) {
|
||||
for (auto& fw : pool_) {
|
||||
if (!fw.active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fw.temps_vida += delta_time;
|
||||
|
||||
// 1. Fricció lineal (aplicar en la direcció del movement).
|
||||
const float SPEED = std::sqrt(
|
||||
(fw.velocity.x * fw.velocity.x) + (fw.velocity.y * fw.velocity.y));
|
||||
|
||||
if (SPEED > 1.0F) {
|
||||
const float DIR_X = fw.velocity.x / SPEED;
|
||||
const float DIR_Y = fw.velocity.y / SPEED;
|
||||
float new_speed = SPEED + (fw.acceleration * delta_time);
|
||||
new_speed = std::max(new_speed, 0.0F);
|
||||
fw.velocity.x = DIR_X * new_speed;
|
||||
fw.velocity.y = DIR_Y * new_speed;
|
||||
} else {
|
||||
fw.velocity = {.x = 0.0F, .y = 0.0F};
|
||||
}
|
||||
|
||||
// 2. Avançar head.
|
||||
fw.head.x += fw.velocity.x * delta_time;
|
||||
fw.head.y += fw.velocity.y * delta_time;
|
||||
|
||||
// 3. Rebot contra PLAYAREA.
|
||||
bounceOffPlayArea(fw.head, fw.velocity);
|
||||
|
||||
// 4. Calcular longitud i brillor segons fase.
|
||||
if (fw.temps_vida < fw.grow_duration) {
|
||||
// Fase 1: creixement lineal de 0 a max_length.
|
||||
const float T = fw.temps_vida / fw.grow_duration;
|
||||
fw.current_length = fw.max_length * T;
|
||||
fw.brightness = Defaults::FX::Firework::INITIAL_BRIGHTNESS;
|
||||
} else {
|
||||
// Fase 2: longitud i brillor proporcionals a la velocity actual.
|
||||
const float CURRENT_SPEED = std::sqrt(
|
||||
(fw.velocity.x * fw.velocity.x) + (fw.velocity.y * fw.velocity.y));
|
||||
const float RATIO = (fw.initial_speed > 0.01F)
|
||||
? std::min(CURRENT_SPEED / fw.initial_speed, 1.0F)
|
||||
: 0.0F;
|
||||
fw.current_length = fw.max_length * RATIO;
|
||||
fw.brightness = Defaults::FX::Firework::INITIAL_BRIGHTNESS * RATIO;
|
||||
}
|
||||
|
||||
// 5. Morir si la longitud o el brillor cauen sota llindar.
|
||||
if (fw.current_length < Defaults::FX::Firework::MIN_LENGTH ||
|
||||
fw.brightness < Defaults::FX::Firework::MIN_BRIGHTNESS) {
|
||||
fw.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FireworkManager::draw() const {
|
||||
for (const auto& fw : pool_) {
|
||||
if (!fw.active) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// tail = head − velocity_normalitzada × current_length.
|
||||
const float SPEED = std::sqrt(
|
||||
(fw.velocity.x * fw.velocity.x) + (fw.velocity.y * fw.velocity.y));
|
||||
if (SPEED < 0.01F) {
|
||||
continue; // Sense direcció no podem orientar la línia.
|
||||
}
|
||||
|
||||
const float DIR_X = fw.velocity.x / SPEED;
|
||||
const float DIR_Y = fw.velocity.y / SPEED;
|
||||
|
||||
const Vec2 TAIL = {
|
||||
.x = fw.head.x - (DIR_X * fw.current_length),
|
||||
.y = fw.head.y - (DIR_Y * fw.current_length),
|
||||
};
|
||||
|
||||
Rendering::linea(renderer_,
|
||||
static_cast<int>(fw.head.x),
|
||||
static_cast<int>(fw.head.y),
|
||||
static_cast<int>(TAIL.x),
|
||||
static_cast<int>(TAIL.y),
|
||||
fw.brightness,
|
||||
0.0F,
|
||||
fw.color);
|
||||
}
|
||||
}
|
||||
|
||||
void FireworkManager::reset() {
|
||||
for (auto& fw : pool_) {
|
||||
fw.active = false;
|
||||
}
|
||||
}
|
||||
|
||||
auto FireworkManager::getActiveCount() const -> int {
|
||||
int count = 0;
|
||||
for (const auto& fw : pool_) {
|
||||
if (fw.active) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
auto FireworkManager::findFreeSlot() -> Firework* {
|
||||
for (auto& fw : pool_) {
|
||||
if (!fw.active) {
|
||||
return &fw;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace Effects
|
||||
@@ -0,0 +1,50 @@
|
||||
// firework_manager.hpp - Gestor de bursts radials (fireworks)
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/rendering/render_context.hpp"
|
||||
#include "core/types.hpp"
|
||||
#include "firework.hpp"
|
||||
|
||||
namespace Effects {
|
||||
|
||||
// Pool de partícules. spawn() emet un burst d'N línies radials des
|
||||
// d'`origen`. Cada partícula viu independent (update/draw/rebot).
|
||||
class FireworkManager {
|
||||
public:
|
||||
explicit FireworkManager(Rendering::Renderer* renderer);
|
||||
|
||||
// Emet un burst radial:
|
||||
// origen: punt central del burst.
|
||||
// color: color de les línies (heretat del pare).
|
||||
// initial_speed: velocitat radial inicial (px/s).
|
||||
// n_points: nombre de línies. Default Defaults::FX::Firework::N_POINTS.
|
||||
// initial_brightness: 0..1.
|
||||
void spawn(const Vec2& origen,
|
||||
SDL_Color color = Defaults::FX::Firework::DEFAULT_COLOR,
|
||||
float initial_speed = Defaults::FX::Firework::SPEED,
|
||||
int n_points = Defaults::FX::Firework::N_POINTS,
|
||||
float initial_brightness = Defaults::FX::Firework::INITIAL_BRIGHTNESS);
|
||||
|
||||
void update(float delta_time);
|
||||
void draw() const;
|
||||
void reset();
|
||||
|
||||
[[nodiscard]] auto getActiveCount() const -> int;
|
||||
|
||||
private:
|
||||
Rendering::Renderer* renderer_;
|
||||
|
||||
static constexpr int POOL_SIZE = Defaults::FX::Firework::POOL_SIZE;
|
||||
std::array<Firework, POOL_SIZE> pool_;
|
||||
|
||||
auto findFreeSlot() -> Firework*;
|
||||
};
|
||||
|
||||
} // namespace Effects
|
||||
@@ -27,6 +27,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
||||
: sdl_(sdl),
|
||||
context_(context),
|
||||
debris_manager_(sdl.getRenderer()),
|
||||
firework_manager_(sdl.getRenderer()),
|
||||
floating_score_manager_(sdl.getRenderer()),
|
||||
text_(sdl.getRenderer()) {
|
||||
// Recuperar configuración de match des del context
|
||||
@@ -253,6 +254,7 @@ auto GameScene::stepContinueScreen(float delta_time) -> bool {
|
||||
bullet.update(delta_time);
|
||||
}
|
||||
debris_manager_.update(delta_time);
|
||||
firework_manager_.update(delta_time);
|
||||
floating_score_manager_.update(delta_time);
|
||||
return true;
|
||||
}
|
||||
@@ -277,6 +279,7 @@ auto GameScene::stepGameOver(float delta_time) -> bool {
|
||||
bullet.update(delta_time);
|
||||
}
|
||||
debris_manager_.update(delta_time);
|
||||
firework_manager_.update(delta_time);
|
||||
floating_score_manager_.update(delta_time);
|
||||
return true;
|
||||
}
|
||||
@@ -324,6 +327,7 @@ void GameScene::stepDeathSequence(float delta_time) {
|
||||
bullet.update(delta_time);
|
||||
}
|
||||
debris_manager_.update(delta_time);
|
||||
firework_manager_.update(delta_time);
|
||||
floating_score_manager_.update(delta_time);
|
||||
}
|
||||
// El bool 'algun_mort' es puramente interno: no aporta nada al caller
|
||||
@@ -392,6 +396,7 @@ void GameScene::runStageLevelStart(float delta_time) {
|
||||
bullet.update(delta_time);
|
||||
}
|
||||
debris_manager_.update(delta_time);
|
||||
firework_manager_.update(delta_time);
|
||||
}
|
||||
|
||||
void GameScene::runStagePlaying(float delta_time) {
|
||||
@@ -423,6 +428,7 @@ void GameScene::runStagePlaying(float delta_time) {
|
||||
|
||||
runCollisionDetections();
|
||||
debris_manager_.update(delta_time);
|
||||
firework_manager_.update(delta_time);
|
||||
floating_score_manager_.update(delta_time);
|
||||
}
|
||||
|
||||
@@ -439,6 +445,7 @@ void GameScene::runStageLevelCompleted(float delta_time) {
|
||||
bullet.update(delta_time);
|
||||
}
|
||||
debris_manager_.update(delta_time);
|
||||
firework_manager_.update(delta_time);
|
||||
floating_score_manager_.update(delta_time);
|
||||
}
|
||||
|
||||
@@ -451,6 +458,7 @@ void GameScene::runCollisionDetections() {
|
||||
.score_per_player = score_per_player_,
|
||||
.lives_per_player = lives_per_player_,
|
||||
.debris_manager = debris_manager_,
|
||||
.firework_manager = firework_manager_,
|
||||
.floating_score_manager = floating_score_manager_,
|
||||
.match_config = match_config_,
|
||||
.on_player_hit = [this](uint8_t pid) { tocado(pid); },
|
||||
@@ -511,6 +519,7 @@ void GameScene::drawContinueState() {
|
||||
drawEnemies();
|
||||
drawBullets();
|
||||
debris_manager_.draw();
|
||||
firework_manager_.draw();
|
||||
floating_score_manager_.draw();
|
||||
drawScoreboard();
|
||||
drawContinue();
|
||||
@@ -521,6 +530,7 @@ void GameScene::drawGameOverState() {
|
||||
drawEnemies();
|
||||
drawBullets();
|
||||
debris_manager_.draw();
|
||||
firework_manager_.draw();
|
||||
floating_score_manager_.draw();
|
||||
|
||||
const std::string GAME_OVER_TEXT = "GAME OVER";
|
||||
@@ -587,6 +597,7 @@ void GameScene::drawLevelStartState() {
|
||||
drawActiveShipsAlive();
|
||||
drawBullets();
|
||||
debris_manager_.draw();
|
||||
firework_manager_.draw();
|
||||
floating_score_manager_.draw();
|
||||
drawStageMessage(stage_manager_->getLevelStartMessage());
|
||||
drawScoreboard();
|
||||
@@ -598,6 +609,7 @@ void GameScene::drawPlayingState() {
|
||||
drawEnemies();
|
||||
drawBullets();
|
||||
debris_manager_.draw();
|
||||
firework_manager_.draw();
|
||||
floating_score_manager_.draw();
|
||||
drawScoreboard();
|
||||
}
|
||||
@@ -607,6 +619,7 @@ void GameScene::drawLevelCompletedState() {
|
||||
drawActiveShipsAlive();
|
||||
drawBullets();
|
||||
debris_manager_.draw();
|
||||
firework_manager_.draw();
|
||||
floating_score_manager_.draw();
|
||||
drawStageMessage(StageSystem::Constants::MISSATGE_LEVEL_COMPLETED);
|
||||
drawScoreboard();
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "core/types.hpp"
|
||||
#include "game/constants.hpp"
|
||||
#include "game/effects/debris_manager.hpp"
|
||||
#include "game/effects/firework_manager.hpp"
|
||||
#include "game/effects/floating_score_manager.hpp"
|
||||
#include "game/entities/bullet.hpp"
|
||||
#include "game/entities/enemy.hpp"
|
||||
@@ -53,6 +54,7 @@ class GameScene final : public Scene {
|
||||
|
||||
// Efectes visuals
|
||||
Effects::DebrisManager debris_manager_;
|
||||
Effects::FireworkManager firework_manager_;
|
||||
Effects::FloatingScoreManager floating_score_manager_;
|
||||
|
||||
// Estat del juego
|
||||
|
||||
@@ -76,6 +76,10 @@ namespace Systems::Collision {
|
||||
Defaults::Physics::Debris::ENEMY_LIFETIME,
|
||||
Defaults::Physics::Debris::ENEMY_FRICTION,
|
||||
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER);
|
||||
|
||||
// Firework burst radial des del centro de l'enemic (efecte adicional al debris).
|
||||
// No heretem color: el burst usa el blanc per defecte per a un feel més lluminós.
|
||||
ctx.firework_manager.spawn(ENEMY_POS);
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "core/defaults.hpp"
|
||||
#include "core/system/game_config.hpp"
|
||||
#include "game/effects/debris_manager.hpp"
|
||||
#include "game/effects/firework_manager.hpp"
|
||||
#include "game/effects/floating_score_manager.hpp"
|
||||
#include "game/entities/bullet.hpp"
|
||||
#include "game/entities/enemy.hpp"
|
||||
@@ -36,6 +37,7 @@ namespace Systems::Collision {
|
||||
std::array<int, 2>& score_per_player;
|
||||
std::array<int, 2>& lives_per_player;
|
||||
Effects::DebrisManager& debris_manager;
|
||||
Effects::FireworkManager& firework_manager;
|
||||
Effects::FloatingScoreManager& floating_score_manager;
|
||||
const GameConfig::MatchConfig& match_config;
|
||||
// Trigger de muerte del jugador (GameScene::tocado).
|
||||
|
||||
Reference in New Issue
Block a user