// ship.cpp - Implementación de la nave del player // © 2026 JailDesigner #include "game/entities/ship.hpp" #include #include #include #include #include #include "core/defaults.hpp" #include "core/entities/entity.hpp" #include "core/graphics/shape_loader.hpp" #include "core/input/input.hpp" #include "core/input/input_types.hpp" #include "core/rendering/shape_renderer.hpp" #include "core/types.hpp" #include "game/constants.hpp" Ship::Ship(Rendering::Renderer* renderer, const char* shape_file) : Entity(renderer) { // Brightness específico para naves brightness_ = Defaults::Brightness::NAU; // Configuración del cuerpo físico body_.setMass(10.0F); // Masa de referencia para choques body_.radius = Defaults::Entities::SHIP_RADIUS; // Radio de colisión body_.restitution = 0.6F; // Rebote moderado contra paredes body_.linear_damping = 1.5F; // Fricción exponencial (s⁻¹) body_.angular_damping = 0.0F; // La rotación es 100% por input, no inercial // Cargar shape compartida desde archivo shape_ = Graphics::ShapeLoader::load(shape_file); if (!shape_ || !shape_->isValid()) { std::cerr << "[Ship] Error: no se ha podido cargar " << shape_file << '\n'; } } void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) { // Posición inicial if (spawn_point != nullptr) { center_ = *spawn_point; } else { float centre_x; float centre_y; Constants::getPlayAreaCenter(centre_x, centre_y); center_ = {.x = centre_x, .y = centre_y}; } // Reset orientación angle_ = 0.0F; // Sincronizar cuerpo físico con la posición/orientación inicial body_.position = center_; body_.angle = angle_; body_.velocity = Vec2{}; body_.angular_velocity = 0.0F; body_.clearAccumulators(); // Activar invulnerabilidad solo si es respawn invulnerable_timer_ = activar_invulnerabilitat ? Defaults::Ship::INVULNERABILITY_DURATION : 0.0F; is_hit_ = false; } void Ship::processInput(float delta_time, uint8_t player_id) { // Solo procesa input si la nave está viva if (is_hit_) { return; } auto* input = Input::get(); // Rotación: control directo del ángulo (no física, no inercial). // Se actualiza también body_.angle para que el dibujado tras // postUpdate refleje el cambio inmediatamente. const bool ROTATE_RIGHT = (player_id == 0) ? input->checkActionPlayer1(InputAction::RIGHT, Input::ALLOW_REPEAT) : input->checkActionPlayer2(InputAction::RIGHT, Input::ALLOW_REPEAT); const bool ROTATE_LEFT = (player_id == 0) ? input->checkActionPlayer1(InputAction::LEFT, Input::ALLOW_REPEAT) : input->checkActionPlayer2(InputAction::LEFT, Input::ALLOW_REPEAT); const bool THRUST = (player_id == 0) ? input->checkActionPlayer1(InputAction::THRUST, Input::ALLOW_REPEAT) : input->checkActionPlayer2(InputAction::THRUST, Input::ALLOW_REPEAT); if (ROTATE_RIGHT) { body_.angle += Defaults::Physics::ROTATION_SPEED * delta_time; } if (ROTATE_LEFT) { body_.angle -= Defaults::Physics::ROTATION_SPEED * delta_time; } // Thrust: fuerza vectorial en la dirección de la nariz. // angle - PI/2 porque angle=0 apunta hacia arriba (eje Y negativo SDL). if (THRUST) { const float DIR_X = std::cos(body_.angle - (Constants::PI / 2.0F)); const float DIR_Y = std::sin(body_.angle - (Constants::PI / 2.0F)); // Fuerza = masa * aceleración: 10 kg * 400 px/s² = 4000 (unidades arcade) const float MAGNITUDE = body_.mass * Defaults::Physics::ACCELERATION; body_.applyForce(Vec2{.x = DIR_X * MAGNITUDE, .y = DIR_Y * MAGNITUDE}); } } void Ship::update(float delta_time) { // Solo update si la nave está viva if (is_hit_) { return; } // Decrementar timer de invulnerabilidad if (invulnerable_timer_ > 0.0F) { invulnerable_timer_ -= delta_time; invulnerable_timer_ = std::max(invulnerable_timer_, 0.0F); } // El movimiento real lo hace PhysicsWorld::update(). // Aquí solo lógica de estado. // Cap de velocidad: el thrust acumula fuerza sin límite; limitamos // la magnitud de body_.velocity tras aplicar fuerzas para preservar // el feel arcade del MAX_VELOCITY original. const float CURRENT_SPEED = body_.velocity.length(); if (CURRENT_SPEED > Defaults::Physics::MAX_VELOCITY) { body_.velocity = body_.velocity * (Defaults::Physics::MAX_VELOCITY / CURRENT_SPEED); } } void Ship::postUpdate(float /*delta_time*/) { // Sincronizar mirror desde body_ tras la integración del world. center_ = body_.position; angle_ = body_.angle; } void Ship::draw() const { if (is_hit_) { return; } // Parpadeo si invulnerable if (isInvulnerable()) { const float BLINK_CYCLE = Defaults::Ship::BLINK_VISIBLE_TIME + Defaults::Ship::BLINK_INVISIBLE_TIME; const float TIME_IN_CYCLE = std::fmod(invulnerable_timer_, BLINK_CYCLE); if (TIME_IN_CYCLE < Defaults::Ship::BLINK_INVISIBLE_TIME) { return; } } if (!shape_) { return; } // Efecto visual de empuje: escala proporcional a la velocidad. // 0..200 px/s → escala 1.0..1.5 (manteniendo la sensación del Pascal original). const float SPEED = getSpeed(); const float VISUAL_PUSH = SPEED / 33.33F; const float SCALE = 1.0F + (VISUAL_PUSH / 12.0F); Rendering::renderShape(renderer_, shape_, center_, angle_, SCALE, 1.0F, brightness_, /*rotation_3d=*/nullptr, Defaults::Palette::SHIP); }