// ship_animator3d.cpp - Implementació de l'animador de naus 3D // © 2026 JailDesigner #include "ship_animator3d.hpp" #include #include #include "core/defaults.hpp" #include "core/graphics/shape_loader.hpp" #include "core/math/easing.hpp" namespace Title { namespace { // Profunditat d'extrusió de la silueta 2D de la nau (en unitats mundials). constexpr float SHIP_EXTRUSION_DEPTH = 8.0F; // Posicions en l'espai 3D (càmera a (0,0,0) mirant cap a +Z, Y cap amunt). constexpr float SHIP_FLOAT_X = 20.0F; // Separació horitzontal constexpr float SHIP_FLOAT_Y = -8.0F; // Lleugerament per davall del centre constexpr float SHIP_FLOAT_Z = 60.0F; // Distància de flotació // Posició d'entrada (a prop de càmera, fora del frustrum lateral). constexpr float SHIP_ENTRY_X = 50.0F; constexpr float SHIP_ENTRY_Y = -15.0F; constexpr float SHIP_ENTRY_Z = 28.0F; // Posició de fuga (al fons, centre projectat). constexpr float SHIP_EXIT_Z = 800.0F; // Mida visual i animació. constexpr float SHIP_FLOAT_SCALE = 1.0F; constexpr float SHIP_ENTRY_SCALE = 1.0F; // Mida mundial idèntica; la perspectiva fa la resta constexpr float ENTRY_DURATION = 2.0F; constexpr float EXIT_DURATION = 1.0F; // Oscil·lació en unitats mundials (al voltant del target_position). constexpr float FLOAT_AMPLITUDE_X = 1.5F; constexpr float FLOAT_AMPLITUDE_Y = 1.0F; constexpr float FLOAT_FREQUENCY_X_BASE = 0.5F; constexpr float FLOAT_FREQUENCY_Y_BASE = 0.7F; constexpr float FLOAT_PHASE_OFFSET = 1.57F; constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; constexpr float P1_ENTRY_DELAY = 0.0F; constexpr float P2_ENTRY_DELAY = 0.5F; } // namespace ShipAnimator3D::ShipAnimator3D(Rendering::Renderer* renderer, const Graphics::Camera3D* camera) : renderer_(renderer), camera_(camera) { } void ShipAnimator3D::init() { auto shape_p1 = Graphics::ShapeLoader::load("ship.shp"); auto shape_p2 = Graphics::ShapeLoader::load("ship2.shp"); ships_[0].player_id = 1; if (shape_p1 && shape_p1->isValid()) { ships_[0].mesh = Graphics::extrudeShape2D(*shape_p1, SHIP_EXTRUSION_DEPTH); } configureShipP1(ships_[0]); ships_[1].player_id = 2; if (shape_p2 && shape_p2->isValid()) { ships_[1].mesh = Graphics::extrudeShape2D(*shape_p2, SHIP_EXTRUSION_DEPTH); } configureShipP2(ships_[1]); } void ShipAnimator3D::update(float delta_time) { for (auto& ship : ships_) { if (!ship.visible) { continue; } switch (ship.state) { case ShipState3D::ENTERING: updateEntering(ship, delta_time); break; case ShipState3D::FLOATING: updateFloating(ship, delta_time); break; case ShipState3D::EXITING: updateExiting(ship, delta_time); break; } } } void ShipAnimator3D::draw() const { if (camera_ == nullptr || renderer_ == nullptr) { return; } for (const auto& ship : ships_) { if (!ship.visible) { continue; } const Graphics::Transform3D TRANSFORM{ .position = ship.current_position, .rotation_euler = Vec3{}, .scale = ship.current_scale, }; Graphics::drawWireframe(renderer_, *camera_, ship.mesh, TRANSFORM, 1.0F); } } void ShipAnimator3D::startEntryAnimation() { for (auto& ship : ships_) { ship.state = ShipState3D::ENTERING; ship.state_time = 0.0F; ship.current_position = ship.initial_position; ship.current_scale = ship.initial_scale; } } void ShipAnimator3D::triggerExitAnimation() { for (auto& ship : ships_) { ship.state = ShipState3D::EXITING; ship.state_time = 0.0F; ship.initial_position = ship.current_position; } } void ShipAnimator3D::triggerExitAnimationForPlayer(int player_id) { for (auto& ship : ships_) { if (ship.player_id == player_id) { ship.state = ShipState3D::EXITING; ship.state_time = 0.0F; ship.initial_position = ship.current_position; break; } } } void ShipAnimator3D::skipToFloatingState() { for (auto& ship : ships_) { ship.state = ShipState3D::FLOATING; ship.state_time = 0.0F; ship.oscillation_phase = 0.0F; ship.current_position = ship.target_position; ship.current_scale = ship.target_scale; } } void ShipAnimator3D::setVisible(bool visible) { for (auto& ship : ships_) { ship.visible = visible; } } auto ShipAnimator3D::isVisible() const -> bool { return std::ranges::any_of(ships_, [](const TitleShip3D& s) { return s.visible; }); } auto ShipAnimator3D::isAnimationComplete() const -> bool { return std::ranges::all_of(ships_, [](const TitleShip3D& s) { return !s.visible; }); } void ShipAnimator3D::updateEntering(TitleShip3D& ship, float delta_time) { ship.state_time += delta_time; if (ship.state_time < ship.entry_delay) { ship.current_position = ship.initial_position; ship.current_scale = ship.initial_scale; return; } const float ELAPSED = ship.state_time - ship.entry_delay; const float PROGRESS = std::min(1.0F, ELAPSED / ENTRY_DURATION); const float EASED = Easing::easeOutQuad(PROGRESS); ship.current_position.x = Easing::lerp(ship.initial_position.x, ship.target_position.x, EASED); ship.current_position.y = Easing::lerp(ship.initial_position.y, ship.target_position.y, EASED); ship.current_position.z = Easing::lerp(ship.initial_position.z, ship.target_position.z, EASED); ship.current_scale = Easing::lerp(ship.initial_scale, ship.target_scale, EASED); if (ELAPSED >= ENTRY_DURATION) { ship.state = ShipState3D::FLOATING; ship.state_time = 0.0F; ship.oscillation_phase = 0.0F; } } void ShipAnimator3D::updateFloating(TitleShip3D& ship, float delta_time) { ship.state_time += delta_time; ship.oscillation_phase += delta_time; const float TWO_PI = 2.0F * Defaults::Math::PI; const float OFFSET_X = ship.amplitude_x * std::sin(TWO_PI * ship.frequency_x * ship.oscillation_phase); const float OFFSET_Y = ship.amplitude_y * std::sin((TWO_PI * ship.frequency_y * ship.oscillation_phase) + FLOAT_PHASE_OFFSET); ship.current_position.x = ship.target_position.x + OFFSET_X; ship.current_position.y = ship.target_position.y + OFFSET_Y; ship.current_position.z = ship.target_position.z; ship.current_scale = ship.target_scale; } void ShipAnimator3D::updateExiting(TitleShip3D& ship, float delta_time) { ship.state_time += delta_time; const float PROGRESS = std::min(1.0F, ship.state_time / EXIT_DURATION); const float EASED = Easing::easeInQuad(PROGRESS); // Vola cap al centre projectat (x=0, y=0) i a Z gran (lluny). ship.current_position.x = Easing::lerp(ship.initial_position.x, 0.0F, EASED); ship.current_position.y = Easing::lerp(ship.initial_position.y, 0.0F, EASED); ship.current_position.z = Easing::lerp(ship.initial_position.z, SHIP_EXIT_Z, EASED); ship.current_scale = ship.target_scale; // L'escala visual baixa via la perspectiva if (PROGRESS >= 1.0F) { ship.visible = false; } } void ShipAnimator3D::configureShipP1(TitleShip3D& ship) { ship.state = ShipState3D::FLOATING; ship.state_time = 0.0F; // Esquerra: x negatiu. ship.target_position = Vec3{ .x = -SHIP_FLOAT_X, .y = SHIP_FLOAT_Y, .z = SHIP_FLOAT_Z}; ship.initial_position = Vec3{ .x = -SHIP_ENTRY_X, .y = SHIP_ENTRY_Y, .z = SHIP_ENTRY_Z}; ship.current_position = ship.initial_position; ship.target_scale = SHIP_FLOAT_SCALE; ship.current_scale = SHIP_FLOAT_SCALE; ship.initial_scale = SHIP_ENTRY_SCALE; ship.oscillation_phase = 0.0F; ship.entry_delay = P1_ENTRY_DELAY; 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; ship.visible = true; } void ShipAnimator3D::configureShipP2(TitleShip3D& ship) { ship.state = ShipState3D::FLOATING; ship.state_time = 0.0F; // Dreta: x positiu. ship.target_position = Vec3{ .x = SHIP_FLOAT_X, .y = SHIP_FLOAT_Y, .z = SHIP_FLOAT_Z}; ship.initial_position = Vec3{ .x = SHIP_ENTRY_X, .y = SHIP_ENTRY_Y, .z = SHIP_ENTRY_Z}; ship.current_position = ship.initial_position; ship.target_scale = SHIP_FLOAT_SCALE; ship.current_scale = SHIP_FLOAT_SCALE; ship.initial_scale = SHIP_ENTRY_SCALE; ship.oscillation_phase = 0.0F; ship.entry_delay = P2_ENTRY_DELAY; 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; ship.visible = true; } } // namespace Title