diff --git a/source/game/title/ship_animator3d.cpp b/source/game/title/ship_animator3d.cpp new file mode 100644 index 0000000..6b4139d --- /dev/null +++ b/source/game/title/ship_animator3d.cpp @@ -0,0 +1,271 @@ +// 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 diff --git a/source/game/title/ship_animator3d.hpp b/source/game/title/ship_animator3d.hpp new file mode 100644 index 0000000..334a7b0 --- /dev/null +++ b/source/game/title/ship_animator3d.hpp @@ -0,0 +1,81 @@ +// ship_animator3d.hpp - Sistema d'animació de naus 3D per a l'escena de títol +// © 2026 JailDesigner +// +// Equivalent 3D del `Title::ShipAnimator`. Manté la mateixa màquina d'estats +// (ENTERING → FLOATING → EXITING) però treballa amb posicions Vec3 i emet +// wireframes a través d'una `Camera3D`. La geometria s'extrau de `ship.shp` +// (P1) i `ship2.shp` (P2) per extrusió en Z. + +#pragma once + +#include +#include + +#include "core/graphics/camera3d.hpp" +#include "core/graphics/wireframe3d.hpp" +#include "core/rendering/render_context.hpp" +#include "core/types.hpp" + +namespace Title { + + enum class ShipState3D : std::uint8_t { + ENTERING, + FLOATING, + EXITING, + }; + + struct TitleShip3D { + int player_id{0}; + ShipState3D state{ShipState3D::ENTERING}; + float state_time{0.0F}; + + Vec3 initial_position{}; + Vec3 target_position{}; + Vec3 current_position{}; + + float initial_scale{1.0F}; + float target_scale{1.0F}; + float current_scale{1.0F}; + + float oscillation_phase{0.0F}; + float entry_delay{0.0F}; + + float amplitude_x{0.0F}; + float amplitude_y{0.0F}; + float frequency_x{0.0F}; + float frequency_y{0.0F}; + + Graphics::Mesh3D mesh; + bool visible{false}; + }; + + class ShipAnimator3D { + public: + ShipAnimator3D(Rendering::Renderer* renderer, const Graphics::Camera3D* camera); + + void init(); + void update(float delta_time); + void draw() const; + + void startEntryAnimation(); + void triggerExitAnimation(); + void triggerExitAnimationForPlayer(int player_id); + void skipToFloatingState(); + + void setVisible(bool visible); + [[nodiscard]] auto isAnimationComplete() const -> bool; + [[nodiscard]] auto isVisible() const -> bool; + + private: + Rendering::Renderer* renderer_; + const Graphics::Camera3D* camera_; + std::array ships_; + + static void updateEntering(TitleShip3D& ship, float delta_time); + static void updateFloating(TitleShip3D& ship, float delta_time); + static void updateExiting(TitleShip3D& ship, float delta_time); + static void configureShipP1(TitleShip3D& ship); + static void configureShipP2(TitleShip3D& ship); + }; + +} // namespace Title