420 lines
17 KiB
C++
420 lines
17 KiB
C++
// debris_manager.cpp - Implementació del gestor de fragments
|
||
// © 2026 JailDesigner
|
||
|
||
#include "debris_manager.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <cstdlib>
|
||
#include <iostream>
|
||
|
||
#include "core/audio/audio.hpp"
|
||
#include "core/defaults.hpp"
|
||
#include "core/rendering/line_renderer.hpp"
|
||
|
||
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 {
|
||
// 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;
|
||
|
||
// 2. Aplicar scale al point centrat
|
||
float scaled_x = centered_x * scale;
|
||
float scaled_y = centered_y * scale;
|
||
|
||
// 3. Aplicar rotación
|
||
float cos_a = std::cos(angle);
|
||
float sin_a = std::sin(angle);
|
||
|
||
float rotated_x = (scaled_x * cos_a) - (scaled_y * sin_a);
|
||
float rotated_y = (scaled_x * sin_a) + (scaled_y * cos_a);
|
||
|
||
// 4. Aplicar traslación a posición mundial
|
||
return {.x = rotated_x + position.x, .y = rotated_y + position.y};
|
||
}
|
||
|
||
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,
|
||
const Vec2& centro,
|
||
float angle,
|
||
float scale,
|
||
float velocitat_base,
|
||
float brightness,
|
||
const Vec2& velocitat_objecte,
|
||
float velocitat_angular,
|
||
float factor_herencia_visual,
|
||
const std::string& sound,
|
||
SDL_Color color,
|
||
float lifetime,
|
||
float friction,
|
||
int segment_multiplier,
|
||
float shrink_rate) {
|
||
if (!shape || !shape->isValid()) {
|
||
return;
|
||
}
|
||
|
||
// Reproducir sonido de explosión
|
||
Audio::get()->playSound(sound, Audio::Group::GAME);
|
||
|
||
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
|
||
Vec2 world_p1 = transformPoint(local_p1, shape_centre, centro, angle, scale);
|
||
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, lifetime, friction, shrink_rate)) {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
auto DebrisManager::extractSegments(const Graphics::ShapePrimitive& primitive)
|
||
-> std::vector<std::pair<Vec2, Vec2>> {
|
||
std::vector<std::pair<Vec2, Vec2>> segments;
|
||
|
||
if (primitive.type == Graphics::PrimitiveType::POLYLINE) {
|
||
// Polyline: extreure segments consecutius
|
||
for (size_t i = 0; i + 1 < primitive.points.size(); i++) {
|
||
segments.emplace_back(primitive.points[i], primitive.points[i + 1]);
|
||
}
|
||
return segments;
|
||
}
|
||
// PrimitiveType::LINE: un únic segment (si té els 2 punts)
|
||
if (primitive.points.size() >= 2) {
|
||
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, float lifetime, float friction, float shrink_rate) -> bool {
|
||
Debris* debris = findFreeSlot();
|
||
if (debris == nullptr) {
|
||
std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n";
|
||
return false;
|
||
}
|
||
|
||
// Geometria autoritaritzada: centro + original_angle + original_half_length.
|
||
// p1/p2 es reconstrueixen cada frame en update() des d'aquestes dades.
|
||
const float DX = world_p2.x - world_p1.x;
|
||
const float DY = world_p2.y - world_p1.y;
|
||
debris->centro = {.x = (world_p1.x + world_p2.x) / 2.0F,
|
||
.y = (world_p1.y + world_p2.y) / 2.0F};
|
||
debris->original_angle = std::atan2(DY, DX);
|
||
debris->original_half_length = std::sqrt((DX * DX) + (DY * DY)) / 2.0F;
|
||
debris->p1 = world_p1;
|
||
debris->p2 = world_p2;
|
||
|
||
// Direcció radial (desde el centro hacia el segment)
|
||
Vec2 direccio = computeExplosionDirection(world_p1, world_p2, centro);
|
||
|
||
// Velocidad inicial (base ± variació aleatòria + velocity heretada de l'objecte)
|
||
float speed =
|
||
velocitat_base +
|
||
(((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) *
|
||
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 = friction;
|
||
|
||
// Rotación de trayectoria (con conversió a tangencial si excedeix cap)
|
||
applyAngularVelocity(*debris, direccio, velocitat_angular);
|
||
|
||
// Rotación visual (proporcional o aleatòria)
|
||
applyVisualRotation(*debris, velocitat_angular, factor_herencia_visual);
|
||
|
||
debris->angle_rotacio = 0.0F;
|
||
|
||
// Estat inicial: INTACTE durant `lifetime` segons (o fins que la
|
||
// velocity baixi de SHRINK_SPEED_THRESHOLD). Després MENGUANT a
|
||
// shrink_rate per segon fins arribar a size_factor=0 → mor.
|
||
debris->temps_vida = 0.0F;
|
||
debris->intact_time = lifetime;
|
||
debris->shrinking = false;
|
||
debris->size_factor = 1.0F;
|
||
debris->shrink_rate = shrink_rate;
|
||
|
||
// Visuals heretades
|
||
debris->brightness = brightness;
|
||
debris->color = color;
|
||
|
||
debris->active = true;
|
||
return true;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
// FASE 1: Aplicar herència i variació
|
||
float factor_herencia =
|
||
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN +
|
||
((std::rand() / static_cast<float>(RAND_MAX)) *
|
||
(Defaults::Physics::Debris::FACTOR_HERENCIA_MAX -
|
||
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN));
|
||
float velocitat_ang_heretada = velocitat_angular * factor_herencia;
|
||
float variacio = ((std::rand() / static_cast<float>(RAND_MAX)) * 0.2F) - 0.1F;
|
||
velocitat_ang_heretada *= (1.0F + variacio);
|
||
|
||
// FASE 2: Cap a la velocity màxima; l'excés es converteix en tangencial
|
||
constexpr float CAP = Defaults::Physics::Debris::VELOCITAT_ROT_MAX;
|
||
float abs_ang = std::abs(velocitat_ang_heretada);
|
||
float sign_ang = (velocitat_ang_heretada >= 0.0F) ? 1.0F : -1.0F;
|
||
|
||
if (abs_ang <= CAP) {
|
||
debris.velocitat_rot = velocitat_ang_heretada;
|
||
return;
|
||
}
|
||
|
||
// Excés: converteix l'excés de velocitat angular en velocitat tangencial lineal
|
||
float excess = abs_ang - CAP;
|
||
constexpr float RADIUS = 20.0F; // Radi típic de la shape (enemigos = 20 px)
|
||
float v_tangential = excess * RADIUS;
|
||
|
||
// Direcció tangencial: perpendicular a la radial (90° CCW): tangent = (-dy, dx)
|
||
debris.velocity.x += -direccio.y * v_tangential;
|
||
debris.velocity.y += direccio.x * v_tangential;
|
||
|
||
// 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) {
|
||
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;
|
||
float variacio_visual =
|
||
((std::rand() / static_cast<float>(RAND_MAX)) * 0.1F) - 0.05F;
|
||
debris.velocitat_rot_visual *= (1.0F + variacio_visual);
|
||
return;
|
||
}
|
||
|
||
// Rotación visual aleatòria (factor = 0.0 o sin velocidad angular)
|
||
debris.velocitat_rot_visual =
|
||
Defaults::Physics::Debris::ROTACIO_MIN +
|
||
((std::rand() / static_cast<float>(RAND_MAX)) *
|
||
(Defaults::Physics::Debris::ROTACIO_MAX -
|
||
Defaults::Physics::Debris::ROTACIO_MIN));
|
||
|
||
// 50% probabilitat de rotación en sentit contrari
|
||
if (std::rand() % 2 == 0) {
|
||
debris.velocitat_rot_visual = -debris.velocitat_rot_visual;
|
||
}
|
||
}
|
||
|
||
// INTACTE → MENGUANT → mort. Retorna true si el debris segueix viu.
|
||
static auto updateLifecycle(Debris& debris, float delta_time) -> bool {
|
||
debris.temps_vida += delta_time;
|
||
|
||
const float SPEED_SQ = (debris.velocity.x * debris.velocity.x) +
|
||
(debris.velocity.y * debris.velocity.y);
|
||
|
||
if (!debris.shrinking) {
|
||
const bool TIME_TRIGGER = debris.temps_vida >= debris.intact_time;
|
||
const bool SPEED_TRIGGER = SPEED_SQ < Defaults::Physics::Debris::SHRINK_SPEED_THRESHOLD_SQ;
|
||
if (TIME_TRIGGER || SPEED_TRIGGER) {
|
||
debris.shrinking = true;
|
||
}
|
||
}
|
||
|
||
if (debris.shrinking) {
|
||
debris.size_factor -= debris.shrink_rate * delta_time;
|
||
if (debris.size_factor <= 0.0F) {
|
||
debris.active = false;
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// Fricció lineal: redueix |velocity| en acceleration per segon.
|
||
static void applyLinearFriction(Debris& debris, float delta_time) {
|
||
const float SPEED = std::sqrt((debris.velocity.x * debris.velocity.x) +
|
||
(debris.velocity.y * debris.velocity.y));
|
||
if (SPEED > 1.0F) {
|
||
const float DIR_X = debris.velocity.x / SPEED;
|
||
const float DIR_Y = debris.velocity.y / SPEED;
|
||
const float NEW_SPEED = std::max(SPEED + (debris.acceleration * delta_time), 0.0F);
|
||
debris.velocity.x = DIR_X * NEW_SPEED;
|
||
debris.velocity.y = DIR_Y * NEW_SPEED;
|
||
} else {
|
||
debris.velocity.x = 0.0F;
|
||
debris.velocity.y = 0.0F;
|
||
}
|
||
}
|
||
|
||
// Aplica fricció a una velocity angular (genèric per a trajectòria i visual).
|
||
static void decayAngular(float& vel, float friction, float delta_time) {
|
||
if (std::abs(vel) <= 0.01F) {
|
||
vel = 0.0F;
|
||
return;
|
||
}
|
||
const float SIGN = (vel > 0) ? 1.0F : -1.0F;
|
||
vel -= SIGN * friction * delta_time;
|
||
if ((vel > 0) != (SIGN > 0)) {
|
||
vel = 0.0F;
|
||
}
|
||
}
|
||
|
||
// Rota el vector velocity (trajectòria corba) i aplica fricció a ambdues
|
||
// velocitats angulars (trajectòria i visual).
|
||
static void applyAngularDynamics(Debris& debris, float delta_time) {
|
||
// Rotar vector de velocity amb matriu 2D (només si encara gira la trajectòria)
|
||
if (std::abs(debris.velocitat_rot) > 0.01F) {
|
||
const float DANGLE = debris.velocitat_rot * delta_time;
|
||
const float VX = debris.velocity.x;
|
||
const float VY = debris.velocity.y;
|
||
const float COS_A = std::cos(DANGLE);
|
||
const float SIN_A = std::sin(DANGLE);
|
||
debris.velocity.x = (VX * COS_A) - (VY * SIN_A);
|
||
debris.velocity.y = (VX * SIN_A) + (VY * COS_A);
|
||
}
|
||
|
||
// Decay independent de les dues velocitats angulars.
|
||
decayAngular(debris.velocitat_rot,
|
||
Defaults::Physics::Debris::FRICCIO_ANGULAR,
|
||
delta_time);
|
||
decayAngular(debris.velocitat_rot_visual,
|
||
Defaults::Physics::Debris::FRICCIO_VISUAL,
|
||
delta_time);
|
||
}
|
||
|
||
void DebrisManager::update(float delta_time) {
|
||
for (auto& debris : debris_pool_) {
|
||
if (!debris.active) {
|
||
continue;
|
||
}
|
||
|
||
if (!updateLifecycle(debris, delta_time)) {
|
||
continue;
|
||
}
|
||
|
||
applyLinearFriction(debris, delta_time);
|
||
applyAngularDynamics(debris, delta_time);
|
||
|
||
// Posició del centre: integra velocity (separat de p1/p2).
|
||
debris.centro.x += debris.velocity.x * delta_time;
|
||
debris.centro.y += debris.velocity.y * delta_time;
|
||
|
||
// Acumular rotació visual. Modulada per size_factor: durant
|
||
// INTACTE (size=1) no afecta, però quan el segment mengua la
|
||
// rotació també s'apaga.
|
||
debris.angle_rotacio += debris.velocitat_rot_visual * debris.size_factor * delta_time;
|
||
|
||
// Reconstruir p1/p2 des de la geometria autoritaritzada:
|
||
// centro + (cos/sin(original_angle + angle_rotacio)) × original_half_length × size_factor
|
||
// No iteratiu — evita la rotació quadràtica i el shrink exponencial.
|
||
const float CURRENT_ANGLE = debris.original_angle + debris.angle_rotacio;
|
||
const float HALF_LEN = debris.original_half_length * std::max(0.0F, debris.size_factor);
|
||
const float COS_A = std::cos(CURRENT_ANGLE);
|
||
const float SIN_A = std::sin(CURRENT_ANGLE);
|
||
debris.p1.x = debris.centro.x - (HALF_LEN * COS_A);
|
||
debris.p1.y = debris.centro.y - (HALF_LEN * SIN_A);
|
||
debris.p2.x = debris.centro.x + (HALF_LEN * COS_A);
|
||
debris.p2.y = debris.centro.y + (HALF_LEN * SIN_A);
|
||
}
|
||
}
|
||
|
||
void DebrisManager::draw() const {
|
||
for (const auto& debris : debris_pool_) {
|
||
if (!debris.active) {
|
||
continue;
|
||
}
|
||
|
||
// Dibujar segmento con brightness y color heredados del padre.
|
||
Rendering::linea(renderer_,
|
||
static_cast<int>(debris.p1.x),
|
||
static_cast<int>(debris.p1.y),
|
||
static_cast<int>(debris.p2.x),
|
||
static_cast<int>(debris.p2.y),
|
||
debris.brightness,
|
||
0.0F,
|
||
debris.color);
|
||
}
|
||
}
|
||
|
||
auto DebrisManager::findFreeSlot() -> Debris* {
|
||
for (auto& debris : debris_pool_) {
|
||
if (!debris.active) {
|
||
return &debris;
|
||
}
|
||
}
|
||
return nullptr; // Pool ple
|
||
}
|
||
|
||
auto DebrisManager::computeExplosionDirection(const Vec2& p1,
|
||
const Vec2& p2,
|
||
const Vec2& centre_objecte) -> Vec2 {
|
||
// 1. Calcular centro del segment
|
||
float centro_seg_x = (p1.x + p2.x) / 2.0F;
|
||
float centro_seg_y = (p1.y + p2.y) / 2.0F;
|
||
|
||
// 2. Calcular vector des del centro de l'objecte hacia el centro del segment
|
||
// Això garanteix que la direcció siempre apunte hacia fuera (direcció radial)
|
||
float dx = centro_seg_x - centre_objecte.x;
|
||
float dy = centro_seg_y - centre_objecte.y;
|
||
|
||
// 3. Normalitzar (obtenir vector unitari)
|
||
float length = std::sqrt((dx * dx) + (dy * dy));
|
||
if (length < 0.001F) {
|
||
// Segment al centro (cas extrem mucho improbable), retornar direcció aleatòria
|
||
float angle_rand =
|
||
(std::rand() / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI;
|
||
return {.x = std::cos(angle_rand), .y = std::sin(angle_rand)};
|
||
}
|
||
|
||
dx /= length;
|
||
dy /= length;
|
||
|
||
// 4. Añadir variació aleatòria pequeña (±15°) per varietat visual
|
||
float angle_variacio =
|
||
((std::rand() % 30) - 15) * Defaults::Math::PI / 180.0F;
|
||
|
||
float cos_v = std::cos(angle_variacio);
|
||
float sin_v = std::sin(angle_variacio);
|
||
|
||
float final_x = (dx * cos_v) - (dy * sin_v);
|
||
float final_y = (dx * sin_v) + (dy * cos_v);
|
||
|
||
return {.x = final_x, .y = final_y};
|
||
}
|
||
|
||
void DebrisManager::reset() {
|
||
for (auto& debris : debris_pool_) {
|
||
debris.active = false;
|
||
}
|
||
}
|
||
|
||
auto DebrisManager::getActiveCount() const -> int {
|
||
int count = 0;
|
||
for (const auto& debris : debris_pool_) {
|
||
if (debris.active) {
|
||
count++;
|
||
}
|
||
}
|
||
return count;
|
||
}
|
||
|
||
} // namespace Effects
|