// ship_animator.cpp - Implementació del sistema d'animació de naus // © 2025 Port a C++20 amb SDL3 #include "ship_animator.hpp" #include #include #include "core/defaults.hpp" #include "core/graphics/shape_loader.hpp" #include "core/math/easing.hpp" #include "core/rendering/shape_renderer.hpp" namespace Title { ShipAnimator::ShipAnimator(SDL_Renderer* renderer) : renderer_(renderer) { } void ShipAnimator::init() { // Carregar formes de naus amb perspectiva pre-calculada auto forma_p1 = Graphics::ShapeLoader::load("ship_perspective.shp"); // Perspectiva esquerra auto forma_p2 = Graphics::ShapeLoader::load("ship2_perspective.shp"); // Perspectiva dreta // Configurar ship P1 ships_[0].player_id = 1; ships_[0].shape = forma_p1; configurar_nau_p1(ships_[0]); // Configurar ship P2 ships_[1].player_id = 2; ships_[1].shape = forma_p2; configurar_nau_p2(ships_[1]); } void ShipAnimator::update(float delta_time) { // Dispatcher segons state de cada ship for (auto& ship : ships_) { if (!ship.visible) { continue; } switch (ship.state) { case ShipState::ENTERING: actualitzar_entering(ship, delta_time); break; case ShipState::FLOATING: actualitzar_floating(ship, delta_time); break; case ShipState::EXITING: actualitzar_exiting(ship, delta_time); break; } } } void ShipAnimator::draw() const { for (const auto& ship : ships_) { if (!ship.visible) { continue; } // Renderitzar ship (perspectiva ja incorporada a la shape) Rendering::render_shape( renderer_, ship.shape, ship.current_position, 0.0F, // angle (rotació 2D no utilitzada) ship.current_scale, 1.0F, // progress (sempre visible) 1.0F // brightness (brightness màxima) ); } } void ShipAnimator::start_entry_animation() { using namespace Defaults::Title::Ships; // Configurar ship P1 per a l'animació d'entrada ships_[0].state = ShipState::ENTERING; ships_[0].state_time = 0.0F; ships_[0].initial_position = calcular_posicio_fora_pantalla(CLOCK_8_ANGLE); ships_[0].current_position = ships_[0].initial_position; ships_[0].current_scale = ships_[0].initial_scale; // Configurar ship P2 per a l'animació d'entrada ships_[1].state = ShipState::ENTERING; ships_[1].state_time = 0.0F; ships_[1].initial_position = calcular_posicio_fora_pantalla(CLOCK_4_ANGLE); ships_[1].current_position = ships_[1].initial_position; ships_[1].current_scale = ships_[1].initial_scale; } void ShipAnimator::trigger_exit_animation() { // Configurar ambdues naus per a l'animació de sortida for (auto& ship : ships_) { // Canviar state a EXITING ship.state = ShipState::EXITING; ship.state_time = 0.0F; // Preservar posició actual (pot estar a mig camí si START es prem durant ENTERING) ship.initial_position = ship.current_position; // La scale objectiu es preserva per a calcular la interpolació // (current_scale pot ser diferent si està en ENTERING) } } void ShipAnimator::skip_to_floating_state() { // Posar ambdues naus directament en state FLOATING for (auto& ship : ships_) { ship.state = ShipState::FLOATING; ship.state_time = 0.0F; ship.oscillation_phase = 0.0F; // Posar en posició objectiu (sense animació) ship.current_position = ship.target_position; ship.current_scale = ship.target_scale; // NO establir visibilitat aquí - ja ho fa el caller // (evita fer visibles ambdues naus quan només una ha premut START) } } bool ShipAnimator::is_visible() const { // Retorna true si almenys una ship és visible for (const auto& ship : ships_) { if (ship.visible) { return true; } } return false; } void ShipAnimator::trigger_exit_animation_for_player(int player_id) { // Trobar la ship del player especificat for (auto& ship : ships_) { if (ship.player_id == player_id) { // Canviar state a EXITING només per aquesta ship ship.state = ShipState::EXITING; ship.state_time = 0.0F; // Preservar posició actual (pot estar a mig camí si START es prem durant ENTERING) ship.initial_position = ship.current_position; // La scale objectiu es preserva per a calcular la interpolació // (current_scale pot ser diferent si està en ENTERING) break; // Només una ship per player } } } void ShipAnimator::set_visible(bool visible) { for (auto& ship : ships_) { ship.visible = visible; } } bool ShipAnimator::is_animation_complete() const { // Comprovar si totes les naus són invisibles (han completat l'animació de sortida) for (const auto& ship : ships_) { if (ship.visible) { return false; // Encara hi ha alguna ship visible } } return true; // Totes les naus són invisibles } // Mètodes d'animació (stubs) void ShipAnimator::actualitzar_entering(TitleShip& ship, float delta_time) { using namespace Defaults::Title::Ships; ship.state_time += delta_time; // Esperar al delay abans de començar l'animació if (ship.state_time < ship.entry_delay) { // Encara en delay: la ship es queda fora de pantalla (posició inicial) ship.current_position = ship.initial_position; ship.current_scale = ship.initial_scale; return; } // Càlcul del progrés (restant el delay) float elapsed = ship.state_time - ship.entry_delay; float progress = std::min(1.0F, elapsed / ENTRY_DURATION); // Aplicar easing (ease_out_quad per arribada suau) float eased_progress = Easing::ease_out_quad(progress); // Lerp posició (inicial → objectiu) ship.current_position.x = Easing::lerp(ship.initial_position.x, ship.target_position.x, eased_progress); ship.current_position.y = Easing::lerp(ship.initial_position.y, ship.target_position.y, eased_progress); // Lerp scale (gran → normal) ship.current_scale = Easing::lerp(ship.initial_scale, ship.target_scale, eased_progress); // Transicionar a FLOATING quan completi if (elapsed >= ENTRY_DURATION) { ship.state = ShipState::FLOATING; ship.state_time = 0.0F; ship.oscillation_phase = 0.0F; // Reiniciar fase d'oscil·lació } } void ShipAnimator::actualitzar_floating(TitleShip& ship, float delta_time) { using namespace Defaults::Title::Ships; // Actualitzar time i fase d'oscil·lació ship.state_time += delta_time; ship.oscillation_phase += delta_time; // Oscil·lació sinusoïdal X/Y (paràmetres específics per ship) float offset_x = ship.amplitude_x * std::sin(2.0F * Defaults::Math::PI * ship.frequency_x * ship.oscillation_phase); float offset_y = ship.amplitude_y * std::sin((2.0F * Defaults::Math::PI * ship.frequency_y * ship.oscillation_phase) + FLOAT_PHASE_OFFSET); // Aplicar oscil·lació a la posició objectiu ship.current_position.x = ship.target_position.x + offset_x; ship.current_position.y = ship.target_position.y + offset_y; // Escala constant (sense "breathing" per ara) ship.current_scale = ship.target_scale; } void ShipAnimator::actualitzar_exiting(TitleShip& ship, float delta_time) { using namespace Defaults::Title::Ships; ship.state_time += delta_time; // Calcular progrés (0.0 → 1.0) float progress = std::min(1.0F, ship.state_time / EXIT_DURATION); // Aplicar easing (ease_in_quad per acceleració cap al point de fuga) float eased_progress = Easing::ease_in_quad(progress); // Vec2 de fuga (centre del starfield) constexpr Vec2 punt_fuga{.x = VANISHING_POINT_X, .y = VANISHING_POINT_Y}; // Lerp posició cap al point de fuga (preservar posició inicial actual) // Nota: initial_position conté la posició on estava quan es va activar EXITING ship.current_position.x = Easing::lerp(ship.initial_position.x, punt_fuga.x, eased_progress); ship.current_position.y = Easing::lerp(ship.initial_position.y, punt_fuga.y, eased_progress); // Escala redueix a 0 (simula Z → infinit) ship.current_scale = ship.target_scale * (1.0F - eased_progress); // Marcar invisible quan l'animació completi if (progress >= 1.0F) { ship.visible = false; } } // Configuració void ShipAnimator::configurar_nau_p1(TitleShip& ship) { using namespace Defaults::Title::Ships; // Estat inicial: FLOATING (per test estàtic) ship.state = ShipState::FLOATING; ship.state_time = 0.0F; // Posicions (clock 8, bottom-left) ship.target_position = {.x = P1_TARGET_X(), .y = P1_TARGET_Y()}; // Calcular posició inicial (fora de pantalla) ship.initial_position = calcular_posicio_fora_pantalla(CLOCK_8_ANGLE); ship.current_position = ship.initial_position; // Començar fora de pantalla // Escales ship.target_scale = FLOATING_SCALE; ship.current_scale = FLOATING_SCALE; ship.initial_scale = ENTRY_SCALE_START; // Flotació ship.oscillation_phase = 0.0F; // Paràmetres d'entrada ship.entry_delay = P1_ENTRY_DELAY; // Paràmetres d'oscil·lació específics P1 ship.amplitude_x = FLOAT_AMPLITUDE_X; ship.amplitude_y = FLOAT_AMPLITUDE_Y; ship.frequency_x = FLOAT_FREQUENCY_X_BASE * P1_FREQUENCY_MULTIPLIER; ship.frequency_y = FLOAT_FREQUENCY_Y_BASE * P1_FREQUENCY_MULTIPLIER; // Visibilitat ship.visible = true; } void ShipAnimator::configurar_nau_p2(TitleShip& ship) { using namespace Defaults::Title::Ships; // Estat inicial: FLOATING (per test estàtic) ship.state = ShipState::FLOATING; ship.state_time = 0.0F; // Posicions (clock 4, bottom-right) ship.target_position = {.x = P2_TARGET_X(), .y = P2_TARGET_Y()}; // Calcular posició inicial (fora de pantalla) ship.initial_position = calcular_posicio_fora_pantalla(CLOCK_4_ANGLE); ship.current_position = ship.initial_position; // Començar fora de pantalla // Escales ship.target_scale = FLOATING_SCALE; ship.current_scale = FLOATING_SCALE; ship.initial_scale = ENTRY_SCALE_START; // Flotació ship.oscillation_phase = 0.0F; // Paràmetres d'entrada ship.entry_delay = P2_ENTRY_DELAY; // Paràmetres d'oscil·lació específics P2 ship.amplitude_x = FLOAT_AMPLITUDE_X; ship.amplitude_y = FLOAT_AMPLITUDE_Y; ship.frequency_x = FLOAT_FREQUENCY_X_BASE * P2_FREQUENCY_MULTIPLIER; ship.frequency_y = FLOAT_FREQUENCY_Y_BASE * P2_FREQUENCY_MULTIPLIER; // Visibilitat ship.visible = true; } Vec2 ShipAnimator::calcular_posicio_fora_pantalla(float angle_rellotge) const { using namespace Defaults::Title::Ships; // Convertir angle del rellotge a radians (per exemple: 240° per clock 8) // Calcular posició en direcció radial des del centre, però més lluny // ENTRY_OFFSET es calcula automàticament: (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN float extended_radius = CLOCK_RADIUS + ENTRY_OFFSET; float x = (Defaults::Game::WIDTH / 2.0F) + (extended_radius * std::cos(angle_rellotge)); float y = (Defaults::Game::HEIGHT / 2.0F) + (extended_radius * std::sin(angle_rellotge)); return {.x = x, .y = y}; } } // namespace Title