diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md index a5b1288..7640a7a 100644 --- a/MIGRATION_PLAN.md +++ b/MIGRATION_PLAN.md @@ -35,44 +35,37 @@ Tag de seguridad: **`beta-3.0`** (snapshot de `main` antes de empezar). | 5 — Infraestructura física (`RigidBody`, `PhysicsWorld`) | ✅ | `0fd9360` | | 6a+b — `body_` en Entity + `world_` en GameScene | ✅ | `0574077` | | 6c — Migrar Ship | ✅ | `2fe22ff` | -| **6d — Migrar Enemy** | ✅ | `27242f5` | -| 6e — Migrar Bullet | 🔲 **siguiente** | — | -| 7 — Migración a SDL3 GPU (sin fallback) | 🔲 | — | +| 6d — Migrar Enemy | ✅ | `27242f5` | +| **6e — Migrar Bullet** | ✅ | (este commit) | +| 7 — Migración a SDL3 GPU (sin fallback) | 🔲 **siguiente** | — | | 8 — Postprocesado, color, paleta por tipo | 🔲 | — | | 9 — Refactor de GameScene (2.877 LOC → módulos) | 🔲 | — | | 10 — Tuning final de masa/restitución/damping | 🔲 | — | -## Lo que queda inmediato (Fase 6e) +## Lo que queda inmediato (Fase 7) -Migrar `Bullet` al PhysicsWorld. Es la entidad más simple: +**Migración del renderizado a SDL3 GPU sin fallback.** -- Constructor: `body_.setMass(0.5)`, `radius = BULLET_RADIUS = 3`, `restitution = 0` - (NO rebotan, salen del PLAYAREA y se desactivan), `linear_damping = 0`. -- `init()` resetea body_. -- `disparar()`/spawn: setear `body_.position` y `body_.velocity` cartesiano a `140 px/s` en - dirección del shooter. -- `update()`: detectar si salió del PLAYAREA con margen; si sí, `desactivar()`. -- `postUpdate()`: sincronizar `center_` desde `body_.position`. -- En GameScene: registrar bodies en `init()`, llamar `postUpdate()` en `update()`. -- Cuando un bullet se desactiva: `body_.radius = 0` para no estorbar. -- **Edge case importante**: las balas NO deben rebotar contra las naves de los - jugadores. Las colisiones físicas las hace el world automáticamente. Para - evitarlo: - - Opción A: dejar que el world resuelva el impulso, pero la lógica de - gameplay (en GameScene) detecta el hit y desactiva el bullet antes de que - se vea visualmente el rebote (1 frame de margen). - - Opción B: hacer que `body_.radius = 0` para las balas, así no colisionan - físicamente, y solo las colisiones de gameplay (check_collision en - GameScene) las gestionan. - - Recomendado: **Opción B**. Las balas físicamente no necesitan colisión - del world — son cinemáticas. Solo el gameplay las usa. +Fase 6e completada: las balas son cinemáticas dentro del PhysicsWorld (radius=0 +en el world → no colisionan físicamente, solo gameplay-level via check_collision). +Body con mass=0.5, restitution=0, damping=0. `disparar()` setea +`body_.position` + `body_.velocity` cartesiana; `update()` detecta salida de +PLAYAREA y desactiva; `postUpdate()` sincroniza `center_` desde body. Smoke +test xvfb pasa sin errores. -Tras 6e, hacer **Fase 10 (tuning)**: ajustar masas, restitución, damping según -feel. Probablemente: -- Ship: subir `mass` a 15-20 (más "anclado", menos empujable por enemies). -- Enemies: reducir `restitution` a 0.85 (rebote más natural, sin "pelota"). -- Probar collision con friendly fire / nave-enemy en debris ahora que ambos - tienen body real. +Decisión técnica del proyecto: **no fallback a SDL_Renderer** +([memoria](./.claude/projects/-home-sergio-gitea-orni-attack/memory/project_no_sdl_fallback.md)). +La fase 7 reemplaza completamente el pipeline actual de SDL_Renderer +(`SDL_RenderLine`, `SDL_RenderGeometry`) por SDL3 GPU. + +Tareas previstas Fase 7: +- Sustituir `SDL_Renderer*` por `SDL_GPUDevice*` en SDLManager. +- Setup de swapchain + shaders básicos (vertex + fragment) para líneas vectoriales. +- Migrar `Rendering::render_shape` a un pipeline GPU que reciba vertex buffers. +- Postprocesado mínimo (Fase 8): bloom/glow, paleta por tipo de entidad. + +**Fase 10 (tuning)** queda pendiente para después de SDL3 GPU + postpro. El usuario +prefiere terminar la migración técnica antes de tunear feel. ## Memoria del proyecto @@ -115,16 +108,22 @@ Resumen: indica en qué fase estamos). 6. Tras cada commit, push automático: `git push origin rewrite/physics-gpu`. -## Configuración física actual (Fase 6d) +## Configuración física actual (Fase 6e) -| Entidad | mass | radius | restitution | linear_damping | angular_damping | +| Entidad | mass | radius (world) | restitution | linear_damping | angular_damping | |---|---|---|---|---|---| | Ship | 10.0 | 12 | 0.6 | 1.5 | 0.0 | | Enemy Pentagon | 5.0 | 20 | 1.0 | 0.0 | 0.0 | | Enemy Quadrat | 8.0 | 20 | 1.0 | 0.0 | 0.0 | | Enemy Molinillo | 4.0 | 20 | 1.0 | 0.0 | 0.0 | -| Bullet | (pendiente Fase 6e) | 3 | 0 | 0 | 0 | +| Bullet | 0.5 | **0** (cinemática) | 0 | 0 | 0 | | Wall (PLAYAREA bounds) | ∞ (estático) | — | — | — | — | +Nota: Bullet usa `radius=0` en el body físico para que no participe en las +colisiones del world (ni body-body ni bounds). El radio de gameplay +(`Defaults::Entities::BULLET_RADIUS = 3`) lo expone vía `getCollisionRadius()` +y lo consumen `check_collision` y la detección de salida del PLAYAREA en +`Bullet::update`. + Las paredes son implícitas: `physics_world_.setBounds(PLAYAREA)` en `GameScene::init`. No son `RigidBody` separados, sino un AABB que rebota. diff --git a/source/game/entities/bullet.cpp b/source/game/entities/bullet.cpp index f178c0d..b3a854b 100644 --- a/source/game/entities/bullet.cpp +++ b/source/game/entities/bullet.cpp @@ -1,4 +1,4 @@ -// bullet.cpp - Implementació de projectils de la ship +// bullet.cpp - Implementación de projectils de la ship // © 1999 Visente i Sergi (versión Pascal) // © 2025 Port a C++20 con SDL3 @@ -17,18 +17,33 @@ #include "core/types.hpp" #include "game/constants.hpp" +namespace { +// Velocidad escalar de las balas (px/s). Conserva el feel del Pascal original +// (7 px/frame × 20 FPS = 140 px/s). +constexpr float BULLET_SPEED = 140.0F; +} // namespace + Bullet::Bullet(SDL_Renderer* renderer) : Entity(renderer), - velocity_(0.0F), esta_(false), owner_id_(0), grace_timer_(0.0F) { - // [NUEVO] Brightness específic per balas + // Brightness específico para balas brightness_ = Defaults::Brightness::BALA; - // [NUEVO] Carregar shape compartida desde file - shape_ = Graphics::ShapeLoader::load("bullet.shp"); + // Configuración del cuerpo físico. + // Las balas son cinemáticas: no colisionan con otros bodies ni paredes. + // El gameplay (GameScene) gestiona los hits con check_collision y la + // salida del PLAYAREA. Por eso radius=0 en el world (no participa en + // resolveBodyCollisions ni resolveBoundsCollisions). + body_.setMass(0.5F); // Ligera (no afecta a nadie, pero por consistencia) + body_.radius = 0.0F; // Sin colisión física (cinemática pura) + body_.restitution = 0.0F; // Irrelevante (no rebota) + body_.linear_damping = 0.0F; // Sin fricción (movimiento rectilíneo uniforme) + body_.angular_damping = 0.0F; + // Cargar shape compartida desde archivo + shape_ = Graphics::ShapeLoader::load("bullet.shp"); if (!shape_ || !shape_->isValid()) { std::cerr << "[Bullet] Error: no s'ha pogut load bullet.shp" << '\n'; } @@ -39,77 +54,57 @@ void Bullet::init() { esta_ = false; center_ = {.x = 0.0F, .y = 0.0F}; angle_ = 0.0F; - velocity_ = 0.0F; grace_timer_ = 0.0F; + + // Reset del cuerpo físico + body_.position = Vec2{}; + body_.velocity = Vec2{}; + body_.angle = 0.0F; + body_.angular_velocity = 0.0F; + body_.clearAccumulators(); } void Bullet::disparar(const Vec2& position, float angle, uint8_t owner_id) { - // Activar bullet i posicionar-la a la ship - // Basat en joc_asteroides.cpp línies 188-200 - // Activar bullet esta_ = true; - // Posición inicial = centro de la ship - center_.x = position.x; - center_.y = position.y; - - // Angle = angle de la ship (dispara en la direcció que apunta) - angle_ = angle; - // Almacenar propietario (0=P1, 1=P2) owner_id_ = owner_id; - // Velocidad alta (el juego Pascal original usava 7 px/frame) - // 7 px/frame × 20 FPS = 140 px/s - velocity_ = 140.0F; - // Activar grace period (prevents instant self-collision) grace_timer_ = Defaults::Game::BULLET_GRACE_PERIOD; + // Posición y orientación iniciales = ship + center_ = position; + angle_ = angle; + + // Sincronizar el body físico: posición + velocidad cartesiana + // angle - PI/2 porque angle=0 apunta hacia arriba (eje Y negativo SDL) + body_.position = position; + body_.angle = angle; + const float DIR_X = std::cos(angle - (Constants::PI / 2.0F)); + const float DIR_Y = std::sin(angle - (Constants::PI / 2.0F)); + body_.velocity = Vec2{.x = DIR_X * BULLET_SPEED, .y = DIR_Y * BULLET_SPEED}; + body_.angular_velocity = 0.0F; + body_.clearAccumulators(); + // Reproducir sonido de disparo láser Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME); } void Bullet::update(float delta_time) { - if (esta_) { - // Decrementar grace timer - if (grace_timer_ > 0.0F) { - grace_timer_ -= delta_time; - grace_timer_ = std::max(grace_timer_, 0.0F); - } - - mou(delta_time); + if (!esta_) { + return; } -} -void Bullet::draw() const { - if (esta_ && shape_) { - // [NUEVO] Usar render_shape en lloc de rota_pol - // Les balas roten segons l'angle de trajectòria - Rendering::render_shape(renderer_, shape_, center_, angle_, 1.0F, 1.0F, brightness_); + // Decrementar grace timer + if (grace_timer_ > 0.0F) { + grace_timer_ -= delta_time; + grace_timer_ = std::max(grace_timer_, 0.0F); } -} -void Bullet::mou(float delta_time) { - // Moviment rectilini de la bullet - // Basat en el codi Pascal original: procedure mou_bales - // Copiat EXACTAMENT de joc_asteroides.cpp línies 396-419 - - // Calcular nueva posición (movement polar time-based) - // velocity ya está en px/s (140 px/s), solo necesario multiplicar per delta_time - float velocitat_efectiva = velocity_ * delta_time; - - // Calcular desplaçament (angle-PI/2 perquè angle=0 apunta amunt) - float dy = velocitat_efectiva * std::sin(angle_ - (Constants::PI / 2.0F)); - float dx = velocitat_efectiva * std::cos(angle_ - (Constants::PI / 2.0F)); - - // Acumulació directa con precisió subpíxel - center_.y += dy; - center_.x += dx; - - // Desactivar si surt de la zona de juego (no rebota como los ORNIs) - // CORRECCIÓ: Usar límits segurs con radi de la bullet + // El movimiento real lo hace PhysicsWorld::update() (integración). + // Aquí solo lógica de estado: detectar salida del PLAYAREA y desactivar. float min_x; float max_x; float min_y; @@ -120,8 +115,28 @@ void Bullet::mou(float delta_time) { min_y, max_y); - if (center_.x < min_x || center_.x > max_x || - center_.y < min_y || center_.y > max_y) { - esta_ = false; + if (body_.position.x < min_x || body_.position.x > max_x || + body_.position.y < min_y || body_.position.y > max_y) { + desactivar(); + } +} + +void Bullet::postUpdate(float /*delta_time*/) { + // Sincronizar mirror desde body_ tras la integración del world. + center_ = body_.position; + // angle_ no cambia (las balas no rotan visualmente). +} + +void Bullet::desactivar() { + esta_ = false; + // Detener el cuerpo físico para que no acumule deriva mientras inactiva. + body_.velocity = Vec2{}; + body_.angular_velocity = 0.0F; +} + +void Bullet::draw() const { + if (esta_ && shape_) { + // Les bales roten segons l'angle de trayectòria (estático tras disparo) + Rendering::render_shape(renderer_, shape_, center_, angle_, 1.0F, 1.0F, brightness_); } } diff --git a/source/game/entities/bullet.hpp b/source/game/entities/bullet.hpp index 0cf5518..8b95410 100644 --- a/source/game/entities/bullet.hpp +++ b/source/game/entities/bullet.hpp @@ -20,31 +20,29 @@ class Bullet : public Entities::Entity { void init() override; void disparar(const Vec2& position, float angle, uint8_t owner_id); void update(float delta_time) override; + void postUpdate(float delta_time) override; void draw() const override; - // Override: Interfície d'Entity - [[nodiscard]] bool isActive() const override { return esta_; } + // Override: Interfaz de Entity + [[nodiscard]] auto isActive() const -> bool override { return esta_; } - // Override: Interfície de colisión - [[nodiscard]] float getCollisionRadius() const override { + // Override: Interfaz de colisión (gameplay-level: PLAYAREA bounds-check) + [[nodiscard]] auto getCollisionRadius() const -> float override { return Defaults::Entities::BULLET_RADIUS; } - [[nodiscard]] bool isCollidable() const override { + [[nodiscard]] auto isCollidable() const -> bool override { return esta_ && grace_timer_ <= 0.0F; } - // Getters (API pública sin canvis) - [[nodiscard]] bool esta_activa() const { return esta_; } - [[nodiscard]] uint8_t get_owner_id() const { return owner_id_; } - [[nodiscard]] float get_grace_timer() const { return grace_timer_; } - void desactivar() { esta_ = false; } + // Getters (API pública sin cambios) + [[nodiscard]] auto esta_activa() const -> bool { return esta_; } + [[nodiscard]] auto getOwnerId() const -> uint8_t { return owner_id_; } + [[nodiscard]] auto getGraceTimer() const -> float { return grace_timer_; } + void desactivar(); private: - // Membres específics de Bullet (heretats: renderer_, shape_, center_, angle_, brightness_) - float velocity_; + // Miembros específicos de Bullet (heredados: renderer_, shape_, center_, angle_, brightness_, body_) bool esta_; uint8_t owner_id_; // 0=P1, 1=P2 float grace_timer_; // Grace period timer (0.0 = vulnerable) - - void mou(float delta_time); }; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index ccc40ef..7546cd7 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -205,9 +205,13 @@ void GameScene::init() { // DON'T call enemy.init() here - stage system handles spawning } - // Inicialitzar balas (now 6 instead of 3) + // Inicialitzar balas (now 6 instead of 3). + // Se registran en el physics_world para integración cinemática. + // Como su body_.radius=0, no colisionan físicamente con nadie (las + // colisiones de gameplay se gestionan en detectar_col·lisions_*). for (auto& bullet : bullets_) { bullet.init(); + physics_world_.addBody(&bullet.getBody()); } // [ELIMINAT] Iniciar música de juego (ara es gestiona en stage_manager) @@ -230,6 +234,9 @@ void GameScene::update(float delta_time) { for (auto& enemy : enemies_) { enemy.postUpdate(delta_time); } + for (auto& bullet : bullets_) { + bullet.postUpdate(delta_time); + } // Processar disparos (state-based, no event-based) if (game_over_state_ == GameOverState::NONE) { @@ -996,7 +1003,7 @@ void GameScene::detectar_col·lisions_bales_enemics() { } // 2. Add score to the player who shot it - uint8_t owner_id = bullet.get_owner_id(); + uint8_t owner_id = bullet.getOwnerId(); score_per_player_[owner_id] += points; // 3. Create floating score number @@ -1078,11 +1085,11 @@ void GameScene::detectar_col·lisions_bales_jugadors() { } // Skip bullets in grace period (prevents instant self-collision) - if (bullet.get_grace_timer() > 0.0F) { + if (bullet.getGraceTimer() > 0.0F) { continue; } - uint8_t bullet_owner = bullet.get_owner_id(); + uint8_t bullet_owner = bullet.getOwnerId(); // Check collision with BOTH players for (uint8_t player_id = 0; player_id < 2; player_id++) {