diff --git a/data/shapes/ship2_perspective.shp b/data/shapes/ship2_perspective.shp deleted file mode 100644 index e32ca3e..0000000 --- a/data/shapes/ship2_perspective.shp +++ /dev/null @@ -1,28 +0,0 @@ -# ship2_perspective.shp - Nave P2 con perspectiva pre-calculada -# Posición optimizada: "4 del reloj" (Abajo-Derecha) -# Dirección: Volando hacia el fondo (centro pantalla) - -name: ship2_perspective -scale: 1.0 -center: 0, 0 - -# TRANSFORMACIÓN APLICADA: -# 1. Rotación -45° (apuntando al centro desde abajo-dcha) -# 2. Proyección de perspectiva: -# - Punta (p1): Reducida al 60% (simula lejanía) -# - Base (p2, p3): Aumentada al 110% (simula cercanía) -# 3. Flip horizontal (simétrica a ship_starfield.shp) -# -# Nuevos Punts (aprox): -# p1 (Punta): (-4, -4) -> Lejos, pequeña y apuntando arriba-izq -# p2 (Ala Izq): (-3, 11) -> Cerca, lado interior -# p4 (Base Cnt): (3, 5) -> Centro base -# p3 (Ala Dcha): (11, 2) -> Cerca, lado exterior (más grande) - -#polyline: -4,-4 -3,11 3,5 11,2 -4,-4 -polyline: -4,-4 -3,11 11,2 -4,-4 - -# Circulito central (octàgon r=2.5) -# Distintiu visual del jugador 2 -# Sin perspectiva (está en el centro de la nave) -polyline: 0,-2.5 1.77,-1.77 2.5,0 1.77,1.77 0,2.5 -1.77,1.77 -2.5,0 -1.77,-1.77 0,-2.5 diff --git a/data/shapes/ship_perspective.shp b/data/shapes/ship_perspective.shp deleted file mode 100644 index 6fe6502..0000000 --- a/data/shapes/ship_perspective.shp +++ /dev/null @@ -1,21 +0,0 @@ -# ship_perspective.shp - Nave con perspectiva pre-calculada -# Posición optimizada: "8 del reloj" (Abajo-Izquierda) -# Dirección: Volando hacia el fondo (centro pantalla) - -name: ship_perspective -scale: 1.0 -center: 0, 0 - -# TRANSFORMACIÓN APLICADA: -# 1. Rotación +45° (apuntando al centro desde abajo-izq) -# 2. Proyección de perspectiva: -# - Punta (p1): Reducida al 60% (simula lejanía) -# - Base (p2, p3): Aumentada al 110% (simula cercanía) -# -# Nuevos Puntos (aprox): -# p1 (Punta): (4, -4) -> Lejos, pequeña y apuntando arriba-dcha -# p2 (Ala Dcha): (3, 11) -> Cerca, lado interior -# p4 (Base Cnt): (-3, 5) -> Centro base -# p3 (Ala Izq): (-11, 2) -> Cerca, lado exterior (más grande) - -polyline: 4,-4 3,11 -3,5 -11,2 4,-4 \ No newline at end of file diff --git a/source/core/graphics/starfield.cpp b/source/core/graphics/starfield.cpp index 01e5623..b6babe2 100644 --- a/source/core/graphics/starfield.cpp +++ b/source/core/graphics/starfield.cpp @@ -1,168 +1,105 @@ -// starfield.cpp - Implementació del sistema de estrelles de fons +// starfield.cpp - Implementació del starfield 3D // © 2026 JailDesigner #include "core/graphics/starfield.hpp" +#include #include #include -#include #include "core/defaults.hpp" -#include "core/graphics/shape_loader.hpp" -#include "core/rendering/shape_renderer.hpp" namespace Graphics { -// Constructor -Starfield::Starfield(Rendering::Renderer* renderer, - const Vec2& punt_fuga, - const SDL_FRect& area, - int densitat) - : shape_estrella_(ShapeLoader::load("star.shp")), - renderer_(renderer), - punt_fuga_(punt_fuga), - area_(area) { - if (!shape_estrella_ || !shape_estrella_->isValid()) { - std::cerr << "ERROR: No s'ha pogut load star.shp" << '\n'; - return; - } + namespace { - // Configurar 3 capes con diferents velocitats i escales - // Capa 0: Fons llunyà (lenta, pequeña) - capes_.push_back({20.0F, 0.3F, 0.8F, densitat / 3}); + // Helper: número aleatori en [0, 1) usant rand()/RAND_MAX (mateixa convenció + // que la resta del joc — veure starfield.cpp). + auto randFloat01() -> float { + return static_cast(rand()) / static_cast(RAND_MAX); + } - // Capa 1: Profunditat mitjana - capes_.push_back({40.0F, 0.5F, 1.2F, densitat / 3}); + auto randRange(float lo, float hi) -> float { + return lo + (randFloat01() * (hi - lo)); + } - // Capa 2: Primer pla (ràpida, grande) - capes_.push_back({80.0F, 0.8F, 2.0F, densitat / 3}); + } // namespace - // Calcular radi màxim (distancia del centro al racó més llunyà) - float dx = std::max(punt_fuga_.x, area_.w - punt_fuga_.x); - float dy = std::max(punt_fuga_.y, area_.h - punt_fuga_.y); - radi_max_ = std::sqrt((dx * dx) + (dy * dy)); - - // Inicialitzar estrelles con posicions distribuïdes (pre-omplir pantalla) - for (int capa_idx = 0; capa_idx < 3; capa_idx++) { - int num = capes_[capa_idx].num_estrelles; - for (int i = 0; i < num; i++) { - Estrella estrella; - estrella.capa = capa_idx; - - // Angle aleatori - estrella.angle = (static_cast(rand()) / static_cast(RAND_MAX)) * 2.0F * Defaults::Math::PI; - - // Distancia aleatòria (0.0 a 1.0) per omplir toda la pantalla - estrella.distancia_centre = static_cast(rand()) / static_cast(RAND_MAX); - - // Calcular posición desde la distancia - float radi = estrella.distancia_centre * radi_max_; - estrella.position.x = punt_fuga_.x + (radi * std::cos(estrella.angle)); - estrella.position.y = punt_fuga_.y + (radi * std::sin(estrella.angle)); - - estrelles_.push_back(estrella); + Starfield::Starfield(Rendering::Renderer* renderer, const Camera3D* camera, int density) + : renderer_(renderer), + camera_(camera), + octahedron_(makeOctahedron()) { + stars_.resize(static_cast(std::max(0, density))); + for (auto& star : stars_) { + // Pre-omplir amb estrelles distribuïdes per tot el rang Z, no només al far. + initStar(star, /*spawn_at_far=*/false); } } -} -// Inicialitzar una estrella (nueva o regenerada) -void Starfield::initStar(Estrella& estrella) const { - // Angle aleatori des del point de fuga hacia fuera - estrella.angle = (static_cast(rand()) / static_cast(RAND_MAX)) * 2.0F * Defaults::Math::PI; + void Starfield::initStar(Star& star, bool spawn_at_far) { + star.position.x = randRange(-HALF_SPAWN_X, HALF_SPAWN_X); + star.position.y = randRange(-HALF_SPAWN_Y, HALF_SPAWN_Y); + star.position.z = spawn_at_far + ? Z_FAR_SPAWN + : randRange(Z_NEAR_RESPAWN, Z_FAR_SPAWN); - // Distancia inicial pequeña (5% del radi màxim) - neix prop del centro - estrella.distancia_centre = 0.05F; + star.velocity_z = -randRange(MIN_VELOCITY_Z, MAX_VELOCITY_Z); + star.rot_phase_y = randFloat01() * Defaults::Math::PI * 2.0F; + star.rot_phase_x = randFloat01() * Defaults::Math::PI * 2.0F; + star.rot_speed_y = randRange(MIN_ROT_SPEED, MAX_ROT_SPEED); + star.rot_speed_x = randRange(MIN_ROT_SPEED, MAX_ROT_SPEED); + star.scale = STAR_BASE_SCALE + (randRange(-1.0F, 1.0F) * STAR_SCALE_JITTER); + } - // Posición inicial: mucho prop del point de fuga - float radi = estrella.distancia_centre * radi_max_; - estrella.position.x = punt_fuga_.x + (radi * std::cos(estrella.angle)); - estrella.position.y = punt_fuga_.y + (radi * std::sin(estrella.angle)); -} + auto Starfield::computeBrightness(const Star& star) const -> float { + // Lerp segons distància Z normalitzada [Z_NEAR_RESPAWN .. Z_FAR_SPAWN]. + const float SPAN = Z_FAR_SPAWN - Z_NEAR_RESPAWN; + const float T_RAW = (star.position.z - Z_NEAR_RESPAWN) / SPAN; + const float T = std::clamp(T_RAW, 0.0F, 1.0F); + const float BRIGHTNESS = BRIGHTNESS_NEAR + (T * (BRIGHTNESS_FAR - BRIGHTNESS_NEAR)); + return std::clamp(BRIGHTNESS * brightness_mult_, 0.0F, 1.0F); + } -// Verificar si una estrella está fuera de l'àrea -auto Starfield::isOutsideArea(const Estrella& estrella) const -> bool { - return (estrella.position.x < area_.x || - estrella.position.x > area_.x + area_.w || - estrella.position.y < area_.y || - estrella.position.y > area_.y + area_.h); -} + void Starfield::update(float delta_time) { + for (auto& star : stars_) { + star.position.z += star.velocity_z * delta_time; + star.rot_phase_y += star.rot_speed_y * delta_time; + star.rot_phase_x += star.rot_speed_x * delta_time; -// Calcular scale dinàmica segons distancia del centro -auto Starfield::computeScale(const Estrella& estrella) const -> float { - const CapaConfig& capa = capes_[estrella.capa]; - - // Interpolació lineal basada en distancia del centro - // distancia_centre: 0.0 (centro) → 1.0 (vora) - return capa.escala_min + - ((capa.escala_max - capa.escala_min) * estrella.distancia_centre); -} - -// Calcular brightness dinàmica segons distancia del centro -auto Starfield::computeBrightness(const Estrella& estrella) const -> float { - // Interpolació lineal: estrelles properes (vora) més brillants - // distancia_centre: 0.0 (centro, llunyanes) → 1.0 (vora, properes) - float brightness_base = Defaults::Brightness::STARFIELD_MIN + - ((Defaults::Brightness::STARFIELD_MAX - Defaults::Brightness::STARFIELD_MIN) * - estrella.distancia_centre); - - // Aplicar multiplicador i limitar a 1.0 - return std::min(1.0F, brightness_base * multiplicador_brightness_); -} - -// Actualitzar posicions de las estrelles -void Starfield::update(float delta_time) { - for (auto& estrella : estrelles_) { - // Obtenir configuración de la capa - const CapaConfig& capa = capes_[estrella.capa]; - - // Moure hacia fuera des del centro - float velocity = capa.velocitat_base; - float dx = velocity * std::cos(estrella.angle) * delta_time; - float dy = velocity * std::sin(estrella.angle) * delta_time; - - estrella.position.x += dx; - estrella.position.y += dy; - - // Actualitzar distancia del centro - float dx_centre = estrella.position.x - punt_fuga_.x; - float dy_centre = estrella.position.y - punt_fuga_.y; - float dist_px = std::sqrt((dx_centre * dx_centre) + (dy_centre * dy_centre)); - estrella.distancia_centre = dist_px / radi_max_; - - // Si ha sortit de l'àrea, regenerar-la - if (isOutsideArea(estrella)) { - initStar(estrella); + if (star.position.z < Z_NEAR_RESPAWN) { + initStar(star, /*spawn_at_far=*/true); + } } } -} -// Establir multiplicador de brightness -void Starfield::setBrightness(float multiplier) { - multiplicador_brightness_ = std::max(0.0F, multiplier); // Evitar valors negatius -} + void Starfield::draw() const { + if (camera_ == nullptr || renderer_ == nullptr) { + return; + } + // Ordena de més lluny a més a prop perquè el blending additiu acumule + // brightness sense que els elements de davant queden tapats pels de + // darrere. Còpia d'índexs per no modificar l'ordre intern de stars_. + std::vector order(stars_.size()); + for (std::size_t i = 0; i < order.size(); ++i) { + order[i] = i; + } + std::ranges::sort(order, [&](std::size_t a, std::size_t b) { + return stars_[a].position.z > stars_[b].position.z; + }); -// Dibuixar todas las estrelles -void Starfield::draw() { - if (!shape_estrella_->isValid()) { - return; + for (std::size_t idx : order) { + const Star& star = stars_[idx]; + const Transform3D TRANSFORM{ + .position = star.position, + .rotation_euler = Vec3{.x = star.rot_phase_x, .y = star.rot_phase_y, .z = 0.0F}, + .scale = star.scale, + }; + drawWireframe(renderer_, *camera_, octahedron_, TRANSFORM, computeBrightness(star)); + } } - for (const auto& estrella : estrelles_) { - // Calcular scale i brightness dinàmicament - float scale = computeScale(estrella); - float brightness = computeBrightness(estrella); - - // Renderizar estrella sin rotación - Rendering::renderShape( - renderer_, - shape_estrella_, - estrella.position, - 0.0F, // angle (las estrelles no giren) - scale, // scale dinàmica - 1.0F, // progress (siempre visible) - brightness // brightness dinàmica - ); + void Starfield::setBrightness(float multiplier) { + brightness_mult_ = std::max(0.0F, multiplier); } -} } // namespace Graphics diff --git a/source/core/graphics/starfield.hpp b/source/core/graphics/starfield.hpp index de18ecf..012ac7a 100644 --- a/source/core/graphics/starfield.hpp +++ b/source/core/graphics/starfield.hpp @@ -1,83 +1,68 @@ -// starfield.hpp - Sistema de estrelles de fons con efecte de profunditat +// starfield.hpp - Camp d'estrelles 3D per a l'escena de títol // © 2026 JailDesigner +// +// Cada estrella és un octaedre +// wireframe situat en l'espai mundial (Vec3). Es desplacen cap a la càmera +// (Z disminueix); quan creuen el pla Z_NEAR_RESPAWN es regeneren a Z_FAR_SPAWN +// amb X/Y aleatori. Cada octaedre rota lentament sobre Y i X per donar volum. #pragma once -#include "core/rendering/render_context.hpp" - -#include - -#include #include -#include "core/graphics/shape.hpp" +#include "core/graphics/camera3d.hpp" +#include "core/graphics/wireframe3d.hpp" +#include "core/rendering/render_context.hpp" #include "core/types.hpp" namespace Graphics { -// Configuración per cada capa de profunditat -struct CapaConfig { - float velocitat_base; // Velocidad base de esta capa (px/s) - float escala_min; // Escala mínima prop del centro - float escala_max; // Escala màxima al límit de pantalla - int num_estrelles; // Nombre de estrelles en esta capa -}; + class Starfield { + public: + Starfield(Rendering::Renderer* renderer, const Camera3D* camera, int density = 200); -// Clase Starfield - camp de estrelles animat con efecte de profunditat -class Starfield { - public: - // Constructor - // - renderer: SDL renderer - // - punt_fuga: point de origin/fuga des de on surten las estrelles - // - area: rectangle on actuen las estrelles (SDL_FRect) - // - densitat: nombre total de estrelles (es divideix entre capes) - Starfield(Rendering::Renderer* renderer, - const Vec2& punt_fuga, - const SDL_FRect& area, - int densitat = 150); - - // Actualitzar posicions de las estrelles void update(float delta_time); + void draw() const; - // Dibuixar todas las estrelles - void draw(); - - // Setters per ajustar parámetros en time real - void setVanishingPoint(const Vec2& point) { punt_fuga_ = point; } void setBrightness(float multiplier); - private: - // Estructura interna per cada estrella - struct Estrella { - Vec2 position; // Posición actual - float angle; // Angle de movement (radians) - float distancia_centre; // Distancia normalitzada del centro (0.0-1.0) - int capa; // Índex de capa (0=lluny, 1=mitjà, 2=prop) + private: + struct Star { + Vec3 position{}; + float velocity_z{0.0F}; // Negatiu: cap a càmera + float rot_phase_y{0.0F}; + float rot_phase_x{0.0F}; + float rot_speed_y{0.0F}; + float rot_speed_x{0.0F}; + float scale{1.0F}; }; - // Inicialitzar una estrella (nueva o regenerada) - void initStar(Estrella& estrella) const; + static void initStar(Star& star, bool spawn_at_far); + [[nodiscard]] auto computeBrightness(const Star& star) const -> float; - // Verificar si una estrella está fuera de l'àrea - [[nodiscard]] auto isOutsideArea(const Estrella& estrella) const -> bool; - - // Calcular scale dinàmica segons distancia del centro - [[nodiscard]] auto computeScale(const Estrella& estrella) const -> float; - - // Calcular brightness dinàmica segons distancia del centro - [[nodiscard]] auto computeBrightness(const Estrella& estrella) const -> float; - - // Dades - std::vector estrelles_; - std::vector capes_; // Configuración de las 3 capes - std::shared_ptr shape_estrella_; Rendering::Renderer* renderer_; + const Camera3D* camera_; + std::vector stars_; + Mesh3D octahedron_; + float brightness_mult_{1.0F}; - // Configuración - Vec2 punt_fuga_; // Vec2 de origin de las estrelles - SDL_FRect area_; // Àrea activa - float radi_max_; // Distancia màxima del centro al límit de pantalla - float multiplicador_brightness_{1.0F}; // Multiplicador de brightness (1.0 = default) -}; + // Volum de spawn / regeneració en l'espai 3D. + static constexpr float Z_NEAR_RESPAWN = 5.0F; // Si Z < aquest valor → regenera + static constexpr float Z_FAR_SPAWN = 1500.0F; // Z de regeneració (lluny — més profunditat) + static constexpr float HALF_SPAWN_X = 900.0F; // X aleatori dins [-, +] + static constexpr float HALF_SPAWN_Y = 540.0F; // Y aleatori dins [-, +] + + // Mida i moviment. + static constexpr float STAR_BASE_SCALE = 1.8F; + static constexpr float STAR_SCALE_JITTER = 0.6F; + static constexpr float MIN_VELOCITY_Z = 80.0F; + static constexpr float MAX_VELOCITY_Z = 200.0F; + static constexpr float MIN_ROT_SPEED = 0.2F; + static constexpr float MAX_ROT_SPEED = 0.8F; + + // Brightness en funció de la distància Z (a prop = més brillant). + static constexpr float BRIGHTNESS_FAR = 0.15F; + static constexpr float BRIGHTNESS_NEAR = 1.0F; + }; } // namespace Graphics diff --git a/source/core/graphics/starfield3d.cpp b/source/core/graphics/starfield3d.cpp deleted file mode 100644 index 2923ffa..0000000 --- a/source/core/graphics/starfield3d.cpp +++ /dev/null @@ -1,105 +0,0 @@ -// starfield3d.cpp - Implementació del starfield 3D -// © 2026 JailDesigner - -#include "core/graphics/starfield3d.hpp" - -#include -#include -#include - -#include "core/defaults.hpp" - -namespace Graphics { - - namespace { - - // Helper: número aleatori en [0, 1) usant rand()/RAND_MAX (mateixa convenció - // que la resta del joc — veure starfield.cpp). - auto randFloat01() -> float { - return static_cast(rand()) / static_cast(RAND_MAX); - } - - auto randRange(float lo, float hi) -> float { - return lo + (randFloat01() * (hi - lo)); - } - - } // namespace - - Starfield3D::Starfield3D(Rendering::Renderer* renderer, const Camera3D* camera, int density) - : renderer_(renderer), - camera_(camera), - octahedron_(makeOctahedron()) { - stars_.resize(static_cast(std::max(0, density))); - for (auto& star : stars_) { - // Pre-omplir amb estrelles distribuïdes per tot el rang Z, no només al far. - initStar(star, /*spawn_at_far=*/false); - } - } - - void Starfield3D::initStar(Star& star, bool spawn_at_far) { - star.position.x = randRange(-HALF_SPAWN_X, HALF_SPAWN_X); - star.position.y = randRange(-HALF_SPAWN_Y, HALF_SPAWN_Y); - star.position.z = spawn_at_far - ? Z_FAR_SPAWN - : randRange(Z_NEAR_RESPAWN, Z_FAR_SPAWN); - - star.velocity_z = -randRange(MIN_VELOCITY_Z, MAX_VELOCITY_Z); - star.rot_phase_y = randFloat01() * Defaults::Math::PI * 2.0F; - star.rot_phase_x = randFloat01() * Defaults::Math::PI * 2.0F; - star.rot_speed_y = randRange(MIN_ROT_SPEED, MAX_ROT_SPEED); - star.rot_speed_x = randRange(MIN_ROT_SPEED, MAX_ROT_SPEED); - star.scale = STAR_BASE_SCALE + (randRange(-1.0F, 1.0F) * STAR_SCALE_JITTER); - } - - auto Starfield3D::computeBrightness(const Star& star) const -> float { - // Lerp segons distància Z normalitzada [Z_NEAR_RESPAWN .. Z_FAR_SPAWN]. - const float SPAN = Z_FAR_SPAWN - Z_NEAR_RESPAWN; - const float T_RAW = (star.position.z - Z_NEAR_RESPAWN) / SPAN; - const float T = std::clamp(T_RAW, 0.0F, 1.0F); - const float BRIGHTNESS = BRIGHTNESS_NEAR + (T * (BRIGHTNESS_FAR - BRIGHTNESS_NEAR)); - return std::clamp(BRIGHTNESS * brightness_mult_, 0.0F, 1.0F); - } - - void Starfield3D::update(float delta_time) { - for (auto& star : stars_) { - star.position.z += star.velocity_z * delta_time; - star.rot_phase_y += star.rot_speed_y * delta_time; - star.rot_phase_x += star.rot_speed_x * delta_time; - - if (star.position.z < Z_NEAR_RESPAWN) { - initStar(star, /*spawn_at_far=*/true); - } - } - } - - void Starfield3D::draw() const { - if (camera_ == nullptr || renderer_ == nullptr) { - return; - } - // Ordena de més lluny a més a prop perquè el blending additiu acumule - // brightness sense que els elements de davant queden tapats pels de - // darrere. Còpia d'índexs per no modificar l'ordre intern de stars_. - std::vector order(stars_.size()); - for (std::size_t i = 0; i < order.size(); ++i) { - order[i] = i; - } - std::ranges::sort(order, [&](std::size_t a, std::size_t b) { - return stars_[a].position.z > stars_[b].position.z; - }); - - for (std::size_t idx : order) { - const Star& star = stars_[idx]; - const Transform3D TRANSFORM{ - .position = star.position, - .rotation_euler = Vec3{.x = star.rot_phase_x, .y = star.rot_phase_y, .z = 0.0F}, - .scale = star.scale, - }; - drawWireframe(renderer_, *camera_, octahedron_, TRANSFORM, computeBrightness(star)); - } - } - - void Starfield3D::setBrightness(float multiplier) { - brightness_mult_ = std::max(0.0F, multiplier); - } - -} // namespace Graphics diff --git a/source/core/graphics/starfield3d.hpp b/source/core/graphics/starfield3d.hpp deleted file mode 100644 index 5f7b2fc..0000000 --- a/source/core/graphics/starfield3d.hpp +++ /dev/null @@ -1,68 +0,0 @@ -// starfield3d.hpp - Camp de estrelles 3D real per a l'escena de títol -// © 2026 JailDesigner -// -// Equivalent 3D del `Graphics::Starfield`. Cada estrella és un octaedre -// wireframe situat en l'espai mundial (Vec3). Es desplacen cap a la càmera -// (Z disminueix); quan creuen el pla Z_NEAR_RESPAWN es regeneren a Z_FAR_SPAWN -// amb X/Y aleatori. Cada octaedre rota lentament sobre Y i X per donar volum. - -#pragma once - -#include - -#include "core/graphics/camera3d.hpp" -#include "core/graphics/wireframe3d.hpp" -#include "core/rendering/render_context.hpp" -#include "core/types.hpp" - -namespace Graphics { - - class Starfield3D { - public: - Starfield3D(Rendering::Renderer* renderer, const Camera3D* camera, int density = 200); - - void update(float delta_time); - void draw() const; - - void setBrightness(float multiplier); - - private: - struct Star { - Vec3 position{}; - float velocity_z{0.0F}; // Negatiu: cap a càmera - float rot_phase_y{0.0F}; - float rot_phase_x{0.0F}; - float rot_speed_y{0.0F}; - float rot_speed_x{0.0F}; - float scale{1.0F}; - }; - - static void initStar(Star& star, bool spawn_at_far); - [[nodiscard]] auto computeBrightness(const Star& star) const -> float; - - Rendering::Renderer* renderer_; - const Camera3D* camera_; - std::vector stars_; - Mesh3D octahedron_; - float brightness_mult_{1.0F}; - - // Volum de spawn / regeneració en l'espai 3D. - static constexpr float Z_NEAR_RESPAWN = 5.0F; // Si Z < aquest valor → regenera - static constexpr float Z_FAR_SPAWN = 1500.0F; // Z de regeneració (lluny — més profunditat) - static constexpr float HALF_SPAWN_X = 900.0F; // X aleatori dins [-, +] - static constexpr float HALF_SPAWN_Y = 540.0F; // Y aleatori dins [-, +] - - // Mida i moviment. - static constexpr float STAR_BASE_SCALE = 1.8F; - static constexpr float STAR_SCALE_JITTER = 0.6F; - static constexpr float MIN_VELOCITY_Z = 80.0F; - static constexpr float MAX_VELOCITY_Z = 200.0F; - static constexpr float MIN_ROT_SPEED = 0.2F; - static constexpr float MAX_ROT_SPEED = 0.8F; - - // Brightness en funció de la distància Z (a prop = més brillant). - static constexpr float BRIGHTNESS_FAR = 0.15F; - static constexpr float BRIGHTNESS_NEAR = 1.0F; - }; - -} // namespace Graphics diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index d4a1193..4eea305 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -24,7 +24,6 @@ #include "game/scenes/game_scene.hpp" #include "game/scenes/logo_scene.hpp" #include "game/scenes/title_scene.hpp" -#include "game/scenes/title_scene_3d.hpp" #include "global_events.hpp" #include "project.h" #include "scene.hpp" @@ -292,17 +291,8 @@ auto Director::buildScene(SceneType type, SDLManager& sdl, SceneContext& context switch (type) { case SceneType::LOGO: return std::make_unique(sdl, context); - case SceneType::TITLE: { - // Env var ORNI_TITLE_3D=1 redirigeix la TITLE clàssica cap a la - // variant 3D real en proves; en qualsevol altre cas, la 2D. - const char* env = std::getenv("ORNI_TITLE_3D"); - if (env != nullptr && env[0] == '1' && env[1] == '\0') { - return std::make_unique(sdl, context); - } + case SceneType::TITLE: return std::make_unique(sdl, context); - } - case SceneType::TITLE_3D: - return std::make_unique(sdl, context); case SceneType::GAME: return std::make_unique(sdl, context); case SceneType::EXIT: diff --git a/source/core/system/scene_context.hpp b/source/core/system/scene_context.hpp index 7d8377d..7cf3c30 100644 --- a/source/core/system/scene_context.hpp +++ b/source/core/system/scene_context.hpp @@ -15,13 +15,10 @@ namespace SceneManager { public: // Tipo de escena del juego enum class SceneType : std::uint8_t { - LOGO, // Pantalla de start (logo JAILGAMES) - TITLE, // Pantalla de título (versió 2D actual). Si l'env var - // ORNI_TITLE_3D=1 està activa, Director::buildScene - // redirigeix aquest valor a TitleScene3D. - TITLE_3D, // Pantalla de títol 3D real (variant en proves) - GAME, // Juego principal (Asteroids) - EXIT // Salir del programa + LOGO, // Pantalla de start (logo JAILGAMES) + TITLE, // Pantalla de título (3D) + GAME, // Juego principal (Asteroids) + EXIT // Salir del programa }; // Opciones específiques para cada escena diff --git a/source/game/scenes/title_scene.cpp b/source/game/scenes/title_scene.cpp index 1c76878..0a04f51 100644 --- a/source/game/scenes/title_scene.cpp +++ b/source/game/scenes/title_scene.cpp @@ -1,4 +1,4 @@ -// title_scene.cpp - Implementació de l'escena de título +// title_scene.cpp - Implementació de l'escena de títol 3D real // © 2026 JailDesigner #include "title_scene.hpp" @@ -18,115 +18,96 @@ #include "core/system/scene_context.hpp" #include "project.h" -// Using declarations per simplificar el codi using SceneManager::SceneContext; using SceneType = SceneContext::SceneType; using Option = SceneContext::Option; +namespace { + + // Botons per iniciar partida des de MAIN (només START). Duplicat del que viu + + constexpr std::array START_GAME_BUTTONS = {InputAction::START}; + +} // namespace + TitleScene::TitleScene(SDLManager& sdl, SceneContext& context) : sdl_(sdl), context_(context), - text_(sdl.getRenderer()) - { + text_(sdl.getRenderer()) { std::cout << "SceneType Titol: Inicialitzant...\n"; - // Inicialitzar configuración de match (sin player active per defecte) match_config_.jugador1_actiu = false; match_config_.jugador2_actiu = false; match_config_.mode = GameConfig::Mode::NORMAL; - // Processar opción del context auto option = context_.consumeOption(); - if (option == Option::JUMP_TO_TITLE_MAIN) { - std::cout << "SceneType Titol: Opción JUMP_TO_TITLE_MAIN activada\n"; + std::cout << "SceneType Titol: Opció JUMP_TO_TITLE_MAIN activada\n"; estat_actual_ = TitleState::MAIN; temps_estat_main_ = 0.0F; } - // Crear starfield de fons - Vec2 centre_pantalla{ - .x = Defaults::Game::WIDTH / 2.0F, - .y = Defaults::Game::HEIGHT / 2.0F}; - - SDL_FRect area_completa{ - 0, - 0, + // Càmera 3D: posicionada a l'origen, mirant cap a +Z, amb Y cap amunt. + camera_ = std::make_unique( + Vec3{.x = 0.0F, .y = 0.0F, .z = 0.0F}, + Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F}, + Vec3{.x = 0.0F, .y = 1.0F, .z = 0.0F}, + CAMERA_FOV_Y_RAD, static_cast(Defaults::Game::WIDTH), - static_cast(Defaults::Game::HEIGHT)}; + static_cast(Defaults::Game::HEIGHT)); starfield_ = std::make_unique( sdl_.getRenderer(), - centre_pantalla, - area_completa, - 150 // densitat: 150 estrelles (50 per capa) - ); - - // Brightness depèn de l'opción + camera_.get(), + 200); if (estat_actual_ == TitleState::MAIN) { - // Si saltem a MAIN, starfield instantàniament brillant starfield_->setBrightness(BRIGHTNESS_STARFIELD); } else { - // Flux normal: comença con brightness 0.0 per fade-in starfield_->setBrightness(0.0F); } - // Inicialitzar animador de naves 3D - ship_animator_ = std::make_unique(sdl_.getRenderer()); + ship_animator_ = std::make_unique(sdl_.getRenderer(), camera_.get()); ship_animator_->init(); if (estat_actual_ == TitleState::MAIN) { - // Jump to MAIN: empezar entrada inmediatamente ship_animator_->setVisible(true); ship_animator_->startEntryAnimation(); } else { - // Flux normal: NO empezar entrada todavía (esperaran a MAIN) ship_animator_->setVisible(false); } - // Inicialitzar lletres del título "ORNI ATTACK!" initTitle(); - - // Logo JAILGAMES pequeño sobre el copyright inferior. inicialitzarJailgames(); - // Iniciar música de título si no está sonant if (Audio::getMusicState() != Audio::MusicState::PLAYING) { Audio::get()->playMusic("title.ogg"); } } TitleScene::~TitleScene() { - // Aturar música de título cuando es destrueix l'escena Audio::get()->stopMusic(); } void TitleScene::initTitle() { using namespace Graphics; - // === LÍNIA 1: "ORNI" === - std::vector fitxers_orni = { + const std::vector FITXERS_ORNI = { "title/letra_o.shp", "title/letra_r.shp", "title/letra_n.shp", "title/letra_i.shp"}; - // Pas 1: Carregar formes i calcular amplades per "ORNI" float ancho_total_orni = 0.0F; - - for (const auto& file : fitxers_orni) { + for (const auto& file : FITXERS_ORNI) { auto shape = ShapeLoader::load(file); if (!shape || !shape->isValid()) { std::cerr << "[TitleScene] Error carregant " << file << '\n'; continue; } - - // Calcular bounding box de la shape (trobar ancho i altura) float min_x = FLT_MAX; float max_x = -FLT_MAX; float min_y = FLT_MAX; float max_y = -FLT_MAX; - for (const auto& prim : shape->getPrimitives()) { for (const auto& point : prim.points) { min_x = std::min(min_x, point.x); @@ -135,72 +116,46 @@ void TitleScene::initTitle() { max_y = std::max(max_y, point.y); } } - - float ancho_sin_escalar = max_x - min_x; - float altura_sin_escalar = max_y - min_y; - - // Escalar ancho, altura i offset con LOGO_SCALE - float ancho = ancho_sin_escalar * Defaults::Title::Layout::LOGO_SCALE; - float altura = altura_sin_escalar * Defaults::Title::Layout::LOGO_SCALE; - float offset_centre = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE; - - lletres_orni_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ancho, altura, offset_centre}); - - ancho_total_orni += ancho; + const float ANCHO = (max_x - min_x) * Defaults::Title::Layout::LOGO_SCALE; + const float ALTURA = (max_y - min_y) * Defaults::Title::Layout::LOGO_SCALE; + const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE; + lletres_orni_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ANCHO, ALTURA, OFFSET_CENTRE}); + ancho_total_orni += ANCHO; } + ancho_total_orni += ESPAI_ENTRE_LLETRES * static_cast(lletres_orni_.size() - 1); - // Añadir espaiat entre lletres - ancho_total_orni += ESPAI_ENTRE_LLETRES * (lletres_orni_.size() - 1); - - // Calcular posición inicial (centrat horitzontal) per "ORNI" - float x_inicial_orni = (Defaults::Game::WIDTH - ancho_total_orni) / 2.0F; - float x_actual = x_inicial_orni; - + float x_actual = (Defaults::Game::WIDTH - ancho_total_orni) / 2.0F; for (auto& lletra : lletres_orni_) { lletra.position.x = x_actual + lletra.offset_centre; lletra.position.y = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS; x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES; } - std::cout << "[TitleScene] Línia 1 (ORNI): " << lletres_orni_.size() - << " lletres, ancho total: " << ancho_total_orni << " px\n"; + const float ALTURA_ORNI = lletres_orni_.empty() ? 50.0F : lletres_orni_[0].altura; + const float Y_ORNI = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS; + const float SEPARACION = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_LINE_SPACING; + y_attack_dinamica_ = Y_ORNI + ALTURA_ORNI + SEPARACION; - // === Calcular posición Y dinàmica per "ATTACK!" === - // Todas las lletres ORNI tenen la misma altura, utilitzem la primera - float altura_orni = lletres_orni_.empty() ? 50.0F : lletres_orni_[0].altura; - float y_orni = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS; - float separacion_lineas = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_LINE_SPACING; - y_attack_dinamica_ = y_orni + altura_orni + separacion_lineas; - - std::cout << "[TitleScene] Altura ORNI: " << altura_orni - << " px, Y_ATTACK dinàmica: " << y_attack_dinamica_ << " px\n"; - - // === LÍNIA 2: "ATTACK!" === - std::vector fitxers_attack = { + const std::vector FITXERS_ATTACK = { "title/letra_a.shp", "title/letra_t.shp", - "title/letra_t.shp", // T repetida - "title/letra_a.shp", // A repetida + "title/letra_t.shp", + "title/letra_a.shp", "title/letra_c.shp", "title/letra_k.shp", "title/letra_exclamacion.shp"}; - // Pas 1: Carregar formes i calcular amplades per "ATTACK!" float ancho_total_attack = 0.0F; - - for (const auto& file : fitxers_attack) { + for (const auto& file : FITXERS_ATTACK) { auto shape = ShapeLoader::load(file); if (!shape || !shape->isValid()) { std::cerr << "[TitleScene] Error carregant " << file << '\n'; continue; } - - // Calcular bounding box de la shape (trobar ancho i altura) float min_x = FLT_MAX; float max_x = -FLT_MAX; float min_y = FLT_MAX; float max_y = -FLT_MAX; - for (const auto& prim : shape->getPrimitives()) { for (const auto& point : prim.points) { min_x = std::min(min_x, point.x); @@ -209,54 +164,34 @@ void TitleScene::initTitle() { max_y = std::max(max_y, point.y); } } - - float ancho_sin_escalar = max_x - min_x; - float altura_sin_escalar = max_y - min_y; - - // Escalar ancho, altura i offset con LOGO_SCALE - float ancho = ancho_sin_escalar * Defaults::Title::Layout::LOGO_SCALE; - float altura = altura_sin_escalar * Defaults::Title::Layout::LOGO_SCALE; - float offset_centre = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE; - - lletres_attack_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ancho, altura, offset_centre}); - - ancho_total_attack += ancho; + const float ANCHO = (max_x - min_x) * Defaults::Title::Layout::LOGO_SCALE; + const float ALTURA = (max_y - min_y) * Defaults::Title::Layout::LOGO_SCALE; + const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE; + lletres_attack_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ANCHO, ALTURA, OFFSET_CENTRE}); + ancho_total_attack += ANCHO; } + ancho_total_attack += ESPAI_ENTRE_LLETRES * static_cast(lletres_attack_.size() - 1); - // Añadir espaiat entre lletres - ancho_total_attack += ESPAI_ENTRE_LLETRES * (lletres_attack_.size() - 1); - - // Calcular posición inicial (centrat horitzontal) per "ATTACK!" - float x_inicial_attack = (Defaults::Game::WIDTH - ancho_total_attack) / 2.0F; - x_actual = x_inicial_attack; - + x_actual = (Defaults::Game::WIDTH - ancho_total_attack) / 2.0F; for (auto& lletra : lletres_attack_) { lletra.position.x = x_actual + lletra.offset_centre; - lletra.position.y = y_attack_dinamica_; // Usar posición dinàmica + lletra.position.y = y_attack_dinamica_; x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES; } - std::cout << "[TitleScene] Línia 2 (ATTACK!): " << lletres_attack_.size() - << " lletres, ancho total: " << ancho_total_attack << " px\n"; - - // Guardar posicions originals per l'animación orbital posicions_originals_orni_.clear(); for (const auto& lletra : lletres_orni_) { posicions_originals_orni_.push_back(lletra.position); } - posicions_originals_attack_.clear(); for (const auto& lletra : lletres_attack_) { posicions_originals_attack_.push_back(lletra.position); } - - std::cout << "[TitleScene] Animación: Posicions originals guardades\n"; } void TitleScene::inicialitzarJailgames() { using namespace Graphics; - // Mismas letras que la LogoScene, mismo orden (J-A-I-L-G-A-M-E-S). const std::vector FITXERS = { "logo/letra_j.shp", "logo/letra_a.shp", @@ -270,17 +205,14 @@ void TitleScene::inicialitzarJailgames() { constexpr float SCALE = Defaults::Title::Layout::JAILGAMES_SCALE; - // Pas 1: carregar formes i calcular amplada/altura escalades. float ancho_total = 0.0F; float altura_max = 0.0F; - for (const auto& file : FITXERS) { auto shape = ShapeLoader::load(file); if (!shape || !shape->isValid()) { std::cerr << "[TitleScene] Error carregant " << file << '\n'; continue; } - float min_x = FLT_MAX; float max_x = -FLT_MAX; float min_y = FLT_MAX; @@ -296,24 +228,18 @@ void TitleScene::inicialitzarJailgames() { const float ANCHO = (max_x - min_x) * SCALE; const float ALTURA = (max_y - min_y) * SCALE; const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * SCALE; - - lletres_jailgames_.push_back({shape, {.x = 0.0F, .y = 0.0F}, - ANCHO, ALTURA, OFFSET_CENTRE}); - + lletres_jailgames_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ANCHO, ALTURA, OFFSET_CENTRE}); ancho_total += ANCHO; altura_max = std::max(altura_max, ALTURA); } - - // Espaiat entre lletres (proporcional a la escala, para que no quede pegado). constexpr float ESPAI_JAILGAMES = ESPAI_ENTRE_LLETRES * SCALE; if (!lletres_jailgames_.empty()) { ancho_total += ESPAI_JAILGAMES * static_cast(lletres_jailgames_.size() - 1); } - // Pas 2: centrar horizontalmente y colocar JUST encima de la línea de copyright. - const float Y_COPYRIGHT = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS; + const float Y_COPY = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS; const float GAP = Defaults::Game::HEIGHT * Defaults::Title::Layout::JAILGAMES_COPYRIGHT_GAP; - const float Y_CENTRE = Y_COPYRIGHT - GAP - (altura_max / 2.0F); + const float Y_CENTRE = Y_COPY - GAP - (altura_max / 2.0F); const float X_INICIAL = (Defaults::Game::WIDTH - ancho_total) / 2.0F; float x_actual = X_INICIAL; @@ -325,15 +251,9 @@ void TitleScene::inicialitzarJailgames() { } void TitleScene::dibuixarPeuTitol(float spacing) const { - // Logo JAILGAMES pequeño sobre el copyright. for (const auto& lletra : lletres_jailgames_) { - Rendering::renderShape(sdl_.getRenderer(), lletra.shape, - lletra.position, 0.0F, - Defaults::Title::Layout::JAILGAMES_SCALE, - 1.0F); + Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::JAILGAMES_SCALE, 1.0F); } - - // Copyright en una sola línea, centrado, en mayúsculas. std::string copyright = Project::COPYRIGHT; for (char& c : copyright) { if (c >= 'a' && c <= 'z') { @@ -342,8 +262,7 @@ void TitleScene::dibuixarPeuTitol(float spacing) const { } const float Y_COPY = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS; const float CENTRE_X = Defaults::Game::WIDTH / 2.0F; - text_.renderCentered(copyright, {.x = CENTRE_X, .y = Y_COPY}, - Defaults::Title::Layout::COPYRIGHT_SCALE, spacing); + text_.renderCentered(copyright, {.x = CENTRE_X, .y = Y_COPY}, Defaults::Title::Layout::COPYRIGHT_SCALE, spacing); } auto TitleScene::isFinished() const -> bool { @@ -351,12 +270,9 @@ auto TitleScene::isFinished() const -> bool { } void TitleScene::update(float delta_time) { - // Actualitzar starfield (siempre active) if (starfield_) { starfield_->update(delta_time); } - - // Actualitzar naves (cuando visibles) if (ship_animator_ && (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD || @@ -389,19 +305,12 @@ void TitleScene::update(float delta_time) { void TitleScene::updateStarfieldFadeInState(float delta_time) { temps_acumulat_ += delta_time; - - // Calcular progrés del fade (0.0 → 1.0) - float progress = std::min(1.0F, temps_acumulat_ / DURACIO_FADE_IN); - - // Lerp brightness de 0.0 a BRIGHTNESS_STARFIELD - float brightness_actual = progress * BRIGHTNESS_STARFIELD; - starfield_->setBrightness(brightness_actual); - - // Transición a STARFIELD cuando el fade es completa + const float PROGRESS = std::min(1.0F, temps_acumulat_ / DURACIO_FADE_IN); + starfield_->setBrightness(PROGRESS * BRIGHTNESS_STARFIELD); if (temps_acumulat_ >= DURACIO_FADE_IN) { estat_actual_ = TitleState::STARFIELD; - temps_acumulat_ = 0.0F; // Reset timer per al següent state - starfield_->setBrightness(BRIGHTNESS_STARFIELD); // Assegurar value final + temps_acumulat_ = 0.0F; + starfield_->setBrightness(BRIGHTNESS_STARFIELD); } } @@ -409,147 +318,100 @@ void TitleScene::updateStarfieldState(float delta_time) { temps_acumulat_ += delta_time; if (temps_acumulat_ >= DURACIO_INIT) { estat_actual_ = TitleState::MAIN; - temps_estat_main_ = 0.0F; // Reset timer al entrar a MAIN - animacio_activa_ = false; // Comença estàtic - factor_lerp_ = 0.0F; // Sin animación aún - - // Naves esperaran ENTRANCE_DELAY antes de entrar (no start aquí) + temps_estat_main_ = 0.0F; + animacio_activa_ = false; + factor_lerp_ = 0.0F; } } void TitleScene::updateMainState(float delta_time) { temps_estat_main_ += delta_time; - - // Iniciar animación de entrada de naves después del delay if (temps_estat_main_ >= Defaults::Title::Ships::ENTRANCE_DELAY && ship_animator_ && !ship_animator_->isVisible()) { ship_animator_->setVisible(true); ship_animator_->startEntryAnimation(); } - // Fase 1: Estàtic (0-10s) if (temps_estat_main_ < DELAY_INICI_ANIMACIO) { factor_lerp_ = 0.0F; animacio_activa_ = false; - } - // Fase 2: Lerp (10-12s) - else if (temps_estat_main_ < DELAY_INICI_ANIMACIO + DURACIO_LERP) { - float temps_lerp = temps_estat_main_ - DELAY_INICI_ANIMACIO; - factor_lerp_ = temps_lerp / DURACIO_LERP; // 0.0 → 1.0 linealment + } else if (temps_estat_main_ < DELAY_INICI_ANIMACIO + DURACIO_LERP) { + const float TEMPS_LERP = temps_estat_main_ - DELAY_INICI_ANIMACIO; + factor_lerp_ = TEMPS_LERP / DURACIO_LERP; animacio_activa_ = true; - } - // Fase 3: Animación completa (12s+) - else { + } else { factor_lerp_ = 1.0F; animacio_activa_ = true; } - - // Actualitzar animación del logo updateLogoAnimation(delta_time); } void TitleScene::updatePlayerJoinPhaseState(float delta_time) { temps_acumulat_ += delta_time; - - // Continuar animación orbital durante la transición updateLogoAnimation(delta_time); - // [NOU] Continuar comprovant si l'altre player quiere unir-se durante la transición ("late join") - bool p1_actiu_abans = match_config_.jugador1_actiu; - bool p2_actiu_abans = match_config_.jugador2_actiu; + const bool P1_ABANS = match_config_.jugador1_actiu; + const bool P2_ABANS = match_config_.jugador2_actiu; if (checkStartGameButtonPressed()) { - // Updates match_config_ if pressed, logs are in the method context_.setMatchConfig(match_config_); - - // Trigger animación de salida per la ship que acaba de unir-se - triggerExitForJoinedPlayers(p1_actiu_abans, p2_actiu_abans, "late join - "); - - // Reproducir so de START cuando el segon player s'uneix + triggerExitForJoinedPlayers(P1_ABANS, P2_ABANS, "late join - "); Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME); - - // Reiniciar el timer per allargar el time de transición temps_acumulat_ = 0.0F; - - std::cout << "[TitleScene] Segon player s'ha unit - so i timer reiniciats\n"; } if (temps_acumulat_ >= DURACIO_TRANSITION) { - // Transición a pantalla negra estat_actual_ = TitleState::BLACK_SCREEN; temps_acumulat_ = 0.0F; - std::cout << "[TitleScene] Passant a BLACK_SCREEN\n"; } } void TitleScene::updateBlackScreenState(float delta_time) { temps_acumulat_ += delta_time; - - // No animation, no input checking - just wait if (temps_acumulat_ >= DURACIO_BLACK_SCREEN) { - // Transición a escena GAME (el Director detecta isFinished()). context_.setNextScene(SceneType::GAME); - std::cout << "[TitleScene] Canviant a escena GAME\n"; } } void TitleScene::handleSkipInput() { - // Verificar botones de skip (FIRE/THRUST/START) para saltar escenas ANTES de MAIN if (estat_actual_ != TitleState::STARFIELD_FADE_IN && estat_actual_ != TitleState::STARFIELD) { return; } if (!checkSkipButtonPressed()) { return; } - // Saltar a MAIN estat_actual_ = TitleState::MAIN; starfield_->setBrightness(BRIGHTNESS_STARFIELD); temps_estat_main_ = 0.0F; - // Naves esperaran ENTRANCE_DELAY antes de entrar (no start aquí) } void TitleScene::handleStartInput() { - // Verificar boton START para start match desde MAIN if (estat_actual_ != TitleState::MAIN) { return; } - - // Guardar state anterior per detectar qui ha premut START AQUEST frame - bool p1_actiu_abans = match_config_.jugador1_actiu; - bool p2_actiu_abans = match_config_.jugador2_actiu; + const bool P1_ABANS = match_config_.jugador1_actiu; + const bool P2_ABANS = match_config_.jugador2_actiu; if (!checkStartGameButtonPressed()) { return; } - // Si START es prem durante el delay (naves aún invisibles), saltar-las a FLOATING if (ship_animator_ && !ship_animator_->isVisible()) { ship_animator_->setVisible(true); ship_animator_->skipToFloatingState(); } - // Configurar match antes de canviar de escena context_.setMatchConfig(match_config_); - std::cout << "[TitleScene] Configuración de match - P1: " - << (match_config_.jugador1_actiu ? "ACTIU" : "INACTIU") - << ", P2: " - << (match_config_.jugador2_actiu ? "ACTIU" : "INACTIU") - << '\n'; - - // El setNextScene a GAME se hace al final de BLACK_SCREEN para no - // saltar la animación de salida (isFinished() lo recoge entonces). estat_actual_ = TitleState::PLAYER_JOIN_PHASE; temps_acumulat_ = 0.0F; - // Trigger animación de salida NOMÉS per las naves que han premut START - triggerExitForJoinedPlayers(p1_actiu_abans, p2_actiu_abans, ""); + triggerExitForJoinedPlayers(P1_ABANS, P2_ABANS, ""); Audio::get()->fadeOutMusic(MUSIC_FADE); Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME); } -void TitleScene::triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, - const char* log_prefix) { +void TitleScene::triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix) { if (ship_animator_ == nullptr) { return; } @@ -564,42 +426,30 @@ void TitleScene::triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_act } void TitleScene::updateLogoAnimation(float delta_time) { - // Solo calcular i aplicar offsets si l'animación está activa - if (animacio_activa_) { - // Acumular time escalat - temps_animacio_ += delta_time * factor_lerp_; + if (!animacio_activa_) { + return; + } + temps_animacio_ += delta_time * factor_lerp_; - // Usar amplituds i freqüències completes - float amplitude_x_actual = ORBIT_AMPLITUDE_X; - float amplitude_y_actual = ORBIT_AMPLITUDE_Y; - float frequency_x_actual = ORBIT_FREQUENCY_X; - float frequency_y_actual = ORBIT_FREQUENCY_Y; + const float TWO_PI = 2.0F * Defaults::Math::PI; + const float OFFSET_X = ORBIT_AMPLITUDE_X * std::sin(TWO_PI * ORBIT_FREQUENCY_X * temps_animacio_); + const float OFFSET_Y = ORBIT_AMPLITUDE_Y * std::sin((TWO_PI * ORBIT_FREQUENCY_Y * temps_animacio_) + ORBIT_PHASE_OFFSET); - // Calcular offset orbital - float offset_x = amplitude_x_actual * std::sin(2.0F * Defaults::Math::PI * frequency_x_actual * temps_animacio_); - float offset_y = amplitude_y_actual * std::sin((2.0F * Defaults::Math::PI * frequency_y_actual * temps_animacio_) + ORBIT_PHASE_OFFSET); - - // Aplicar offset a todas las lletres de "ORNI" - for (size_t i = 0; i < lletres_orni_.size(); ++i) { - lletres_orni_[i].position.x = posicions_originals_orni_[i].x + static_cast(std::round(offset_x)); - lletres_orni_[i].position.y = posicions_originals_orni_[i].y + static_cast(std::round(offset_y)); - } - - // Aplicar offset a todas las lletres de "ATTACK!" - for (size_t i = 0; i < lletres_attack_.size(); ++i) { - lletres_attack_[i].position.x = posicions_originals_attack_[i].x + static_cast(std::round(offset_x)); - lletres_attack_[i].position.y = posicions_originals_attack_[i].y + static_cast(std::round(offset_y)); - } + for (std::size_t i = 0; i < lletres_orni_.size(); ++i) { + lletres_orni_[i].position.x = posicions_originals_orni_[i].x + std::round(OFFSET_X); + lletres_orni_[i].position.y = posicions_originals_orni_[i].y + std::round(OFFSET_Y); + } + for (std::size_t i = 0; i < lletres_attack_.size(); ++i) { + lletres_attack_[i].position.x = posicions_originals_attack_[i].x + std::round(OFFSET_X); + lletres_attack_[i].position.y = posicions_originals_attack_[i].y + std::round(OFFSET_Y); } } void TitleScene::draw() { - // Dibuixar starfield de fons (en todos los estats excepte BLACK_SCREEN) if (starfield_ && estat_actual_ != TitleState::BLACK_SCREEN) { starfield_->draw(); } - // Dibuixar naves (después starfield, antes logo) if (ship_animator_ && (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD || @@ -608,116 +458,59 @@ void TitleScene::draw() { ship_animator_->draw(); } - // En los estats STARFIELD_FADE_IN i STARFIELD, solo mostrar starfield (sin text) if (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD) { return; } - // Estat MAIN i PLAYER_JOIN_PHASE: Dibuixar título i text (sobre el starfield) - // BLACK_SCREEN: no draw res (fons negre ya está netejat) - if (estat_actual_ == TitleState::MAIN || estat_actual_ == TitleState::PLAYER_JOIN_PHASE) { - // === Calcular i renderizar ombra (solo si animación activa) === - if (animacio_activa_) { - float temps_shadow = temps_animacio_ - SHADOW_DELAY; - temps_shadow = std::max(temps_shadow, 0.0F); // Evitar time negatiu - - // Usar amplituds i freqüències completes per l'ombra - float amplitude_x_shadow = ORBIT_AMPLITUDE_X; - float amplitude_y_shadow = ORBIT_AMPLITUDE_Y; - float frequency_x_shadow = ORBIT_FREQUENCY_X; - float frequency_y_shadow = ORBIT_FREQUENCY_Y; - - // Calcular offset de l'ombra - float shadow_offset_x = (amplitude_x_shadow * std::sin(2.0F * Defaults::Math::PI * frequency_x_shadow * temps_shadow)) + SHADOW_OFFSET_X; - float shadow_offset_y = (amplitude_y_shadow * std::sin((2.0F * Defaults::Math::PI * frequency_y_shadow * temps_shadow) + ORBIT_PHASE_OFFSET)) + SHADOW_OFFSET_Y; - - // === RENDERITZAR OMBRA PRIMER (darrera del logo principal) === - - // Ombra "ORNI" - for (size_t i = 0; i < lletres_orni_.size(); ++i) { - Vec2 pos_shadow; - pos_shadow.x = posicions_originals_orni_[i].x + static_cast(std::round(shadow_offset_x)); - pos_shadow.y = posicions_originals_orni_[i].y + static_cast(std::round(shadow_offset_y)); - - Rendering::renderShape( - sdl_.getRenderer(), - lletres_orni_[i].shape, - pos_shadow, - 0.0F, - Defaults::Title::Layout::LOGO_SCALE, - 1.0F, // progress = 1.0 (totalment visible) - SHADOW_BRIGHTNESS // brightness = 0.4 (brightness reduïda) - ); - } - - // Ombra "ATTACK!" - for (size_t i = 0; i < lletres_attack_.size(); ++i) { - Vec2 pos_shadow; - pos_shadow.x = posicions_originals_attack_[i].x + static_cast(std::round(shadow_offset_x)); - pos_shadow.y = posicions_originals_attack_[i].y + static_cast(std::round(shadow_offset_y)); - - Rendering::renderShape( - sdl_.getRenderer(), - lletres_attack_[i].shape, - pos_shadow, - 0.0F, - Defaults::Title::Layout::LOGO_SCALE, - 1.0F, // progress = 1.0 (totalment visible) - SHADOW_BRIGHTNESS); - } - } - - // === RENDERITZAR LOGO PRINCIPAL (damunt) === - - // Dibuixar "ORNI" (línia 1) - for (const auto& lletra : lletres_orni_) { - Rendering::renderShape( - sdl_.getRenderer(), - lletra.shape, - lletra.position, - 0.0F, - Defaults::Title::Layout::LOGO_SCALE, - 1.0F // Brillantor completa - ); - } - - // Dibuixar "ATTACK!" (línia 2) - for (const auto& lletra : lletres_attack_) { - Rendering::renderShape( - sdl_.getRenderer(), - lletra.shape, - lletra.position, - 0.0F, - Defaults::Title::Layout::LOGO_SCALE, - 1.0F // Brillantor completa - ); - } - - // === Text "PRESS START TO PLAY" === - // En state MAIN: siempre visible - // En state TRANSITION: parpellejant (blink con sinusoide) - - const float SPACING = Defaults::Title::Layout::TEXT_SPACING; - - bool mostrar_text = true; - if (estat_actual_ == TitleState::PLAYER_JOIN_PHASE) { - // Parpelleig: sin oscil·la entre -1 i 1, volem ON cuando > 0 - float fase = temps_acumulat_ * BLINK_FREQUENCY * 2.0F * std::numbers::pi_v; // 2π × freq × time - mostrar_text = (std::sin(fase) > 0.0F); - } - - if (mostrar_text) { - const std::string MAIN_TEXT = "PRESS START TO PLAY"; - const float MAIN_SCALE = Defaults::Title::Layout::PRESS_START_SCALE; - - float centre_x = Defaults::Game::WIDTH / 2.0F; - float centre_y = Defaults::Game::HEIGHT * Defaults::Title::Layout::PRESS_START_POS; - - text_.renderCentered(MAIN_TEXT, {.x = centre_x, .y = centre_y}, MAIN_SCALE, SPACING); - } - - dibuixarPeuTitol(SPACING); + if (estat_actual_ != TitleState::MAIN && estat_actual_ != TitleState::PLAYER_JOIN_PHASE) { + return; } + + if (animacio_activa_) { + float temps_shadow = std::max(0.0F, temps_animacio_ - SHADOW_DELAY); + const float TWO_PI = 2.0F * Defaults::Math::PI; + const float SHADOW_OX = (ORBIT_AMPLITUDE_X * std::sin(TWO_PI * ORBIT_FREQUENCY_X * temps_shadow)) + SHADOW_OFFSET_X; + const float SHADOW_OY = (ORBIT_AMPLITUDE_Y * std::sin((TWO_PI * ORBIT_FREQUENCY_Y * temps_shadow) + ORBIT_PHASE_OFFSET)) + SHADOW_OFFSET_Y; + + for (std::size_t i = 0; i < lletres_orni_.size(); ++i) { + const Vec2 POS_SHADOW{ + .x = posicions_originals_orni_[i].x + std::round(SHADOW_OX), + .y = posicions_originals_orni_[i].y + std::round(SHADOW_OY), + }; + Rendering::renderShape(sdl_.getRenderer(), lletres_orni_[i].shape, POS_SHADOW, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, SHADOW_BRIGHTNESS); + } + for (std::size_t i = 0; i < lletres_attack_.size(); ++i) { + const Vec2 POS_SHADOW{ + .x = posicions_originals_attack_[i].x + std::round(SHADOW_OX), + .y = posicions_originals_attack_[i].y + std::round(SHADOW_OY), + }; + Rendering::renderShape(sdl_.getRenderer(), lletres_attack_[i].shape, POS_SHADOW, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, SHADOW_BRIGHTNESS); + } + } + + for (const auto& lletra : lletres_orni_) { + Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F); + } + for (const auto& lletra : lletres_attack_) { + Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F); + } + + const float SPACING = Defaults::Title::Layout::TEXT_SPACING; + + bool mostrar_text = true; + if (estat_actual_ == TitleState::PLAYER_JOIN_PHASE) { + const float FASE = temps_acumulat_ * BLINK_FREQUENCY * 2.0F * std::numbers::pi_v; + mostrar_text = (std::sin(FASE) > 0.0F); + } + if (mostrar_text) { + const std::string MAIN_TEXT = "PRESS START TO PLAY"; + const float MAIN_SCALE = Defaults::Title::Layout::PRESS_START_SCALE; + const float CENTRE_X = Defaults::Game::WIDTH / 2.0F; + const float CENTRE_Y = Defaults::Game::HEIGHT * Defaults::Title::Layout::PRESS_START_POS; + text_.renderCentered(MAIN_TEXT, {.x = CENTRE_X, .y = CENTRE_Y}, MAIN_SCALE, SPACING); + } + + dibuixarPeuTitol(SPACING); } auto TitleScene::checkSkipButtonPressed() -> bool { @@ -727,29 +520,23 @@ auto TitleScene::checkSkipButtonPressed() -> bool { auto TitleScene::checkStartGameButtonPressed() -> bool { auto* input = Input::get(); bool any_pressed = false; - for (auto action : START_GAME_BUTTONS) { if (input->checkActionPlayer1(action, Input::DO_NOT_ALLOW_REPEAT)) { if (!match_config_.jugador1_actiu) { match_config_.jugador1_actiu = true; any_pressed = true; - std::cout << "[TitleScene] P1 pressed START\n"; } } if (input->checkActionPlayer2(action, Input::DO_NOT_ALLOW_REPEAT)) { if (!match_config_.jugador2_actiu) { match_config_.jugador2_actiu = true; any_pressed = true; - std::cout << "[TitleScene] P2 pressed START\n"; } } } - return any_pressed; } void TitleScene::handleEvent(const SDL_Event& event) { - // La lógica de input se decide en update() consultando Input::checkAction; - // aquí no hay eventos puntuales que procesar. (void)event; } diff --git a/source/game/scenes/title_scene.hpp b/source/game/scenes/title_scene.hpp index cc2efe7..82457ef 100644 --- a/source/game/scenes/title_scene.hpp +++ b/source/game/scenes/title_scene.hpp @@ -1,6 +1,12 @@ -// title_scene.hpp - Pantalla de título del juego -// Muestra message "PRESS BUTTON TO PLAY" y copyright +// title_scene.hpp - Escena de títol en 3D real // © 2026 JailDesigner +// +// Clon de `TitleScene` (2D) que substitueix `Graphics::Starfield` per +// `Graphics::Starfield` i `Title::ShipAnimator` per `Title::ShipAnimator`, +// afegint una `Graphics::Camera3D` que projecta l'escena en perspectiva real. +// Tot el bloc d'overlay 2D (logo "ORNI ATTACK!", "PRESS START TO PLAY", peu +// "JAILGAMES + copyright") es manté idèntic. +// #pragma once @@ -11,125 +17,108 @@ #include #include +#include "core/graphics/camera3d.hpp" #include "core/graphics/shape.hpp" #include "core/graphics/starfield.hpp" #include "core/graphics/vector_text.hpp" #include "core/input/input_types.hpp" #include "core/rendering/sdl_manager.hpp" +#include "core/system/game_config.hpp" #include "core/system/scene.hpp" #include "core/system/scene_context.hpp" -#include "core/system/game_config.hpp" #include "core/types.hpp" #include "game/title/ship_animator.hpp" -// Botones para INICIAR PARTIDA desde MAIN (solo START) -static constexpr std::array START_GAME_BUTTONS = { - InputAction::START}; - class TitleScene final : public Scene { - public: - explicit TitleScene(SDLManager& sdl, SceneManager::SceneContext& context); - ~TitleScene() override; // Destructor per aturar música + public: + explicit TitleScene(SDLManager& sdl, SceneManager::SceneContext& context); + ~TitleScene() override; - // Scene interface - void handleEvent(const SDL_Event& event) override; - void update(float delta_time) override; - void draw() override; - [[nodiscard]] auto isFinished() const -> bool override; + void handleEvent(const SDL_Event& event) override; + void update(float delta_time) override; + void draw() override; + [[nodiscard]] auto isFinished() const -> bool override; - private: - // Màquina de estats per la pantalla de título - enum class TitleState : std::uint8_t { - STARFIELD_FADE_IN, // Fade-in del starfield (3.0s) - STARFIELD, // Pantalla con camp de estrelles (4.0s) - MAIN, // Pantalla de título con text (indefinit, hasta START) - PLAYER_JOIN_PHASE, // Fase de unió de jugadors: fade-out música + text parpellejant (2.5s) - BLACK_SCREEN // Pantalla negra de transición (2.0s) - }; + private: + enum class TitleState : std::uint8_t { + STARFIELD_FADE_IN, + STARFIELD, + MAIN, + PLAYER_JOIN_PHASE, + BLACK_SCREEN, + }; - // Estructura per emmagatzemar informació de cada lletra del título - struct LetraLogo { - std::shared_ptr shape; // Forma vectorial de la lletra - Vec2 position; // Posición en pantalla - float ancho; // Amplada scaled - float altura; // Altura scaled - float offset_centre; // Offset del centro per posicionament - }; + struct LetraLogo { + std::shared_ptr shape; + Vec2 position; + float ancho; + float altura; + float offset_centre; + }; - SDLManager& sdl_; - SceneManager::SceneContext& context_; - GameConfig::MatchConfig match_config_; // Configuración de jugadors active - Graphics::VectorText text_; // Sistema de text vectorial - std::unique_ptr starfield_; // Camp de estrelles de fons - std::unique_ptr ship_animator_; // Naves 3D flotantes - TitleState estat_actual_{TitleState::STARFIELD_FADE_IN}; // Estat actual de la màquina - float temps_acumulat_{0.0F}; // Temps acumulat per l'state INIT + SDLManager& sdl_; + SceneManager::SceneContext& context_; + GameConfig::MatchConfig match_config_; + Graphics::VectorText text_; + std::unique_ptr camera_; + std::unique_ptr starfield_; + std::unique_ptr ship_animator_; + TitleState estat_actual_{TitleState::STARFIELD_FADE_IN}; + float temps_acumulat_{0.0F}; - // Lletres del título "ORNI ATTACK!" - std::vector lletres_orni_; // Lletres de "ORNI" (línia 1) - std::vector lletres_attack_; // Lletres de "ATTACK!" (línia 2) - float y_attack_dinamica_; // Posición Y calculada dinàmicament per "ATTACK!" + std::vector lletres_orni_; + std::vector lletres_attack_; + float y_attack_dinamica_{0.0F}; - // Logo "JAILGAMES" pequeño sobre el copyright (esquinas inferiores del título). - std::vector lletres_jailgames_; + std::vector lletres_jailgames_; - // Estat de animación del logo - float temps_animacio_{0.0F}; // Temps acumulat per animación orbital - std::vector posicions_originals_orni_; // Posicions originals de "ORNI" - std::vector posicions_originals_attack_; // Posicions originals de "ATTACK!" + float temps_animacio_{0.0F}; + std::vector posicions_originals_orni_; + std::vector posicions_originals_attack_; - // Estat de arrencada de l'animación - float temps_estat_main_{0.0F}; // Temps acumulat en state MAIN - bool animacio_activa_{false}; // Flag: true cuando animación está activa - float factor_lerp_{0.0F}; // Factor de lerp actual (0.0 → 1.0) + float temps_estat_main_{0.0F}; + bool animacio_activa_{false}; + float factor_lerp_{0.0F}; - // Constants - static constexpr float BRIGHTNESS_STARFIELD = 1.2F; // Brightness del starfield (>1.0 = més brillant) - static constexpr float DURACIO_FADE_IN = 3.0F; // Duració del fade-in del starfield (1.5 segons) - static constexpr float DURACIO_INIT = 4.0F; // Duració de l'state INIT (2 segons) - static constexpr float DURACIO_TRANSITION = 2.5F; // Duració de la transición (1.5 segons) - static constexpr float ESPAI_ENTRE_LLETRES = 10.0F; // Espai entre lletres - static constexpr float BLINK_FREQUENCY = 3.0F; // Freqüència de parpelleig (3 Hz) - static constexpr float DURACIO_BLACK_SCREEN = 2.0F; // Duració pantalla negra (2 segons) - static constexpr int MUSIC_FADE = 1500; // Duracio del fade de la musica del titol al començar a jugar + static constexpr float BRIGHTNESS_STARFIELD = 1.2F; + static constexpr float DURACIO_FADE_IN = 3.0F; + static constexpr float DURACIO_INIT = 4.0F; + static constexpr float DURACIO_TRANSITION = 2.5F; + static constexpr float ESPAI_ENTRE_LLETRES = 10.0F; + static constexpr float BLINK_FREQUENCY = 3.0F; + static constexpr float DURACIO_BLACK_SCREEN = 2.0F; + static constexpr int MUSIC_FADE = 1500; - // Constants de animación del logo - static constexpr float ORBIT_AMPLITUDE_X = 4.0F; // Amplitud oscil·lació horitzontal (píxels) - static constexpr float ORBIT_AMPLITUDE_Y = 3.0F; // Amplitud oscil·lació vertical (píxels) - static constexpr float ORBIT_FREQUENCY_X = 0.8F; // Velocidad oscil·lació horitzontal (Hz) - static constexpr float ORBIT_FREQUENCY_Y = 1.2F; // Velocidad oscil·lació vertical (Hz) - static constexpr float ORBIT_PHASE_OFFSET = 1.57F; // Desfasament entre X i Y (90° per circular) + static constexpr float ORBIT_AMPLITUDE_X = 4.0F; + static constexpr float ORBIT_AMPLITUDE_Y = 3.0F; + static constexpr float ORBIT_FREQUENCY_X = 0.8F; + static constexpr float ORBIT_FREQUENCY_Y = 1.2F; + static constexpr float ORBIT_PHASE_OFFSET = 1.57F; - // Constants de ombra del logo - static constexpr float SHADOW_DELAY = 0.5F; // Retard temporal de l'ombra (segons) - static constexpr float SHADOW_BRIGHTNESS = 0.4F; // Multiplicador de brightness de l'ombra (0.0-1.0) - static constexpr float SHADOW_OFFSET_X = 2.0F; // Offset espacial X fix (píxels) - static constexpr float SHADOW_OFFSET_Y = 2.0F; // Offset espacial Y fix (píxels) + static constexpr float SHADOW_DELAY = 0.5F; + static constexpr float SHADOW_BRIGHTNESS = 0.4F; + static constexpr float SHADOW_OFFSET_X = 2.0F; + static constexpr float SHADOW_OFFSET_Y = 2.0F; - // Temporització de l'arrencada de l'animación - static constexpr float DELAY_INICI_ANIMACIO = 10.0F; // 10s estàtic antes de animar - static constexpr float DURACIO_LERP = 2.0F; // 2s per arribar a amplitud completa + static constexpr float DELAY_INICI_ANIMACIO = 10.0F; + static constexpr float DURACIO_LERP = 2.0F; - // Métodos privats - void updateLogoAnimation(float delta_time); // Actualitza l'animación orbital del logo - // Estático: solo consulta Input (singleton), no estado de la escena. - static auto checkSkipButtonPressed() -> bool; - auto checkStartGameButtonPressed() -> bool; - void initTitle(); // Carrega i posiciona las lletres del título - void inicialitzarJailgames(); // Carrega i posiciona el logo JAILGAMES pequeño - void dibuixarPeuTitol(float spacing) const; // Logo JAILGAMES + línia de copyright + // Càmera 3D: FOV vertical en radians. + static constexpr float CAMERA_FOV_Y_RAD = 1.0472F; // 60° - // Sub-pasos de update() (extreure cada state per reduir complexitat). - void updateStarfieldFadeInState(float delta_time); - void updateStarfieldState(float delta_time); - void updateMainState(float delta_time); - void updatePlayerJoinPhaseState(float delta_time); - void updateBlackScreenState(float delta_time); - // Handlers de input globals (independents de l'state actual). - void handleSkipInput(); - void handleStartInput(); - // Helper compartit: dispara l'animación de salida per las naves del player que - // acaba de fer un join "en aquest frame" (jugadorX_actiu == true && !prev). - void triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, - const char* log_prefix); + void updateLogoAnimation(float delta_time); + static auto checkSkipButtonPressed() -> bool; + auto checkStartGameButtonPressed() -> bool; + void initTitle(); + void inicialitzarJailgames(); + void dibuixarPeuTitol(float spacing) const; + + void updateStarfieldFadeInState(float delta_time); + void updateStarfieldState(float delta_time); + void updateMainState(float delta_time); + void updatePlayerJoinPhaseState(float delta_time); + void updateBlackScreenState(float delta_time); + void handleSkipInput(); + void handleStartInput(); + void triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix); }; diff --git a/source/game/scenes/title_scene_3d.cpp b/source/game/scenes/title_scene_3d.cpp deleted file mode 100644 index 338e9be..0000000 --- a/source/game/scenes/title_scene_3d.cpp +++ /dev/null @@ -1,547 +0,0 @@ -// title_scene_3d.cpp - Implementació de l'escena de títol 3D real -// © 2026 JailDesigner - -#include "title_scene_3d.hpp" - -#include -#include -#include -#include -#include -#include - -#include "core/audio/audio.hpp" -#include "core/defaults.hpp" -#include "core/graphics/shape_loader.hpp" -#include "core/input/input.hpp" -#include "core/rendering/shape_renderer.hpp" -#include "core/system/scene_context.hpp" -#include "project.h" - -using SceneManager::SceneContext; -using SceneType = SceneContext::SceneType; -using Option = SceneContext::Option; - -namespace { - - // Botons per iniciar partida des de MAIN (només START). Duplicat del que viu - // al `title_scene.hpp` perquè no volem un acoblament entre la versió 2D i la - // 3D mentre conviuen. - constexpr std::array START_GAME_BUTTONS_3D = {InputAction::START}; - -} // namespace - -TitleScene3D::TitleScene3D(SDLManager& sdl, SceneContext& context) - : sdl_(sdl), - context_(context), - text_(sdl.getRenderer()) { - std::cout << "SceneType Titol3D: Inicialitzant...\n"; - - match_config_.jugador1_actiu = false; - match_config_.jugador2_actiu = false; - match_config_.mode = GameConfig::Mode::NORMAL; - - auto option = context_.consumeOption(); - if (option == Option::JUMP_TO_TITLE_MAIN) { - std::cout << "SceneType Titol3D: Opció JUMP_TO_TITLE_MAIN activada\n"; - estat_actual_ = TitleState::MAIN; - temps_estat_main_ = 0.0F; - } - - // Càmera 3D: posicionada a l'origen, mirant cap a +Z, amb Y cap amunt. - camera_ = std::make_unique( - Vec3{.x = 0.0F, .y = 0.0F, .z = 0.0F}, - Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F}, - Vec3{.x = 0.0F, .y = 1.0F, .z = 0.0F}, - CAMERA_FOV_Y_RAD, - static_cast(Defaults::Game::WIDTH), - static_cast(Defaults::Game::HEIGHT)); - - starfield_ = std::make_unique( - sdl_.getRenderer(), - camera_.get(), - 200); - if (estat_actual_ == TitleState::MAIN) { - starfield_->setBrightness(BRIGHTNESS_STARFIELD); - } else { - starfield_->setBrightness(0.0F); - } - - ship_animator_ = std::make_unique(sdl_.getRenderer(), camera_.get()); - ship_animator_->init(); - - if (estat_actual_ == TitleState::MAIN) { - ship_animator_->setVisible(true); - ship_animator_->startEntryAnimation(); - } else { - ship_animator_->setVisible(false); - } - - initTitle(); - inicialitzarJailgames(); - - if (Audio::getMusicState() != Audio::MusicState::PLAYING) { - Audio::get()->playMusic("title.ogg"); - } -} - -TitleScene3D::~TitleScene3D() { - Audio::get()->stopMusic(); -} - -void TitleScene3D::initTitle() { - using namespace Graphics; - - const std::vector FITXERS_ORNI = { - "title/letra_o.shp", - "title/letra_r.shp", - "title/letra_n.shp", - "title/letra_i.shp"}; - - float ancho_total_orni = 0.0F; - for (const auto& file : FITXERS_ORNI) { - auto shape = ShapeLoader::load(file); - if (!shape || !shape->isValid()) { - std::cerr << "[TitleScene3D] Error carregant " << file << '\n'; - continue; - } - float min_x = FLT_MAX; - float max_x = -FLT_MAX; - float min_y = FLT_MAX; - float max_y = -FLT_MAX; - for (const auto& prim : shape->getPrimitives()) { - for (const auto& point : prim.points) { - min_x = std::min(min_x, point.x); - max_x = std::max(max_x, point.x); - min_y = std::min(min_y, point.y); - max_y = std::max(max_y, point.y); - } - } - const float ANCHO = (max_x - min_x) * Defaults::Title::Layout::LOGO_SCALE; - const float ALTURA = (max_y - min_y) * Defaults::Title::Layout::LOGO_SCALE; - const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE; - lletres_orni_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ANCHO, ALTURA, OFFSET_CENTRE}); - ancho_total_orni += ANCHO; - } - ancho_total_orni += ESPAI_ENTRE_LLETRES * static_cast(lletres_orni_.size() - 1); - - float x_actual = (Defaults::Game::WIDTH - ancho_total_orni) / 2.0F; - for (auto& lletra : lletres_orni_) { - lletra.position.x = x_actual + lletra.offset_centre; - lletra.position.y = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS; - x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES; - } - - const float ALTURA_ORNI = lletres_orni_.empty() ? 50.0F : lletres_orni_[0].altura; - const float Y_ORNI = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS; - const float SEPARACION = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_LINE_SPACING; - y_attack_dinamica_ = Y_ORNI + ALTURA_ORNI + SEPARACION; - - const std::vector FITXERS_ATTACK = { - "title/letra_a.shp", - "title/letra_t.shp", - "title/letra_t.shp", - "title/letra_a.shp", - "title/letra_c.shp", - "title/letra_k.shp", - "title/letra_exclamacion.shp"}; - - float ancho_total_attack = 0.0F; - for (const auto& file : FITXERS_ATTACK) { - auto shape = ShapeLoader::load(file); - if (!shape || !shape->isValid()) { - std::cerr << "[TitleScene3D] Error carregant " << file << '\n'; - continue; - } - float min_x = FLT_MAX; - float max_x = -FLT_MAX; - float min_y = FLT_MAX; - float max_y = -FLT_MAX; - for (const auto& prim : shape->getPrimitives()) { - for (const auto& point : prim.points) { - min_x = std::min(min_x, point.x); - max_x = std::max(max_x, point.x); - min_y = std::min(min_y, point.y); - max_y = std::max(max_y, point.y); - } - } - const float ANCHO = (max_x - min_x) * Defaults::Title::Layout::LOGO_SCALE; - const float ALTURA = (max_y - min_y) * Defaults::Title::Layout::LOGO_SCALE; - const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE; - lletres_attack_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ANCHO, ALTURA, OFFSET_CENTRE}); - ancho_total_attack += ANCHO; - } - ancho_total_attack += ESPAI_ENTRE_LLETRES * static_cast(lletres_attack_.size() - 1); - - x_actual = (Defaults::Game::WIDTH - ancho_total_attack) / 2.0F; - for (auto& lletra : lletres_attack_) { - lletra.position.x = x_actual + lletra.offset_centre; - lletra.position.y = y_attack_dinamica_; - x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES; - } - - posicions_originals_orni_.clear(); - for (const auto& lletra : lletres_orni_) { - posicions_originals_orni_.push_back(lletra.position); - } - posicions_originals_attack_.clear(); - for (const auto& lletra : lletres_attack_) { - posicions_originals_attack_.push_back(lletra.position); - } -} - -void TitleScene3D::inicialitzarJailgames() { - using namespace Graphics; - - const std::vector FITXERS = { - "logo/letra_j.shp", - "logo/letra_a.shp", - "logo/letra_i.shp", - "logo/letra_l.shp", - "logo/letra_g.shp", - "logo/letra_a.shp", - "logo/letra_m.shp", - "logo/letra_e.shp", - "logo/letra_s.shp"}; - - constexpr float SCALE = Defaults::Title::Layout::JAILGAMES_SCALE; - - float ancho_total = 0.0F; - float altura_max = 0.0F; - for (const auto& file : FITXERS) { - auto shape = ShapeLoader::load(file); - if (!shape || !shape->isValid()) { - std::cerr << "[TitleScene3D] Error carregant " << file << '\n'; - continue; - } - float min_x = FLT_MAX; - float max_x = -FLT_MAX; - float min_y = FLT_MAX; - float max_y = -FLT_MAX; - for (const auto& prim : shape->getPrimitives()) { - for (const auto& point : prim.points) { - min_x = std::min(min_x, point.x); - max_x = std::max(max_x, point.x); - min_y = std::min(min_y, point.y); - max_y = std::max(max_y, point.y); - } - } - const float ANCHO = (max_x - min_x) * SCALE; - const float ALTURA = (max_y - min_y) * SCALE; - const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * SCALE; - lletres_jailgames_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ANCHO, ALTURA, OFFSET_CENTRE}); - ancho_total += ANCHO; - altura_max = std::max(altura_max, ALTURA); - } - constexpr float ESPAI_JAILGAMES = ESPAI_ENTRE_LLETRES * SCALE; - if (!lletres_jailgames_.empty()) { - ancho_total += ESPAI_JAILGAMES * static_cast(lletres_jailgames_.size() - 1); - } - - const float Y_COPY = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS; - const float GAP = Defaults::Game::HEIGHT * Defaults::Title::Layout::JAILGAMES_COPYRIGHT_GAP; - const float Y_CENTRE = Y_COPY - GAP - (altura_max / 2.0F); - const float X_INICIAL = (Defaults::Game::WIDTH - ancho_total) / 2.0F; - - float x_actual = X_INICIAL; - for (auto& lletra : lletres_jailgames_) { - lletra.position.x = x_actual + lletra.offset_centre; - lletra.position.y = Y_CENTRE; - x_actual += lletra.ancho + ESPAI_JAILGAMES; - } -} - -void TitleScene3D::dibuixarPeuTitol(float spacing) const { - for (const auto& lletra : lletres_jailgames_) { - Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::JAILGAMES_SCALE, 1.0F); - } - std::string copyright = Project::COPYRIGHT; - for (char& c : copyright) { - if (c >= 'a' && c <= 'z') { - c = static_cast(c - 32); - } - } - const float Y_COPY = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS; - const float CENTRE_X = Defaults::Game::WIDTH / 2.0F; - text_.renderCentered(copyright, {.x = CENTRE_X, .y = Y_COPY}, Defaults::Title::Layout::COPYRIGHT_SCALE, spacing); -} - -auto TitleScene3D::isFinished() const -> bool { - // Aquesta escena és la destinació d'un setNextScene(TITLE) quan ORNI_TITLE_3D - // està activat; mentre el context continue marcant TITLE com a destí actual, - // l'escena resta viva. També accepta TITLE_3D explícit. - const SceneType NEXT = context_.nextScene(); - return NEXT != SceneType::TITLE && NEXT != SceneType::TITLE_3D; -} - -void TitleScene3D::update(float delta_time) { - if (starfield_) { - starfield_->update(delta_time); - } - if (ship_animator_ && - (estat_actual_ == TitleState::STARFIELD_FADE_IN || - estat_actual_ == TitleState::STARFIELD || - estat_actual_ == TitleState::MAIN || - estat_actual_ == TitleState::PLAYER_JOIN_PHASE)) { - ship_animator_->update(delta_time); - } - - switch (estat_actual_) { - case TitleState::STARFIELD_FADE_IN: - updateStarfieldFadeInState(delta_time); - break; - case TitleState::STARFIELD: - updateStarfieldState(delta_time); - break; - case TitleState::MAIN: - updateMainState(delta_time); - break; - case TitleState::PLAYER_JOIN_PHASE: - updatePlayerJoinPhaseState(delta_time); - break; - case TitleState::BLACK_SCREEN: - updateBlackScreenState(delta_time); - break; - } - - handleSkipInput(); - handleStartInput(); -} - -void TitleScene3D::updateStarfieldFadeInState(float delta_time) { - temps_acumulat_ += delta_time; - const float PROGRESS = std::min(1.0F, temps_acumulat_ / DURACIO_FADE_IN); - starfield_->setBrightness(PROGRESS * BRIGHTNESS_STARFIELD); - if (temps_acumulat_ >= DURACIO_FADE_IN) { - estat_actual_ = TitleState::STARFIELD; - temps_acumulat_ = 0.0F; - starfield_->setBrightness(BRIGHTNESS_STARFIELD); - } -} - -void TitleScene3D::updateStarfieldState(float delta_time) { - temps_acumulat_ += delta_time; - if (temps_acumulat_ >= DURACIO_INIT) { - estat_actual_ = TitleState::MAIN; - temps_estat_main_ = 0.0F; - animacio_activa_ = false; - factor_lerp_ = 0.0F; - } -} - -void TitleScene3D::updateMainState(float delta_time) { - temps_estat_main_ += delta_time; - if (temps_estat_main_ >= Defaults::Title::Ships::ENTRANCE_DELAY && - ship_animator_ && !ship_animator_->isVisible()) { - ship_animator_->setVisible(true); - ship_animator_->startEntryAnimation(); - } - - if (temps_estat_main_ < DELAY_INICI_ANIMACIO) { - factor_lerp_ = 0.0F; - animacio_activa_ = false; - } else if (temps_estat_main_ < DELAY_INICI_ANIMACIO + DURACIO_LERP) { - const float TEMPS_LERP = temps_estat_main_ - DELAY_INICI_ANIMACIO; - factor_lerp_ = TEMPS_LERP / DURACIO_LERP; - animacio_activa_ = true; - } else { - factor_lerp_ = 1.0F; - animacio_activa_ = true; - } - updateLogoAnimation(delta_time); -} - -void TitleScene3D::updatePlayerJoinPhaseState(float delta_time) { - temps_acumulat_ += delta_time; - updateLogoAnimation(delta_time); - - const bool P1_ABANS = match_config_.jugador1_actiu; - const bool P2_ABANS = match_config_.jugador2_actiu; - - if (checkStartGameButtonPressed()) { - context_.setMatchConfig(match_config_); - triggerExitForJoinedPlayers(P1_ABANS, P2_ABANS, "late join - "); - Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME); - temps_acumulat_ = 0.0F; - } - - if (temps_acumulat_ >= DURACIO_TRANSITION) { - estat_actual_ = TitleState::BLACK_SCREEN; - temps_acumulat_ = 0.0F; - } -} - -void TitleScene3D::updateBlackScreenState(float delta_time) { - temps_acumulat_ += delta_time; - if (temps_acumulat_ >= DURACIO_BLACK_SCREEN) { - context_.setNextScene(SceneType::GAME); - } -} - -void TitleScene3D::handleSkipInput() { - if (estat_actual_ != TitleState::STARFIELD_FADE_IN && estat_actual_ != TitleState::STARFIELD) { - return; - } - if (!checkSkipButtonPressed()) { - return; - } - estat_actual_ = TitleState::MAIN; - starfield_->setBrightness(BRIGHTNESS_STARFIELD); - temps_estat_main_ = 0.0F; -} - -void TitleScene3D::handleStartInput() { - if (estat_actual_ != TitleState::MAIN) { - return; - } - const bool P1_ABANS = match_config_.jugador1_actiu; - const bool P2_ABANS = match_config_.jugador2_actiu; - - if (!checkStartGameButtonPressed()) { - return; - } - - if (ship_animator_ && !ship_animator_->isVisible()) { - ship_animator_->setVisible(true); - ship_animator_->skipToFloatingState(); - } - - context_.setMatchConfig(match_config_); - estat_actual_ = TitleState::PLAYER_JOIN_PHASE; - temps_acumulat_ = 0.0F; - - triggerExitForJoinedPlayers(P1_ABANS, P2_ABANS, ""); - - Audio::get()->fadeOutMusic(MUSIC_FADE); - Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME); -} - -void TitleScene3D::triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix) { - if (ship_animator_ == nullptr) { - return; - } - if (match_config_.jugador1_actiu && !p1_was_active) { - ship_animator_->triggerExitAnimationForPlayer(1); - std::cout << "[TitleScene3D] P1 " << log_prefix << "ship exiting\n"; - } - if (match_config_.jugador2_actiu && !p2_was_active) { - ship_animator_->triggerExitAnimationForPlayer(2); - std::cout << "[TitleScene3D] P2 " << log_prefix << "ship exiting\n"; - } -} - -void TitleScene3D::updateLogoAnimation(float delta_time) { - if (!animacio_activa_) { - return; - } - temps_animacio_ += delta_time * factor_lerp_; - - const float TWO_PI = 2.0F * Defaults::Math::PI; - const float OFFSET_X = ORBIT_AMPLITUDE_X * std::sin(TWO_PI * ORBIT_FREQUENCY_X * temps_animacio_); - const float OFFSET_Y = ORBIT_AMPLITUDE_Y * std::sin((TWO_PI * ORBIT_FREQUENCY_Y * temps_animacio_) + ORBIT_PHASE_OFFSET); - - for (std::size_t i = 0; i < lletres_orni_.size(); ++i) { - lletres_orni_[i].position.x = posicions_originals_orni_[i].x + std::round(OFFSET_X); - lletres_orni_[i].position.y = posicions_originals_orni_[i].y + std::round(OFFSET_Y); - } - for (std::size_t i = 0; i < lletres_attack_.size(); ++i) { - lletres_attack_[i].position.x = posicions_originals_attack_[i].x + std::round(OFFSET_X); - lletres_attack_[i].position.y = posicions_originals_attack_[i].y + std::round(OFFSET_Y); - } -} - -void TitleScene3D::draw() { - if (starfield_ && estat_actual_ != TitleState::BLACK_SCREEN) { - starfield_->draw(); - } - - if (ship_animator_ && - (estat_actual_ == TitleState::STARFIELD_FADE_IN || - estat_actual_ == TitleState::STARFIELD || - estat_actual_ == TitleState::MAIN || - estat_actual_ == TitleState::PLAYER_JOIN_PHASE)) { - ship_animator_->draw(); - } - - if (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD) { - return; - } - - if (estat_actual_ != TitleState::MAIN && estat_actual_ != TitleState::PLAYER_JOIN_PHASE) { - return; - } - - if (animacio_activa_) { - float temps_shadow = std::max(0.0F, temps_animacio_ - SHADOW_DELAY); - const float TWO_PI = 2.0F * Defaults::Math::PI; - const float SHADOW_OX = (ORBIT_AMPLITUDE_X * std::sin(TWO_PI * ORBIT_FREQUENCY_X * temps_shadow)) + SHADOW_OFFSET_X; - const float SHADOW_OY = (ORBIT_AMPLITUDE_Y * std::sin((TWO_PI * ORBIT_FREQUENCY_Y * temps_shadow) + ORBIT_PHASE_OFFSET)) + SHADOW_OFFSET_Y; - - for (std::size_t i = 0; i < lletres_orni_.size(); ++i) { - const Vec2 POS_SHADOW{ - .x = posicions_originals_orni_[i].x + std::round(SHADOW_OX), - .y = posicions_originals_orni_[i].y + std::round(SHADOW_OY), - }; - Rendering::renderShape(sdl_.getRenderer(), lletres_orni_[i].shape, POS_SHADOW, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, SHADOW_BRIGHTNESS); - } - for (std::size_t i = 0; i < lletres_attack_.size(); ++i) { - const Vec2 POS_SHADOW{ - .x = posicions_originals_attack_[i].x + std::round(SHADOW_OX), - .y = posicions_originals_attack_[i].y + std::round(SHADOW_OY), - }; - Rendering::renderShape(sdl_.getRenderer(), lletres_attack_[i].shape, POS_SHADOW, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, SHADOW_BRIGHTNESS); - } - } - - for (const auto& lletra : lletres_orni_) { - Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F); - } - for (const auto& lletra : lletres_attack_) { - Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F); - } - - const float SPACING = Defaults::Title::Layout::TEXT_SPACING; - - bool mostrar_text = true; - if (estat_actual_ == TitleState::PLAYER_JOIN_PHASE) { - const float FASE = temps_acumulat_ * BLINK_FREQUENCY * 2.0F * std::numbers::pi_v; - mostrar_text = (std::sin(FASE) > 0.0F); - } - if (mostrar_text) { - const std::string MAIN_TEXT = "PRESS START TO PLAY"; - const float MAIN_SCALE = Defaults::Title::Layout::PRESS_START_SCALE; - const float CENTRE_X = Defaults::Game::WIDTH / 2.0F; - const float CENTRE_Y = Defaults::Game::HEIGHT * Defaults::Title::Layout::PRESS_START_POS; - text_.renderCentered(MAIN_TEXT, {.x = CENTRE_X, .y = CENTRE_Y}, MAIN_SCALE, SPACING); - } - - dibuixarPeuTitol(SPACING); -} - -auto TitleScene3D::checkSkipButtonPressed() -> bool { - return Input::get()->checkAnyPlayerAction(ARCADE_BUTTONS); -} - -auto TitleScene3D::checkStartGameButtonPressed() -> bool { - auto* input = Input::get(); - bool any_pressed = false; - for (auto action : START_GAME_BUTTONS_3D) { - if (input->checkActionPlayer1(action, Input::DO_NOT_ALLOW_REPEAT)) { - if (!match_config_.jugador1_actiu) { - match_config_.jugador1_actiu = true; - any_pressed = true; - } - } - if (input->checkActionPlayer2(action, Input::DO_NOT_ALLOW_REPEAT)) { - if (!match_config_.jugador2_actiu) { - match_config_.jugador2_actiu = true; - any_pressed = true; - } - } - } - return any_pressed; -} - -void TitleScene3D::handleEvent(const SDL_Event& event) { - (void)event; -} diff --git a/source/game/scenes/title_scene_3d.hpp b/source/game/scenes/title_scene_3d.hpp deleted file mode 100644 index 840f802..0000000 --- a/source/game/scenes/title_scene_3d.hpp +++ /dev/null @@ -1,126 +0,0 @@ -// title_scene_3d.hpp - Variant 3D real de l'escena de títol -// © 2026 JailDesigner -// -// Clon de `TitleScene` (2D) que substitueix `Graphics::Starfield` per -// `Graphics::Starfield3D` i `Title::ShipAnimator` per `Title::ShipAnimator3D`, -// afegint una `Graphics::Camera3D` que projecta l'escena en perspectiva real. -// Tot el bloc d'overlay 2D (logo "ORNI ATTACK!", "PRESS START TO PLAY", peu -// "JAILGAMES + copyright") es manté idèntic. -// -// Trigger: env var `ORNI_TITLE_3D=1` interceptada al `Director::buildScene`, -// o transicions explícites a `SceneType::TITLE_3D`. - -#pragma once - -#include - -#include -#include -#include -#include - -#include "core/graphics/camera3d.hpp" -#include "core/graphics/shape.hpp" -#include "core/graphics/starfield3d.hpp" -#include "core/graphics/vector_text.hpp" -#include "core/input/input_types.hpp" -#include "core/rendering/sdl_manager.hpp" -#include "core/system/game_config.hpp" -#include "core/system/scene.hpp" -#include "core/system/scene_context.hpp" -#include "core/types.hpp" -#include "game/title/ship_animator3d.hpp" - -class TitleScene3D final : public Scene { - public: - explicit TitleScene3D(SDLManager& sdl, SceneManager::SceneContext& context); - ~TitleScene3D() override; - - void handleEvent(const SDL_Event& event) override; - void update(float delta_time) override; - void draw() override; - [[nodiscard]] auto isFinished() const -> bool override; - - private: - enum class TitleState : std::uint8_t { - STARFIELD_FADE_IN, - STARFIELD, - MAIN, - PLAYER_JOIN_PHASE, - BLACK_SCREEN, - }; - - struct LetraLogo { - std::shared_ptr shape; - Vec2 position; - float ancho; - float altura; - float offset_centre; - }; - - SDLManager& sdl_; - SceneManager::SceneContext& context_; - GameConfig::MatchConfig match_config_; - Graphics::VectorText text_; - std::unique_ptr camera_; - std::unique_ptr starfield_; - std::unique_ptr ship_animator_; - TitleState estat_actual_{TitleState::STARFIELD_FADE_IN}; - float temps_acumulat_{0.0F}; - - std::vector lletres_orni_; - std::vector lletres_attack_; - float y_attack_dinamica_{0.0F}; - - std::vector lletres_jailgames_; - - float temps_animacio_{0.0F}; - std::vector posicions_originals_orni_; - std::vector posicions_originals_attack_; - - float temps_estat_main_{0.0F}; - bool animacio_activa_{false}; - float factor_lerp_{0.0F}; - - static constexpr float BRIGHTNESS_STARFIELD = 1.2F; - static constexpr float DURACIO_FADE_IN = 3.0F; - static constexpr float DURACIO_INIT = 4.0F; - static constexpr float DURACIO_TRANSITION = 2.5F; - static constexpr float ESPAI_ENTRE_LLETRES = 10.0F; - static constexpr float BLINK_FREQUENCY = 3.0F; - static constexpr float DURACIO_BLACK_SCREEN = 2.0F; - static constexpr int MUSIC_FADE = 1500; - - static constexpr float ORBIT_AMPLITUDE_X = 4.0F; - static constexpr float ORBIT_AMPLITUDE_Y = 3.0F; - static constexpr float ORBIT_FREQUENCY_X = 0.8F; - static constexpr float ORBIT_FREQUENCY_Y = 1.2F; - static constexpr float ORBIT_PHASE_OFFSET = 1.57F; - - static constexpr float SHADOW_DELAY = 0.5F; - static constexpr float SHADOW_BRIGHTNESS = 0.4F; - static constexpr float SHADOW_OFFSET_X = 2.0F; - static constexpr float SHADOW_OFFSET_Y = 2.0F; - - static constexpr float DELAY_INICI_ANIMACIO = 10.0F; - static constexpr float DURACIO_LERP = 2.0F; - - // Càmera 3D: FOV vertical en radians. - static constexpr float CAMERA_FOV_Y_RAD = 1.0472F; // 60° - - void updateLogoAnimation(float delta_time); - static auto checkSkipButtonPressed() -> bool; - auto checkStartGameButtonPressed() -> bool; - void initTitle(); - void inicialitzarJailgames(); - void dibuixarPeuTitol(float spacing) const; - - void updateStarfieldFadeInState(float delta_time); - void updateStarfieldState(float delta_time); - void updateMainState(float delta_time); - void updatePlayerJoinPhaseState(float delta_time); - void updateBlackScreenState(float delta_time); - void handleSkipInput(); - void handleStartInput(); - void triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix); -}; diff --git a/source/game/title/ship_animator.cpp b/source/game/title/ship_animator.cpp index 7e26f70..cd66d99 100644 --- a/source/game/title/ship_animator.cpp +++ b/source/game/title/ship_animator.cpp @@ -1,4 +1,4 @@ -// ship_animator.cpp - Implementació del sistema de animación de naves +// ship_animator.cpp - Implementació de l'animador de naus 3D // © 2026 JailDesigner #include "ship_animator.hpp" @@ -9,319 +9,336 @@ #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(Rendering::Renderer* renderer) - : renderer_(renderer) { -} + namespace { -void ShipAnimator::init() { - // Carregar formes de naves con perspectiva pre-calculada - auto forma_p1 = Graphics::ShapeLoader::load("ship_perspective.shp"); // Perspectiva izquierda - auto forma_p2 = Graphics::ShapeLoader::load("ship2_perspective.shp"); // Perspectiva derecha + // Profunditat d'extrusió de la silueta 2D de la nau (en unitats mundials). + // 0.0F → emet només la silueta plana. >0 emet volum extrudit. + constexpr float SHIP_EXTRUSION_DEPTH = 1.0F; - // Configurar ship P1 - ships_[0].player_id = 1; - ships_[0].shape = forma_p1; - configureShipP1(ships_[0]); + // VP lògic per definir forward_dir / direcció del path. Tots els paths + // s'allunyen cap a aquest punt; les naus exiting continuen MÉS ENLLÀ + // (vegeu SHIP_EXIT_TRAVEL) per no desaparèixer en arribar al VP. + constexpr float SHIP_EXIT_Z = 800.0F; + constexpr Vec3 VANISHING_POINT{.x = 0.0F, .y = 0.0F, .z = SHIP_EXIT_Z}; - // Configurar ship P2 - ships_[1].player_id = 2; - ships_[1].shape = forma_p2; - configureShipP2(ships_[1]); -} + // Profunditat addicional darrere del VP cap a la qual les naus exiting + // convergeixen. Així P1 (X<0) i P2 (X>0) mantenen sempre els seus + // hemisferis i no es creuen al passar pel VP — totes dues acaben al + // centre projectat (640, 360) sense travessar-lo. + constexpr float SHIP_EXIT_OVERFLOW = 700.0F; -void ShipAnimator::update(float delta_time) { - // Dispatcher segons state de cada ship - for (auto& ship : ships_) { - if (!ship.visible) { - continue; + // Directors VP → origen de cada nau, normalitzats. P1 ve des de "les 7" + // del rellotge (baix-esquerra), P2 des de "les 5" (baix-dreta). Els + // components estan calibrats perquè a TARGET_DIST el pixel projectat + // caigui aprox sota la "P de PRESS" / "Y de PLAY" del text del títol. + constexpr Vec3 PATH_DIR_P1{.x = -0.0887F, .y = -0.0683F, .z = -0.9938F}; + constexpr Vec3 PATH_DIR_P2{.x = +0.0887F, .y = -0.0683F, .z = -0.9938F}; + + // Distàncies des del VP al llarg del path (unitats mundials). + // Reduïm TARGET_DIST per acostar el descans al VP (puja en pantalla, + // s'allunya de PRESS START); compensem amb SHIP_FLOAT_SCALE més gran. + constexpr float TARGET_DIST = 480.0F; // Descans a Z≈323 → pixel ≈ (558, 423) + constexpr float ENTRY_DIST = 770.0F; // Inicial a Z≈35 → fora pantalla baix-esq. + + // Pitch addicional sobre el look-at pur per fer que el dors de la nau + // s'incline cap a la càmera (~-14° afegits). Amb forward quasi paral·lel + // a +Z, el pitch look-at és ~-94°; afegint això queda al voltant de -108°, + // que és l'angle visualment validat com a "bo" per l'usuari. + constexpr float PITCH_LIFT_RAD = -0.25F; + + // Look-at: calcula pitch+yaw que duen (0,-1,0) local a forward_dir mundial. + // Requereix l'ordre de rotació X→Y→Z al applyTransform de wireframe3d. + // Si forward és quasi vertical (sin(pitch) ≈ 0), retorna yaw=0 (qualsevol). + auto computePitchYawForLookAt(const Vec3& forward_dir) -> Vec2 { + const float DY = std::clamp(forward_dir.y, -1.0F, 1.0F); + const float PITCH_LOOKAT = -std::acos(-DY); // ∈ [-π, 0] + const float SIN_PITCH = std::sin(PITCH_LOOKAT); + float yaw = 0.0F; + if (std::abs(SIN_PITCH) >= 1.0E-5F) { + const float SY = -forward_dir.x / SIN_PITCH; + const float CY = -forward_dir.z / SIN_PITCH; + yaw = std::atan2(SY, CY); + } + return Vec2{.x = PITCH_LOOKAT + PITCH_LIFT_RAD, .y = yaw}; } - switch (ship.state) { - case ShipState::ENTERING: - updateEntering(ship, delta_time); - break; - case ShipState::FLOATING: - updateFloating(ship, delta_time); - break; - case ShipState::EXITING: - updateExiting(ship, delta_time); - break; - } - } -} - -void ShipAnimator::draw() const { - for (const auto& ship : ships_) { - if (!ship.visible) { - continue; + auto safeNormalize(const Vec3& v, const Vec3& fallback) -> Vec3 { + return v.lengthSquared() > 0.0F ? v.normalized() : fallback; } - // Renderizar ship (perspectiva ya incorporada a la shape) - Rendering::renderShape( - renderer_, - ship.shape, - ship.current_position, - 0.0F, // angle (rotación 2D no utilitzada) - ship.current_scale, - 1.0F, // progress (siempre visible) - 1.0F // brightness (brightness màxima) - ); + auto entryForward(const TitleShip& ship) -> Vec3 { + return safeNormalize(ship.target_position - ship.initial_position, + Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F}); + } + + auto floatingForward(const Vec3& target) -> Vec3 { + return safeNormalize(VANISHING_POINT - target, + Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F}); + } + + auto exitForward(const Vec3& current) -> Vec3 { + return safeNormalize(VANISHING_POINT - current, + Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F}); + } + + // Mida visual i animació. + constexpr float SHIP_FLOAT_SCALE = 2.0F; + constexpr float SHIP_ENTRY_SCALE = 2.0F; // Mida mundial idèntica; la perspectiva fa la resta + constexpr float ENTRY_DURATION = 2.0F; + constexpr float EXIT_DURATION = 1.5F; + + // 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 + + ShipAnimator::ShipAnimator(Rendering::Renderer* renderer, + const Graphics::Camera3D* camera) + : renderer_(renderer), + camera_(camera) { } -} -void ShipAnimator::startEntryAnimation() { - using namespace Defaults::Title::Ships; + void ShipAnimator::init() { + auto shape_p1 = Graphics::ShapeLoader::load("ship.shp"); + auto shape_p2 = Graphics::ShapeLoader::load("ship2.shp"); - // Configurar ship P1 para l'animación de entrada - ships_[0].state = ShipState::ENTERING; - ships_[0].state_time = 0.0F; - ships_[0].initial_position = computeOffscreenPosition(CLOCK_8_ANGLE); - ships_[0].current_position = ships_[0].initial_position; - ships_[0].current_scale = ships_[0].initial_scale; + ships_[0].player_id = 1; + if (shape_p1 && shape_p1->isValid()) { + ships_[0].mesh = Graphics::extrudeShape2D(*shape_p1, SHIP_EXTRUSION_DEPTH); + } + configureShipP1(ships_[0]); - // Configurar ship P2 para l'animación de entrada - ships_[1].state = ShipState::ENTERING; - ships_[1].state_time = 0.0F; - ships_[1].initial_position = computeOffscreenPosition(CLOCK_4_ANGLE); - ships_[1].current_position = ships_[1].initial_position; - ships_[1].current_scale = ships_[1].initial_scale; -} - -void ShipAnimator::triggerExitAnimation() { - // Configurar ambdues naves para l'animación de salida - for (auto& ship : ships_) { - // Canviar state a EXITING - ship.state = ShipState::EXITING; - ship.state_time = 0.0F; - - // Preservar posición actual (pot estar a mig camí si START es prem durante ENTERING) - ship.initial_position = ship.current_position; - - // La scale objetivo es preserva para calcular la interpolació - // (current_scale pot ser diferent si está en ENTERING) + 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 ShipAnimator::skipToFloatingState() { - // Posar ambdues naves 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ón objetivo (sin animación) - ship.current_position = ship.target_position; - ship.current_scale = ship.target_scale; - - // NO establir visibilitat aquí - ya ho hace el caller - // (evita fer visibles ambdues naves cuando solo una ha premut START) + void ShipAnimator::update(float delta_time) { + for (auto& ship : ships_) { + if (!ship.visible) { + continue; + } + switch (ship.state) { + case ShipState::ENTERING: + updateEntering(ship, delta_time); + break; + case ShipState::FLOATING: + updateFloating(ship, delta_time); + break; + case ShipState::EXITING: + updateExiting(ship, delta_time); + break; + } + } } -} -auto ShipAnimator::isVisible() const -> bool { - // Retorna true si almenys una ship es visible - return std::ranges::any_of(ships_, [](const TitleShip& ship) { return ship.visible; }); -} + void ShipAnimator::draw() const { + if (camera_ == nullptr || renderer_ == nullptr) { + return; + } + for (const auto& ship : ships_) { + if (!ship.visible) { + continue; + } + const Vec2 EULER = computePitchYawForLookAt(ship.forward_dir); + const Graphics::Transform3D TRANSFORM{ + .position = ship.current_position, + .rotation_euler = Vec3{.x = EULER.x, .y = EULER.y, .z = 0.0F}, + .scale = ship.current_scale, + }; + Graphics::drawWireframe(renderer_, *camera_, ship.mesh, TRANSFORM, 1.0F); + } + } -void ShipAnimator::triggerExitAnimationForPlayer(int player_id) { - // Trobar la ship del player especificat - for (auto& ship : ships_) { - if (ship.player_id == player_id) { - // Canviar state a EXITING solo per esta ship + void ShipAnimator::startEntryAnimation() { + for (auto& ship : ships_) { + ship.state = ShipState::ENTERING; + ship.state_time = 0.0F; + ship.current_position = ship.initial_position; + ship.current_scale = ship.initial_scale; + ship.forward_dir = entryForward(ship); + } + } + + void ShipAnimator::triggerExitAnimation() { + for (auto& ship : ships_) { ship.state = ShipState::EXITING; ship.state_time = 0.0F; - - // Preservar posición actual (pot estar a mig camí si START es prem durante ENTERING) ship.initial_position = ship.current_position; - - // La scale objetivo es preserva para calcular la interpolació - // (current_scale pot ser diferent si está en ENTERING) - break; // Solo una ship per player + ship.forward_dir = exitForward(ship.current_position); } } -} -void ShipAnimator::setVisible(bool visible) { - for (auto& ship : ships_) { - ship.visible = visible; - } -} - -auto ShipAnimator::isAnimationComplete() const -> bool { - // Comprovar si todas las naves són invisibles (han completat l'animación de salida) - return std::ranges::all_of(ships_, [](const TitleShip& ship) { return !ship.visible; }); -} - -// Métodos de animación (stubs) -void ShipAnimator::updateEntering(TitleShip& ship, float delta_time) { - using namespace Defaults::Title::Ships; - - ship.state_time += delta_time; - - // Esperar al delay antes de començar l'animación - if (ship.state_time < ship.entry_delay) { - // Aún en delay: la ship es queda fuera de pantalla (posición inicial) - ship.current_position = ship.initial_position; - ship.current_scale = ship.initial_scale; - return; + void ShipAnimator::triggerExitAnimationForPlayer(int player_id) { + for (auto& ship : ships_) { + if (ship.player_id == player_id) { + ship.state = ShipState::EXITING; + ship.state_time = 0.0F; + ship.initial_position = ship.current_position; + ship.forward_dir = exitForward(ship.current_position); + break; + } + } } - // Cálculo del progrés (restant el delay) - float elapsed = ship.state_time - ship.entry_delay; - float progress = std::min(1.0F, elapsed / ENTRY_DURATION); + void ShipAnimator::skipToFloatingState() { + for (auto& ship : ships_) { + ship.state = ShipState::FLOATING; + ship.state_time = 0.0F; + ship.oscillation_phase = 0.0F; + ship.current_position = ship.target_position; + ship.current_scale = ship.target_scale; + ship.forward_dir = floatingForward(ship.target_position); + } + } - // Aplicar easing (ease_out_quad per arribada suau) - float eased_progress = Easing::easeOutQuad(progress); + void ShipAnimator::setVisible(bool visible) { + for (auto& ship : ships_) { + ship.visible = visible; + } + } - // Lerp posición (inicial → objetivo) - 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); + auto ShipAnimator::isVisible() const -> bool { + return std::ranges::any_of(ships_, + [](const TitleShip& s) { return s.visible; }); + } - // Lerp scale (grande → normal) - ship.current_scale = Easing::lerp(ship.initial_scale, ship.target_scale, eased_progress); + auto ShipAnimator::isAnimationComplete() const -> bool { + return std::ranges::all_of(ships_, + [](const TitleShip& s) { return !s.visible; }); + } - // Transicionar a FLOATING cuando completi - if (elapsed >= ENTRY_DURATION) { + void ShipAnimator::updateEntering(TitleShip& 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); + + // Acumula la fase d'oscil·lació també durant ENTERING; sense això, + // al passar a FLOATING la posició salta d'amplitud_y de cop perquè + // l'offset Y comença a sin(π/2) = 1. Acumulant-la abans, la nau + // ja oscil·la mentre s'aproxima i la transició és contínua. + ship.oscillation_phase += delta_time; + + const float INTERP_X = Easing::lerp(ship.initial_position.x, ship.target_position.x, EASED); + const float INTERP_Y = Easing::lerp(ship.initial_position.y, ship.target_position.y, EASED); + const float INTERP_Z = Easing::lerp(ship.initial_position.z, ship.target_position.z, EASED); + + 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 = INTERP_X + OFFSET_X; + ship.current_position.y = INTERP_Y + OFFSET_Y; + ship.current_position.z = INTERP_Z; + ship.current_scale = Easing::lerp(ship.initial_scale, ship.target_scale, EASED); + + if (ELAPSED >= ENTRY_DURATION) { + ship.state = ShipState::FLOATING; + ship.state_time = 0.0F; + // No resetegem oscillation_phase: així updateFloating continua + // l'oscil·lació iniciada durant ENTERING sense salt. + ship.forward_dir = floatingForward(ship.target_position); + } + } + + void ShipAnimator::updateFloating(TitleShip& 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 ShipAnimator::updateExiting(TitleShip& 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); + + // Destí: punt fix a (VP.x, VP.y, VP.z + OVERFLOW). Cada nau s'apropa + // al centre projectat des del seu costat sense creuar el VP. + const Vec3 EXIT_DEST{ + .x = VANISHING_POINT.x, + .y = VANISHING_POINT.y, + .z = VANISHING_POINT.z + SHIP_EXIT_OVERFLOW, + }; + ship.current_position.x = Easing::lerp(ship.initial_position.x, EXIT_DEST.x, EASED); + ship.current_position.y = Easing::lerp(ship.initial_position.y, EXIT_DEST.y, EASED); + ship.current_position.z = Easing::lerp(ship.initial_position.z, EXIT_DEST.z, EASED); + ship.current_scale = ship.target_scale; // L'escala visual baixa via la perspectiva + + if (PROGRESS >= 1.0F) { + ship.visible = false; + } + } + + void ShipAnimator::configureShipP1(TitleShip& ship) { ship.state = ShipState::FLOATING; ship.state_time = 0.0F; - ship.oscillation_phase = 0.0F; // Reiniciar fase de oscil·lació + // Target i initial sobre el path VP → "les 7" del rellotge (P1). + ship.target_position = VANISHING_POINT + (PATH_DIR_P1 * TARGET_DIST); + ship.initial_position = VANISHING_POINT + (PATH_DIR_P1 * ENTRY_DIST); + 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.forward_dir = entryForward(ship); + ship.visible = true; } -} -void ShipAnimator::updateFloating(TitleShip& ship, float delta_time) { - using namespace Defaults::Title::Ships; - - // Actualitzar time i fase de oscil·lació - ship.state_time += delta_time; - ship.oscillation_phase += delta_time; - - // Oscil·lació sinusoïdal X/Y (parámetros 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ón objetivo - ship.current_position.x = ship.target_position.x + offset_x; - ship.current_position.y = ship.target_position.y + offset_y; - - // Escala constant (sin "breathing" per ara) - ship.current_scale = ship.target_scale; -} - -void ShipAnimator::updateExiting(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 aceleración hacia el point de fuga) - float eased_progress = Easing::easeInQuad(progress); - - // Vec2 de fuga (centro del starfield) - constexpr Vec2 VANISHING_POINT{.x = VANISHING_POINT_X, .y = VANISHING_POINT_Y}; - - // Lerp posición hacia el point de fuga (preservar posición inicial actual) - // Nota: initial_position conté la posición on estava cuando es va activar EXITING - ship.current_position.x = Easing::lerp(ship.initial_position.x, VANISHING_POINT.x, eased_progress); - ship.current_position.y = Easing::lerp(ship.initial_position.y, VANISHING_POINT.y, eased_progress); - - // Escala redueix a 0 (simula Z → infinit) - ship.current_scale = ship.target_scale * (1.0F - eased_progress); - - // Marcar invisible cuando l'animación completi - if (progress >= 1.0F) { - ship.visible = false; + void ShipAnimator::configureShipP2(TitleShip& ship) { + ship.state = ShipState::FLOATING; + ship.state_time = 0.0F; + // Target i initial sobre el path VP → "les 5" del rellotge (P2). + ship.target_position = VANISHING_POINT + (PATH_DIR_P2 * TARGET_DIST); + ship.initial_position = VANISHING_POINT + (PATH_DIR_P2 * ENTRY_DIST); + 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.forward_dir = entryForward(ship); + ship.visible = true; } -} - -// Configuración -void ShipAnimator::configureShipP1(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 = p1TargetX(), .y = p1TargetY()}; - - // Calcular posición inicial (fuera de pantalla) - ship.initial_position = computeOffscreenPosition(CLOCK_8_ANGLE); - ship.current_position = ship.initial_position; // Començar fuera 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ámetros de entrada - ship.entry_delay = P1_ENTRY_DELAY; - - // Parámetros de 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::configureShipP2(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 = p2TargetX(), .y = p2TargetY()}; - - // Calcular posición inicial (fuera de pantalla) - ship.initial_position = computeOffscreenPosition(CLOCK_4_ANGLE); - ship.current_position = ship.initial_position; // Començar fuera 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ámetros de entrada - ship.entry_delay = P2_ENTRY_DELAY; - - // Parámetros de 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; -} - -auto ShipAnimator::computeOffscreenPosition(float angle_rellotge) -> Vec2 { - using namespace Defaults::Title::Ships; - - // Convertir angle del rellotge a radians (per exemple: 240° per clock 8) - // Calcular posición en direcció radial des del centro, pero 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 diff --git a/source/game/title/ship_animator.hpp b/source/game/title/ship_animator.hpp index fa1af4f..24cc7fd 100644 --- a/source/game/title/ship_animator.hpp +++ b/source/game/title/ship_animator.hpp @@ -1,104 +1,85 @@ -// ship_animator.hpp - Sistema de animación de naves para l'escena de título +// ship_animator.hpp - Sistema d'animació de naus 3D per a l'escena de títol // © 2026 JailDesigner +// +// 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 "core/rendering/render_context.hpp" - -#include - #include #include -#include -#include "core/graphics/shape.hpp" +#include "core/graphics/camera3d.hpp" +#include "core/graphics/wireframe3d.hpp" +#include "core/rendering/render_context.hpp" #include "core/types.hpp" namespace Title { -// Estats de l'animación de la ship -enum class ShipState : std::uint8_t { - ENTERING, // Entrant desde fuera de pantalla - FLOATING, // Flotante en posición estàtica - EXITING // Volant hacia el point de fuga -}; + enum class ShipState : std::uint8_t { + ENTERING, + FLOATING, + EXITING, + }; -// Dades de una ship individual al título. -// Todos los miembros tienen inicializador por defecto: ShipAnimator::ships_ -// es un std::array y sin estos defaults los campos primitivos -// quedarían indeterminados al instanciar el animador. -struct TitleShip { - // Identificació - int player_id{0}; // 1 o 2 - - // Estat + struct TitleShip { + int player_id{0}; ShipState state{ShipState::ENTERING}; - float state_time{0.0F}; // Temps acumulat en l'state actual + float state_time{0.0F}; - // Posicions - Vec2 initial_position{}; // Posición de start (fuera de pantalla per ENTERING) - Vec2 target_position{}; // Posición objetivo (rellotge 8 o 4) - Vec2 current_position{}; // Posición interpolada actual + Vec3 initial_position{}; + Vec3 target_position{}; + Vec3 current_position{}; - // Escales (simulació eix Z) - float initial_scale{1.0F}; // Escala de start (més grande = més a prop) - float target_scale{1.0F}; // Escala objetivo (mida flotació) - float current_scale{1.0F}; // Escala interpolada actual + float initial_scale{1.0F}; + float target_scale{1.0F}; + float current_scale{1.0F}; - // Flotació - float oscillation_phase{0.0F}; // Acumulador de fase per movement sinusoïdal + float oscillation_phase{0.0F}; + float entry_delay{0.0F}; - // Parámetros de entrada - float entry_delay{0.0F}; // Delay antes de entrar (0.0 per P1, 0.5 per P2) - - // Parámetros de oscil·lació per ship float amplitude_x{0.0F}; float amplitude_y{0.0F}; float frequency_x{0.0F}; float frequency_y{0.0F}; - // Forma - std::shared_ptr shape; - - // Visibilitat + Graphics::Mesh3D mesh; + // Vector mundial cap a on apunta el front del shape. Recalculat a cada + // transició d'estat perquè draw() oriente la nau (look-at) en la + // direcció del seu path actual. + Vec3 forward_dir{.x = 0.0F, .y = 0.0F, .z = 1.0F}; bool visible{false}; -}; + }; -// Gestor de animación de naves para l'escena de título -class ShipAnimator { - public: - explicit ShipAnimator(Rendering::Renderer* renderer); + class ShipAnimator { + public: + ShipAnimator(Rendering::Renderer* renderer, const Graphics::Camera3D* camera); - // Cicle de vida void init(); void update(float delta_time); void draw() const; - // Control de state (cridat per TitleScene) void startEntryAnimation(); - void triggerExitAnimation(); // Anima todas las naves - void triggerExitAnimationForPlayer(int player_id); // Anima solo una ship (P1=1, P2=2) - void skipToFloatingState(); // Salta directament a FLOATING sin animación + void triggerExitAnimation(); + void triggerExitAnimationForPlayer(int player_id); + void skipToFloatingState(); - // Control de visibilitat void setVisible(bool visible); [[nodiscard]] auto isAnimationComplete() const -> bool; - [[nodiscard]] auto isVisible() const -> bool; // Comprova si alguna ship es visible + [[nodiscard]] auto isVisible() const -> bool; - private: + private: Rendering::Renderer* renderer_; - std::array ships_; // Naves P1 i P2 + const Graphics::Camera3D* camera_; + std::array ships_; - // Métodos de animación. Estáticos: solo modifican el TitleShip pasado, - // sin tocar otros miembros del ShipAnimator. static void updateEntering(TitleShip& ship, float delta_time); static void updateFloating(TitleShip& ship, float delta_time); static void updateExiting(TitleShip& ship, float delta_time); - - // Configuración (también estáticos: trabajan sobre el ship pasado). static void configureShipP1(TitleShip& ship); static void configureShipP2(TitleShip& ship); - [[nodiscard]] static auto computeOffscreenPosition(float angle_rellotge) -> Vec2; -}; + }; } // namespace Title diff --git a/source/game/title/ship_animator3d.cpp b/source/game/title/ship_animator3d.cpp deleted file mode 100644 index 697dc61..0000000 --- a/source/game/title/ship_animator3d.cpp +++ /dev/null @@ -1,344 +0,0 @@ -// 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). - // 0.0F → emet només la silueta plana. >0 emet volum extrudit. - constexpr float SHIP_EXTRUSION_DEPTH = 1.0F; - - // VP lògic per definir forward_dir / direcció del path. Tots els paths - // s'allunyen cap a aquest punt; les naus exiting continuen MÉS ENLLÀ - // (vegeu SHIP_EXIT_TRAVEL) per no desaparèixer en arribar al VP. - constexpr float SHIP_EXIT_Z = 800.0F; - constexpr Vec3 VANISHING_POINT{.x = 0.0F, .y = 0.0F, .z = SHIP_EXIT_Z}; - - // Profunditat addicional darrere del VP cap a la qual les naus exiting - // convergeixen. Així P1 (X<0) i P2 (X>0) mantenen sempre els seus - // hemisferis i no es creuen al passar pel VP — totes dues acaben al - // centre projectat (640, 360) sense travessar-lo. - constexpr float SHIP_EXIT_OVERFLOW = 700.0F; - - // Directors VP → origen de cada nau, normalitzats. P1 ve des de "les 7" - // del rellotge (baix-esquerra), P2 des de "les 5" (baix-dreta). Els - // components estan calibrats perquè a TARGET_DIST el pixel projectat - // caigui aprox sota la "P de PRESS" / "Y de PLAY" del text del títol. - constexpr Vec3 PATH_DIR_P1{.x = -0.0887F, .y = -0.0683F, .z = -0.9938F}; - constexpr Vec3 PATH_DIR_P2{.x = +0.0887F, .y = -0.0683F, .z = -0.9938F}; - - // Distàncies des del VP al llarg del path (unitats mundials). - // Reduïm TARGET_DIST per acostar el descans al VP (puja en pantalla, - // s'allunya de PRESS START); compensem amb SHIP_FLOAT_SCALE més gran. - constexpr float TARGET_DIST = 480.0F; // Descans a Z≈323 → pixel ≈ (558, 423) - constexpr float ENTRY_DIST = 770.0F; // Inicial a Z≈35 → fora pantalla baix-esq. - - // Pitch addicional sobre el look-at pur per fer que el dors de la nau - // s'incline cap a la càmera (~-14° afegits). Amb forward quasi paral·lel - // a +Z, el pitch look-at és ~-94°; afegint això queda al voltant de -108°, - // que és l'angle visualment validat com a "bo" per l'usuari. - constexpr float PITCH_LIFT_RAD = -0.25F; - - // Look-at: calcula pitch+yaw que duen (0,-1,0) local a forward_dir mundial. - // Requereix l'ordre de rotació X→Y→Z al applyTransform de wireframe3d. - // Si forward és quasi vertical (sin(pitch) ≈ 0), retorna yaw=0 (qualsevol). - auto computePitchYawForLookAt(const Vec3& forward_dir) -> Vec2 { - const float DY = std::clamp(forward_dir.y, -1.0F, 1.0F); - const float PITCH_LOOKAT = -std::acos(-DY); // ∈ [-π, 0] - const float SIN_PITCH = std::sin(PITCH_LOOKAT); - float yaw = 0.0F; - if (std::abs(SIN_PITCH) >= 1.0E-5F) { - const float SY = -forward_dir.x / SIN_PITCH; - const float CY = -forward_dir.z / SIN_PITCH; - yaw = std::atan2(SY, CY); - } - return Vec2{.x = PITCH_LOOKAT + PITCH_LIFT_RAD, .y = yaw}; - } - - auto safeNormalize(const Vec3& v, const Vec3& fallback) -> Vec3 { - return v.lengthSquared() > 0.0F ? v.normalized() : fallback; - } - - auto entryForward(const TitleShip3D& ship) -> Vec3 { - return safeNormalize(ship.target_position - ship.initial_position, - Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F}); - } - - auto floatingForward(const Vec3& target) -> Vec3 { - return safeNormalize(VANISHING_POINT - target, - Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F}); - } - - auto exitForward(const Vec3& current) -> Vec3 { - return safeNormalize(VANISHING_POINT - current, - Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F}); - } - - // Mida visual i animació. - constexpr float SHIP_FLOAT_SCALE = 2.0F; - constexpr float SHIP_ENTRY_SCALE = 2.0F; // Mida mundial idèntica; la perspectiva fa la resta - constexpr float ENTRY_DURATION = 2.0F; - constexpr float EXIT_DURATION = 1.5F; - - // 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 Vec2 EULER = computePitchYawForLookAt(ship.forward_dir); - const Graphics::Transform3D TRANSFORM{ - .position = ship.current_position, - .rotation_euler = Vec3{.x = EULER.x, .y = EULER.y, .z = 0.0F}, - .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; - ship.forward_dir = entryForward(ship); - } - } - - void ShipAnimator3D::triggerExitAnimation() { - for (auto& ship : ships_) { - ship.state = ShipState3D::EXITING; - ship.state_time = 0.0F; - ship.initial_position = ship.current_position; - ship.forward_dir = exitForward(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; - ship.forward_dir = exitForward(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; - ship.forward_dir = floatingForward(ship.target_position); - } - } - - 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); - - // Acumula la fase d'oscil·lació també durant ENTERING; sense això, - // al passar a FLOATING la posició salta d'amplitud_y de cop perquè - // l'offset Y comença a sin(π/2) = 1. Acumulant-la abans, la nau - // ja oscil·la mentre s'aproxima i la transició és contínua. - ship.oscillation_phase += delta_time; - - const float INTERP_X = Easing::lerp(ship.initial_position.x, ship.target_position.x, EASED); - const float INTERP_Y = Easing::lerp(ship.initial_position.y, ship.target_position.y, EASED); - const float INTERP_Z = Easing::lerp(ship.initial_position.z, ship.target_position.z, EASED); - - 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 = INTERP_X + OFFSET_X; - ship.current_position.y = INTERP_Y + OFFSET_Y; - ship.current_position.z = INTERP_Z; - 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; - // No resetegem oscillation_phase: així updateFloating continua - // l'oscil·lació iniciada durant ENTERING sense salt. - ship.forward_dir = floatingForward(ship.target_position); - } - } - - 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); - - // Destí: punt fix a (VP.x, VP.y, VP.z + OVERFLOW). Cada nau s'apropa - // al centre projectat des del seu costat sense creuar el VP. - const Vec3 EXIT_DEST{ - .x = VANISHING_POINT.x, - .y = VANISHING_POINT.y, - .z = VANISHING_POINT.z + SHIP_EXIT_OVERFLOW, - }; - ship.current_position.x = Easing::lerp(ship.initial_position.x, EXIT_DEST.x, EASED); - ship.current_position.y = Easing::lerp(ship.initial_position.y, EXIT_DEST.y, EASED); - ship.current_position.z = Easing::lerp(ship.initial_position.z, EXIT_DEST.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; - // Target i initial sobre el path VP → "les 7" del rellotge (P1). - ship.target_position = VANISHING_POINT + (PATH_DIR_P1 * TARGET_DIST); - ship.initial_position = VANISHING_POINT + (PATH_DIR_P1 * ENTRY_DIST); - 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.forward_dir = entryForward(ship); - ship.visible = true; - } - - void ShipAnimator3D::configureShipP2(TitleShip3D& ship) { - ship.state = ShipState3D::FLOATING; - ship.state_time = 0.0F; - // Target i initial sobre el path VP → "les 5" del rellotge (P2). - ship.target_position = VANISHING_POINT + (PATH_DIR_P2 * TARGET_DIST); - ship.initial_position = VANISHING_POINT + (PATH_DIR_P2 * ENTRY_DIST); - 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.forward_dir = entryForward(ship); - ship.visible = true; - } - -} // namespace Title diff --git a/source/game/title/ship_animator3d.hpp b/source/game/title/ship_animator3d.hpp deleted file mode 100644 index dee4177..0000000 --- a/source/game/title/ship_animator3d.hpp +++ /dev/null @@ -1,85 +0,0 @@ -// 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; - // Vector mundial cap a on apunta el front del shape. Recalculat a cada - // transició d'estat perquè draw() oriente la nau (look-at) en la - // direcció del seu path actual. - Vec3 forward_dir{.x = 0.0F, .y = 0.0F, .z = 1.0F}; - 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