// debris_manager.cpp - Implementació del gestor de fragments // © 2026 JailDesigner #include "debris_manager.hpp" #include #include #include #include #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& 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 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::vector> 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(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(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(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(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(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(debris.p1.x), static_cast(debris.p1.y), static_cast(debris.p2.x), static_cast(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(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