Files

463 lines
19 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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,
const Vec2& bullet_impulse_velocity,
float piece_scale) {
if (!shape || !shape->isValid()) {
return;
}
// Reproducir sonido de explosión. Cadena buida = explosió silenciosa
// (p. ex. el logo dins el cicle d'atracció).
if (!sound.empty()) {
Audio::get()->playSound(sound, Audio::Group::GAME);
}
// Notificar als subscriptors (border, playfield, etc.).
if (explosion_callback_) {
explosion_callback_(centro);
}
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, bullet_impulse_velocity, piece_scale)) {
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_in, const Vec2& world_p2_in, 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, const Vec2& bullet_impulse_velocity, float piece_scale) -> bool {
Debris* debris = findFreeSlot();
if (debris == nullptr) {
std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n";
return false;
}
// Escala el segment al voltant del seu punt mitjà segons piece_scale
// (1.0 = original; 0.3 = "esquerda petita"). La resta del càlcul (angle,
// half_length, p1/p2) en deriva naturalment.
const Vec2 MID = {.x = (world_p1_in.x + world_p2_in.x) / 2.0F,
.y = (world_p1_in.y + world_p2_in.y) / 2.0F};
const Vec2 WORLD_P1 = {.x = MID.x + ((world_p1_in.x - MID.x) * piece_scale),
.y = MID.y + ((world_p1_in.y - MID.y) * piece_scale)};
const Vec2 WORLD_P2 = {.x = MID.x + ((world_p2_in.x - MID.x) * piece_scale),
.y = MID.y + ((world_p2_in.y - MID.y) * piece_scale)};
// 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 +
// velocitat de la bala escalada per BULLET_IMPULSE_FACTOR).
float speed =
velocitat_base +
(((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) *
Defaults::Physics::Debris::VARIACIO_SPEED);
debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x +
(bullet_impulse_velocity.x * Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR);
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y +
(bullet_impulse_velocity.y * Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR);
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;
// Vida i shrinking — min_lifetime és el temps mínim garantit; després
// el fragment mor quan |velocity| < MIN_SPEED_TO_DIE.
debris->elapsed_time = 0.0F;
debris->min_lifetime = lifetime;
debris->factor_shrink = Defaults::Physics::Debris::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::INHERITANCE_FACTOR_MIN +
((std::rand() / static_cast<float>(RAND_MAX)) *
(Defaults::Physics::Debris::INHERITANCE_FACTOR_MAX -
Defaults::Physics::Debris::INHERITANCE_FACTOR_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::SPEED_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::ROTATION_MIN +
((std::rand() / static_cast<float>(RAND_MAX)) *
(Defaults::Physics::Debris::ROTATION_MAX -
Defaults::Physics::Debris::ROTATION_MIN));
// 50% probabilitat de rotación en sentit contrari
if (std::rand() % 2 == 0) {
debris.velocitat_rot_visual = -debris.velocitat_rot_visual;
}
}
// Rebot del fragment contra els límits del PLAYAREA (mateix patró que
// PhysicsWorld::resolveBoundsCollisions per a enemics i ships).
static void bounceOffPlayArea(Vec2& centro, Vec2& velocity) {
const auto& bounds = Defaults::Zones::PLAYAREA;
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;
constexpr float REST = Defaults::Physics::Debris::RESTITUTION_BOUNDS;
if (centro.x < MIN_X) {
centro.x = MIN_X;
if (velocity.x < 0.0F) {
velocity.x = -velocity.x * REST;
}
}
if (centro.x > MAX_X) {
centro.x = MAX_X;
if (velocity.x > 0.0F) {
velocity.x = -velocity.x * REST;
}
}
if (centro.y < MIN_Y) {
centro.y = MIN_Y;
if (velocity.y < 0.0F) {
velocity.y = -velocity.y * REST;
}
}
if (centro.y > MAX_Y) {
centro.y = MAX_Y;
if (velocity.y > 0.0F) {
velocity.y = -velocity.y * REST;
}
}
}
void DebrisManager::update(float delta_time) {
for (auto& debris : debris_pool_) {
if (!debris.active) {
continue;
}
// 1. Actualitzar time de vida
debris.elapsed_time += delta_time;
// 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.elapsed_time >= 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
float speed = std::sqrt((debris.velocity.x * debris.velocity.x) +
(debris.velocity.y * debris.velocity.y));
if (speed > 1.0F) {
// Calcular direcció normalitzada
float dir_x = debris.velocity.x / speed;
float dir_y = debris.velocity.y / speed;
// Aplicar aceleración negativa (fricció)
float nova_speed = speed + (debris.acceleration * delta_time);
nova_speed = std::max(nova_speed, 0.0F);
debris.velocity.x = dir_x * nova_speed;
debris.velocity.y = dir_y * nova_speed;
} else {
// Velocidad mucho baixa, aturar
debris.velocity.x = 0.0F;
debris.velocity.y = 0.0F;
}
// 2b. Rotar vector de velocity (trayectoria curva)
if (std::abs(debris.velocitat_rot) > 0.01F) {
// Calcular angle de rotación este frame
float dangle = debris.velocitat_rot * delta_time;
// Rotar vector de velocity usant matriu de rotación 2D
float vel_x_old = debris.velocity.x;
float vel_y_old = debris.velocity.y;
float cos_a = std::cos(dangle);
float sin_a = std::sin(dangle);
debris.velocity.x = (vel_x_old * cos_a) - (vel_y_old * sin_a);
debris.velocity.y = (vel_x_old * sin_a) + (vel_y_old * cos_a);
}
// 2c. Aplicar fricció angular (desacceleració gradual)
if (std::abs(debris.velocitat_rot) > 0.01F) {
float sign = (debris.velocitat_rot > 0) ? 1.0F : -1.0F;
float reduccion =
Defaults::Physics::Debris::FRICCIO_ANGULAR * delta_time;
debris.velocitat_rot -= sign * reduccion;
// Evitar canvi de signe (no pot passar de CW a CCW)
if ((debris.velocitat_rot > 0) != (sign > 0)) {
debris.velocitat_rot = 0.0F;
}
}
// 3. Actualitzar posició del centre (integra velocity).
debris.centro.x += debris.velocity.x * delta_time;
debris.centro.y += debris.velocity.y * delta_time;
// 4. Rebot contra els límits del PLAYAREA.
bounceOffPlayArea(debris.centro, debris.velocity);
// 5. Actualitzar rotació visual acumulada.
debris.angle_rotacio += debris.velocitat_rot_visual * delta_time;
// 6. Shrink lineal sobre la longitud ORIGINAL (no iteratiu).
// SHRINK_T va de 0 a 1 al llarg de min_lifetime; després queda
// a 1 i el shrink_factor manté el valor mínim (1 - factor_shrink).
const float SHRINK_T = std::min(debris.elapsed_time / debris.min_lifetime, 1.0F);
const float SHRINK_FACTOR = std::max(0.0F, 1.0F - (debris.factor_shrink * SHRINK_T));
// 7. Reconstruir p1/p2 des de la geometria autoritaritzada:
// centro + (cos/sin(original_angle + angle_rotacio)) × original_half_length × shrink_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 * SHRINK_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