feat(debris): vida híbrida (mínima + umbral velocidad) + multiplier para enemigos

This commit is contained in:
2026-05-21 12:07:50 +02:00
parent 44aa4e76e2
commit efd18ff852
5 changed files with 388 additions and 357 deletions
+15 -3
View File
@@ -25,9 +25,15 @@ namespace Defaults::Physics {
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
constexpr float ROTACIO_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
constexpr float ROTACIO_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
constexpr float TEMPS_VIDA = 2.0F; // Duració màxima (segons) - enemy/bullet debris
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris lifetime (matches DEATH_DURATION)
constexpr float SHRINK_RATE = 0.5F; // Reducció de mida (factor/s)
constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION)
constexpr float SHRINK_RATE = 0.5F; // Reducció de mida (factor sobre min_lifetime)
// Política de mort: passat el min_lifetime, el fragment mor quan la
// seva velocity cau per sota d'aquest llindar. Així els fragments
// ràpids no "popen" en moviment.
constexpr float MIN_SPEED_TO_DIE = 5.0F; // px/s — al cuadrat per evitar sqrt en update
constexpr float MIN_SPEED_TO_DIE_SQ = MIN_SPEED_TO_DIE * MIN_SPEED_TO_DIE;
// Herència de velocity angular (trayectorias curvas)
constexpr float FACTOR_HERENCIA_MIN = 0.7F; // Mínimo 70% del drotacio heredat
@@ -41,6 +47,12 @@ namespace Defaults::Physics {
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
// Tuneig específic de l'explosió d'enemic (overrides als defaults
// que es passen com a paràmetres opcionals a explode()).
constexpr float ENEMY_LIFETIME = 3.0F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més
constexpr float ENEMY_FRICTION = -30.0F; // Fricció més suau perquè s'estenguin més
constexpr int ENEMY_SEGMENT_MULTIPLIER = 3; // Còpies de cada segment (5 cares × 3 = 15 trossos)
// Angular velocity sin for trajectory inheritance
// Excess above this threshold is converted to tangential linear velocity
// Prevents "vortex trap" problem with high-rotation enemies
+9 -6
View File
@@ -8,9 +8,9 @@
namespace Effects {
// Debris: un segment de línia que vola perpendicular a sí mismo
// Representa un fragment de una shape destruïda (ship, enemy, bullet)
struct Debris {
// Debris: un segment de línia que vola perpendicular a sí mismo
// Representa un fragment de una shape destruïda (ship, enemy, bullet)
struct Debris {
// Geometria del segment (2 points en coordenades mundials)
Vec2 p1; // Vec2 inicial del segment
Vec2 p2; // Vec2 final del segment
@@ -25,9 +25,12 @@ struct Debris {
float velocitat_rot_visual; // Velocidad de rotación VISUAL del segment (rad/s)
// Estat de vida
// Política: viu sempre durant min_lifetime, després mor quan
// |velocity| < MIN_SPEED_TO_DIE (definit en Defaults). Així els
// fragments ràpids no "popen" en moviment.
float temps_vida; // Temps transcorregut (segons)
float temps_max; // Temps de vida màxim (segons)
bool active; // Está active?
float min_lifetime; // Temps mínim garantit (segons)
bool active; // Està actiu?
// Shrinking (reducció de distancia entre points)
float factor_shrink; // Factor de reducció per segon (0.0-1.0)
@@ -35,6 +38,6 @@ struct Debris {
// Rendering
float brightness; // Factor de brightness (0.0-1.0, heretat de l'objecte original)
SDL_Color color{}; // Color heredado del padre. alpha==0 → usa global oscilador
};
};
} // namespace Effects
+59 -46
View File
@@ -14,9 +14,9 @@
namespace Effects {
// Helper: transformar point con rotación, scale i traslación
// (Copiat de shape_renderer.cpp:12-34)
static auto transformPoint(const Vec2& point, const Vec2& shape_centre, const Vec2& position, float angle, float scale) -> Vec2 {
// Helper: transformar point con rotación, scale i traslación
// (Copiat de shape_renderer.cpp:12-34)
static auto transformPoint(const Vec2& point, const Vec2& shape_centre, const Vec2& position, float angle, float scale) -> Vec2 {
// 1. Centrar el point respecte al centro de la shape
float centered_x = point.x - shape_centre.x;
float centered_y = point.y - shape_centre.y;
@@ -34,17 +34,17 @@ static auto transformPoint(const Vec2& point, const Vec2& shape_centre, const Ve
// 4. Aplicar traslación a posición mundial
return {.x = rotated_x + position.x, .y = rotated_y + position.y};
}
}
DebrisManager::DebrisManager(Rendering::Renderer* renderer)
DebrisManager::DebrisManager(Rendering::Renderer* renderer)
: renderer_(renderer) {
// Inicialitzar todos los debris como inactius
for (auto& debris : debris_pool_) {
debris.active = false;
}
}
}
void DebrisManager::explode(const std::shared_ptr<Graphics::Shape>& shape,
void DebrisManager::explode(const std::shared_ptr<Graphics::Shape>& shape,
const Vec2& centro,
float angle,
float scale,
@@ -54,7 +54,10 @@ void DebrisManager::explode(const std::shared_ptr<Graphics::Shape>& shape,
float velocitat_angular,
float factor_herencia_visual,
const std::string& sound,
SDL_Color color) {
SDL_Color color,
float lifetime,
float friction,
int segment_multiplier) {
if (!shape || !shape->isValid()) {
return;
}
@@ -64,6 +67,11 @@ void DebrisManager::explode(const std::shared_ptr<Graphics::Shape>& shape,
const Vec2& shape_centre = shape->getCenter();
// Multiplier: cada segment s'emet N vegades amb direccions aleatòries
// distintes (la variació ±15° de computeExplosionDirection ho garanteix).
const int COPIES = std::max(1, segment_multiplier);
for (int copy = 0; copy < COPIES; copy++) {
for (const auto& primitive : shape->getPrimitives()) {
for (const auto& [local_p1, local_p2] : extractSegments(primitive)) {
// Transformar points locals → coordenades mundials
@@ -71,16 +79,15 @@ void DebrisManager::explode(const std::shared_ptr<Graphics::Shape>& shape,
Vec2 world_p2 = transformPoint(local_p2, shape_centre, centro, angle, scale);
// Si el pool es ple, no té sentit continuar amb la resta de segments
if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness,
velocitat_objecte, velocitat_angular,
factor_herencia_visual, color)) {
if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness, velocitat_objecte, velocitat_angular, factor_herencia_visual, color, lifetime, friction)) {
return;
}
}
}
}
}
}
auto DebrisManager::extractSegments(const Graphics::ShapePrimitive& primitive)
auto DebrisManager::extractSegments(const Graphics::ShapePrimitive& primitive)
-> std::vector<std::pair<Vec2, Vec2>> {
std::vector<std::pair<Vec2, Vec2>> segments;
@@ -96,12 +103,9 @@ auto DebrisManager::extractSegments(const Graphics::ShapePrimitive& primitive)
segments.emplace_back(primitive.points[0], primitive.points[1]);
}
return segments;
}
}
auto DebrisManager::spawnDebris(const Vec2& world_p1, const Vec2& world_p2,
const Vec2& centro, float velocitat_base, float brightness,
const Vec2& velocitat_objecte, float velocitat_angular,
float factor_herencia_visual, SDL_Color color) -> bool {
auto DebrisManager::spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction) -> bool {
Debris* debris = findFreeSlot();
if (debris == nullptr) {
std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n";
@@ -122,7 +126,7 @@ auto DebrisManager::spawnDebris(const Vec2& world_p1, const Vec2& world_p2,
Defaults::Physics::Debris::VARIACIO_VELOCITAT);
debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x;
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y;
debris->acceleration = Defaults::Physics::Debris::ACCELERACIO;
debris->acceleration = friction;
// Rotación de trayectoria (con conversió a tangencial si excedeix cap)
applyAngularVelocity(*debris, direccio, velocitat_angular);
@@ -132,9 +136,10 @@ auto DebrisManager::spawnDebris(const Vec2& world_p1, const Vec2& world_p2,
debris->angle_rotacio = 0.0F;
// Vida i shrinking
// Vida i shrinking — min_lifetime és el temps mínim garantit; després
// el fragment mor quan |velocity| < MIN_SPEED_TO_DIE.
debris->temps_vida = 0.0F;
debris->temps_max = Defaults::Physics::Debris::TEMPS_VIDA;
debris->min_lifetime = lifetime;
debris->factor_shrink = Defaults::Physics::Debris::SHRINK_RATE;
// Visuals heretades
@@ -143,10 +148,9 @@ auto DebrisManager::spawnDebris(const Vec2& world_p1, const Vec2& world_p2,
debris->active = true;
return true;
}
}
void DebrisManager::applyAngularVelocity(Debris& debris, const Vec2& direccio,
float velocitat_angular) {
void DebrisManager::applyAngularVelocity(Debris& debris, const Vec2& direccio, float velocitat_angular) {
if (std::abs(velocitat_angular) <= 0.01F) {
debris.velocitat_rot = 0.0F; // Nave: sin curvas
return;
@@ -183,10 +187,9 @@ void DebrisManager::applyAngularVelocity(Debris& debris, const Vec2& direccio,
// Velocitat angular limitada al cap (preservant el signe)
debris.velocitat_rot = sign_ang * CAP;
}
}
void DebrisManager::applyVisualRotation(Debris& debris, float velocitat_angular,
float factor_herencia_visual) {
void DebrisManager::applyVisualRotation(Debris& debris, float velocitat_angular, float factor_herencia_visual) {
if (factor_herencia_visual > 0.01F && std::abs(velocitat_angular) > 0.01F) {
// Heredar rotación visual con factor proporcional + ±5% de variació
debris.velocitat_rot_visual = debris.velocitat_rot * factor_herencia_visual;
@@ -207,9 +210,9 @@ void DebrisManager::applyVisualRotation(Debris& debris, float velocitat_angular,
if (std::rand() % 2 == 0) {
debris.velocitat_rot_visual = -debris.velocitat_rot_visual;
}
}
}
void DebrisManager::update(float delta_time) {
void DebrisManager::update(float delta_time) {
for (auto& debris : debris_pool_) {
if (!debris.active) {
continue;
@@ -218,11 +221,17 @@ void DebrisManager::update(float delta_time) {
// 1. Actualitzar time de vida
debris.temps_vida += delta_time;
// Desactivar si ha superat time màxim
if (debris.temps_vida >= debris.temps_max) {
// Política de mort: viu sí o sí durant min_lifetime; després mor
// quan la velocity cau per sota d'un llindar. Així els fragments
// ràpids no desapareixen en moviment.
if (debris.temps_vida >= debris.min_lifetime) {
const float SPEED_SQ = (debris.velocity.x * debris.velocity.x) +
(debris.velocity.y * debris.velocity.y);
if (SPEED_SQ < Defaults::Physics::Debris::MIN_SPEED_TO_DIE_SQ) {
debris.active = false;
continue;
}
}
// 2. Actualitzar velocity (desacceleració)
// Aplicar fricció en la direcció del movement
@@ -286,9 +295,11 @@ void DebrisManager::update(float delta_time) {
// 5. Actualitzar rotación VISUAL
debris.angle_rotacio += debris.velocitat_rot_visual * delta_time;
// 6. Aplicar shrinking (reducció de distancia entre points)
float shrink_factor =
1.0F - (debris.factor_shrink * debris.temps_vida / debris.temps_max);
// 6. Aplicar shrinking (reducció de distancia entre points).
// El shrink es normalitza al min_lifetime (capat a 1.0) perquè els
// fragments que viuen més no es continuïn fent més petits per sempre.
const float SHRINK_T = std::min(debris.temps_vida / debris.min_lifetime, 1.0F);
float shrink_factor = 1.0F - (debris.factor_shrink * SHRINK_T);
shrink_factor = std::max(0.0F, shrink_factor); // No negatiu
// Calcular distancia original entre points
@@ -305,9 +316,9 @@ void DebrisManager::update(float delta_time) {
debris.p2.x = centro.x + (half_length * std::cos(new_angle));
debris.p2.y = centro.y + (half_length * std::sin(new_angle));
}
}
}
void DebrisManager::draw() const {
void DebrisManager::draw() const {
for (const auto& debris : debris_pool_) {
if (!debris.active) {
continue;
@@ -319,20 +330,22 @@ void DebrisManager::draw() const {
static_cast<int>(debris.p1.y),
static_cast<int>(debris.p2.x),
static_cast<int>(debris.p2.y),
debris.brightness, 0.0F, debris.color);
debris.brightness,
0.0F,
debris.color);
}
}
}
auto DebrisManager::findFreeSlot() -> Debris* {
auto DebrisManager::findFreeSlot() -> Debris* {
for (auto& debris : debris_pool_) {
if (!debris.active) {
return &debris;
}
}
return nullptr; // Pool ple
}
}
auto DebrisManager::computeExplosionDirection(const Vec2& p1,
auto DebrisManager::computeExplosionDirection(const Vec2& p1,
const Vec2& p2,
const Vec2& centre_objecte) -> Vec2 {
// 1. Calcular centro del segment
@@ -367,15 +380,15 @@ auto DebrisManager::computeExplosionDirection(const Vec2& p1,
float final_y = (dx * sin_v) + (dy * cos_v);
return {.x = final_x, .y = final_y};
}
}
void DebrisManager::reset() {
void DebrisManager::reset() {
for (auto& debris : debris_pool_) {
debris.active = false;
}
}
}
auto DebrisManager::getActiveCount() const -> int {
auto DebrisManager::getActiveCount() const -> int {
int count = 0;
for (const auto& debris : debris_pool_) {
if (debris.active) {
@@ -383,6 +396,6 @@ auto DebrisManager::getActiveCount() const -> int {
}
}
return count;
}
}
} // namespace Effects
+18 -18
View File
@@ -3,7 +3,6 @@
#pragma once
#include "core/rendering/render_context.hpp"
#include <SDL3/SDL.h>
#include <array>
@@ -13,14 +12,15 @@
#include "core/defaults.hpp"
#include "core/graphics/shape.hpp"
#include "core/rendering/render_context.hpp"
#include "core/types.hpp"
#include "debris.hpp"
namespace Effects {
// Gestor de fragments de explosions
// Manté un pool de objectes Debris i gestiona el seu cicle de vida
class DebrisManager {
// Gestor de fragments de explosions
// Manté un pool de objectes Debris i gestiona el seu cicle de vida
class DebrisManager {
public:
explicit DebrisManager(Rendering::Renderer* renderer);
@@ -34,6 +34,9 @@ class DebrisManager {
// - velocitat_objecte: velocity de l'objecte que explota (px/s, per defecte 0)
// - velocitat_angular: velocity angular heretada (rad/s, per defecte 0)
// - factor_herencia_visual: factor de herència rotación visual (0.0-1.0, per defecte 0.0)
// - lifetime: temps de vida del debris (s, per defecte TEMPS_VIDA = 2s)
// - friction: desacceleració del debris (px/s², per defecte ACCELERACIO = -60)
// - segment_multiplier: nombre de còpies per segment (per defecte 1 = sense duplicar)
void explode(const std::shared_ptr<Graphics::Shape>& shape,
const Vec2& centro,
float angle,
@@ -44,7 +47,10 @@ class DebrisManager {
float velocitat_angular = 0.0F,
float factor_herencia_visual = 0.0F,
const std::string& sound = Defaults::Sound::EXPLOSION,
SDL_Color color = {0, 0, 0, 0}); // alpha==0 → fragmentos usan oscilador global
SDL_Color color = {0, 0, 0, 0}, // alpha==0 → fragmentos usan oscilador global
float lifetime = Defaults::Physics::Debris::TEMPS_VIDA,
float friction = Defaults::Physics::Debris::ACCELERACIO,
int segment_multiplier = 1);
// Actualitzar todos los fragments active
void update(float delta_time);
@@ -62,10 +68,9 @@ class DebrisManager {
Rendering::Renderer* renderer_;
// Pool de fragments (màxim concurrent)
// Un pentágono té 5 línies, 15 enemigos = 75 línies
// + ship (3 línies) + balas (5 línies * 3) = 93 línies màxim
// Arrodonit a 100 per seguretat
static constexpr int MAX_DEBRIS = 150;
// Pentàgon 5 línies × 15 enemics × multiplier 3 = 225 trossos només d'enemics.
// + ship (3 línies) + balas (5 línies × 3) = ~243. Arrodonit a 300.
static constexpr int MAX_DEBRIS = 300;
std::array<Debris, MAX_DEBRIS> debris_pool_;
// Trobar primer slot inactiu
@@ -81,14 +86,9 @@ class DebrisManager {
-> std::vector<std::pair<Vec2, Vec2>>;
// Inicialitza un debris en un slot lliure i el deixa actiu. Retorna
// false si el pool está ple (la cridadora ha d'aturar el bucle).
auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2,
const Vec2& centro, float velocitat_base, float brightness,
const Vec2& velocitat_objecte, float velocitat_angular,
float factor_herencia_visual, SDL_Color color) -> bool;
static void applyAngularVelocity(Debris& debris, const Vec2& direccio,
float velocitat_angular);
static void applyVisualRotation(Debris& debris, float velocitat_angular,
float factor_herencia_visual);
};
auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction) -> bool;
static void applyAngularVelocity(Debris& debris, const Vec2& direccio, float velocitat_angular);
static void applyVisualRotation(Debris& debris, float velocitat_angular, float factor_herencia_visual);
};
} // namespace Effects
+4 -1
View File
@@ -73,7 +73,10 @@ namespace Systems::Collision {
DROTACIO,
0.0F, // sin herencia visual
Defaults::Sound::EXPLOSION,
COLOR);
COLOR,
Defaults::Physics::Debris::ENEMY_LIFETIME,
Defaults::Physics::Debris::ENEMY_FRICTION,
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER);
}
} // anonymous namespace