// ship.cpp - Implementación de la nave del player // © 2026 JailDesigner #include "game/entities/ship.hpp" #include #include #include #include #include #include #include "core/audio/audio.hpp" #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, PlayerConfig config, const char* shape_override) : Entity(renderer), config_(std::move(config)) { brightness_ = Defaults::Brightness::NAU; // El shape pot venir del YAML o ser overridden (ex: P2 amb "ship/wedge.shp"). const std::string SHAPE_PATH = (shape_override != nullptr) ? shape_override : config_.shape.path; shape_ = Graphics::ShapeLoader::load(SHAPE_PATH); if (!shape_ || !shape_->isValid()) { std::cerr << "[Ship] Error: no se ha podido cargar " << SHAPE_PATH << '\n'; } // Radi de col·lisió derivat del cercle circumscrit de la shape * scale * collision_factor. const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F; collision_radius_ = BOUNDING * config_.shape.scale * config_.shape.collision_factor; body_.setMass(config_.physics.mass); body_.radius = collision_radius_; body_.restitution = config_.physics.restitution; body_.linear_damping = config_.physics.linear_damping; body_.angular_damping = config_.physics.angular_damping; } void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) { if (spawn_point != nullptr) { center_ = *spawn_point; } else { float center_x; float center_y; Constants::getPlayAreaCenter(center_x, center_y); center_ = {.x = center_x, .y = center_y}; } angle_ = 0.0F; body_.position = center_; body_.angle = angle_; body_.velocity = Vec2{}; body_.angular_velocity = 0.0F; body_.clearAccumulators(); invulnerable_timer_ = activar_invulnerabilitat ? config_.invulnerability.duration : 0.0F; is_hit_ = false; hurt_timer_ = 0.0F; touching_enemy_prev_frame_ = false; } void Ship::processInput(float delta_time, uint8_t player_id) { if (is_hit_) { return; } auto* input = Input::get(); 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); applyMovement(ROTATE_LEFT, ROTATE_RIGHT, THRUST, delta_time); } void Ship::applyMovement(bool rotate_left, bool rotate_right, bool thrust, float delta_time) { if (is_hit_) { return; } if (rotate_right) { body_.angle += config_.physics.rotation_speed * delta_time; } if (rotate_left) { body_.angle -= config_.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)); const float MAGNITUDE = body_.mass * config_.physics.acceleration; body_.applyForce(Vec2{.x = DIR_X * MAGNITUDE, .y = DIR_Y * MAGNITUDE}); } } void Ship::update(float delta_time) { if (is_hit_) { return; } if (invulnerable_timer_ > 0.0F) { invulnerable_timer_ -= delta_time; invulnerable_timer_ = std::max(invulnerable_timer_, 0.0F); } if (hurt_timer_ > 0.0F) { hurt_timer_ -= delta_time; hurt_timer_ = std::max(hurt_timer_, 0.0F); } // 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 > config_.physics.max_velocity) { body_.velocity = body_.velocity * (config_.physics.max_velocity / CURRENT_SPEED); } } void Ship::postUpdate(float /*delta_time*/) { center_ = body_.position; angle_ = body_.angle; } void Ship::draw() const { if (is_hit_) { return; } if (isInvulnerable()) { const float BLINK_CYCLE = config_.invulnerability.blink_visible + config_.invulnerability.blink_invisible; const float TIME_IN_CYCLE = std::fmod(invulnerable_timer_, BLINK_CYCLE); if (TIME_IN_CYCLE < config_.invulnerability.blink_invisible) { return; } } if (!shape_) { return; } // Efecte visual d'empenta (modulador sobre l'escala base del YAML). const float SPEED = getSpeed(); const float VISUAL_PUSH = SPEED / config_.visual_thrust.push_divisor; const float THRUST_MODULATOR = 1.0F + (VISUAL_PUSH / config_.visual_thrust.scale_divisor); const float SCALE = config_.shape.scale * THRUST_MODULATOR; // Parpelleig mentre està ferida: alterna color normal ↔ color hurt. SDL_Color color = config_.colors.normal; if (hurt_timer_ > 0.0F) { const float CYCLE = 1.0F / config_.hurt.blink_hz; const float T = std::fmod(hurt_timer_, CYCLE); if (T < (CYCLE / 2.0F)) { color = config_.colors.hurt; } } Rendering::renderShape(renderer_, shape_, center_, angle_, SCALE, 1.0F, brightness_, color); } void Ship::hurt() { hurt_timer_ = config_.hurt.duration; Audio::get()->playSound(Defaults::Sound::HURT, Audio::Group::GAME); }