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/camera3d.cpp b/source/core/graphics/camera3d.cpp new file mode 100644 index 0000000..b247ad5 --- /dev/null +++ b/source/core/graphics/camera3d.cpp @@ -0,0 +1,92 @@ +// camera3d.cpp - Implementació de la càmera 3D amb projecció en CPU +// © 2026 JailDesigner + +#include "core/graphics/camera3d.hpp" + +#include + +namespace Graphics { + + Camera3D::Camera3D(const Vec3& position, const Vec3& target, const Vec3& up_world, float fov_y_rad, float viewport_w, float viewport_h, float near_plane, float far_plane) + : position_(position), + target_(target), + up_world_(up_world), + fov_y_rad_(fov_y_rad), + viewport_w_(viewport_w), + viewport_h_(viewport_h), + near_(near_plane), + far_(far_plane) { + recomputeBasis(); + recomputeFocal(); + } + + void Camera3D::setPosition(const Vec3& p) { + position_ = p; + recomputeBasis(); + } + + void Camera3D::setTarget(const Vec3& t) { + target_ = t; + recomputeBasis(); + } + + void Camera3D::setUpWorld(const Vec3& u) { + up_world_ = u; + recomputeBasis(); + } + + void Camera3D::setViewport(float w, float h) { + viewport_w_ = w; + viewport_h_ = h; + recomputeFocal(); + } + + void Camera3D::setFovY(float fov_y_rad) { + fov_y_rad_ = fov_y_rad; + recomputeFocal(); + } + + void Camera3D::recomputeBasis() { + // Forward = del position cap al target. + forward_ = (target_ - position_).normalized(); + // Right = up_world × forward (convenció right-handed amb Y up, + // mirant cap a +Z → right cau a +X). L'invers (forward × up_world) + // donava la base mirall i invertia l'eix X de la projecció. + right_ = up_world_.cross(forward_).normalized(); + // Up ortogonal real = forward × right (manté la mà dreta). + up_ = forward_.cross(right_).normalized(); + } + + void Camera3D::recomputeFocal() { + // Focal length en píxels: (viewport_height / 2) / tan(fov_y / 2). + // Assumeix píxels quadrats (focal_x == focal_y). + const float HALF_FOV = fov_y_rad_ * 0.5F; + const float TAN_HALF = std::tan(HALF_FOV); + focal_ = (TAN_HALF > 0.0F) ? ((viewport_h_ * 0.5F) / TAN_HALF) : 0.0F; + centre_x_ = viewport_w_ * 0.5F; + centre_y_ = viewport_h_ * 0.5F; + } + + auto Camera3D::project(const Vec3& world) const -> std::optional { + const Vec3 REL = world - position_; + const float CX = REL.dot(right_); + const float CY = REL.dot(up_); + const float CZ = REL.dot(forward_); + + if (CZ <= near_) { + return std::nullopt; + } + + const float SCALE = focal_ / CZ; + return ProjectedPoint{ + .screen = Vec2{ + .x = centre_x_ + (CX * SCALE), + // Flip Y: en pantalla Y creix cap avall. + .y = centre_y_ - (CY * SCALE), + }, + .scale = SCALE, + .depth = CZ, + }; + } + +} // namespace Graphics diff --git a/source/core/graphics/camera3d.hpp b/source/core/graphics/camera3d.hpp new file mode 100644 index 0000000..0cc41fd --- /dev/null +++ b/source/core/graphics/camera3d.hpp @@ -0,0 +1,60 @@ +// camera3d.hpp - Càmera 3D amb projecció en perspectiva en CPU +// © 2026 JailDesigner +// +// La càmera viu en l'espai mundial (X dreta, Y amunt, Z davant). El mètode +// project() pren un Vec3 mundial i torna les coordenades 2D en píxels lògics +// de pantalla, més el factor d'escala focal/depth (útil per renderShape). +// Si el punt queda darrere del near plane, torna std::nullopt. + +#pragma once + +#include + +#include "core/types.hpp" + +namespace Graphics { + + class Camera3D { + public: + struct ProjectedPoint { + Vec2 screen; // Píxels lògics + float scale; // focal / depth (escala visual a aquesta Z) + float depth; // Profunditat en l'espai de càmera (cz) + }; + + Camera3D(const Vec3& position, const Vec3& target, const Vec3& up_world, float fov_y_rad, float viewport_w, float viewport_h, float near_plane = 0.1F, float far_plane = 2000.0F); + + void setPosition(const Vec3& p); + void setTarget(const Vec3& t); + void setUpWorld(const Vec3& u); + void setViewport(float w, float h); + void setFovY(float fov_y_rad); + + [[nodiscard]] auto project(const Vec3& world) const -> std::optional; + + [[nodiscard]] auto position() const -> const Vec3& { return position_; } + [[nodiscard]] auto forward() const -> const Vec3& { return forward_; } + [[nodiscard]] auto nearPlane() const -> float { return near_; } + [[nodiscard]] auto farPlane() const -> float { return far_; } + + private: + void recomputeBasis(); + void recomputeFocal(); + + Vec3 position_{}; + Vec3 target_{}; + Vec3 up_world_{}; + Vec3 right_{.x = 1.0F, .y = 0.0F, .z = 0.0F}; + Vec3 up_{.x = 0.0F, .y = 1.0F, .z = 0.0F}; + Vec3 forward_{.x = 0.0F, .y = 0.0F, .z = 1.0F}; + float fov_y_rad_{0.0F}; + float viewport_w_{0.0F}; + float viewport_h_{0.0F}; + float near_{0.1F}; + float far_{2000.0F}; + float focal_{0.0F}; + float centre_x_{0.0F}; + float centre_y_{0.0F}; + }; + +} // namespace Graphics 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/wireframe3d.cpp b/source/core/graphics/wireframe3d.cpp new file mode 100644 index 0000000..4408feb --- /dev/null +++ b/source/core/graphics/wireframe3d.cpp @@ -0,0 +1,185 @@ +// wireframe3d.cpp - Implementació dels meshos 3D wireframe +// © 2026 JailDesigner + +#include "core/graphics/wireframe3d.hpp" + +#include +#include + +#include "core/rendering/line_renderer.hpp" + +namespace Graphics { + + auto applyTransform(const Transform3D& transform, const Vec3& local) -> Vec3 { + // 1. Escala uniforme. + Vec3 v{ + .x = local.x * transform.scale, + .y = local.y * transform.scale, + .z = local.z * transform.scale, + }; + + // Ordre X → Y → Z: amb aquest ordre, una rotació pitch+yaw pot dur el + // vector local (0,-1,0) a qualsevol direcció mundial — necessari perquè + // les naus calculen pitch+yaw look-at per alinear-se amb el seu path. + // L'ordre invers (Y→X) no permet X arbitrari en vectors sobre l'eix Y. + + // 2. Rotació X (pitch): Y i Z. + const float CX = std::cos(transform.rotation_euler.x); + const float SX = std::sin(transform.rotation_euler.x); + { + const float NY = (v.y * CX) - (v.z * SX); + const float NZ = (v.y * SX) + (v.z * CX); + v.y = NY; + v.z = NZ; + } + + // 3. Rotació Y (yaw): X i Z. + const float CY = std::cos(transform.rotation_euler.y); + const float SY = std::sin(transform.rotation_euler.y); + { + const float NX = (v.x * CY) + (v.z * SY); + const float NZ = (-v.x * SY) + (v.z * CY); + v.x = NX; + v.z = NZ; + } + + // 4. Rotació Z (roll): X i Y. + const float CZ = std::cos(transform.rotation_euler.z); + const float SZ = std::sin(transform.rotation_euler.z); + { + const float NX = (v.x * CZ) - (v.y * SZ); + const float NY = (v.x * SZ) + (v.y * CZ); + v.x = NX; + v.y = NY; + } + + // 5. Translació final. + v.x += transform.position.x; + v.y += transform.position.y; + v.z += transform.position.z; + return v; + } + + void drawWireframe(Rendering::Renderer* renderer, const Camera3D& camera, const Mesh3D& mesh, const Transform3D& transform, float brightness, SDL_Color color) { + if (renderer == nullptr || mesh.edges.empty() || mesh.vertices.empty()) { + return; + } + + // Projecta tots els vèrtexs un cop; cau-en si queden darrere del near. + std::vector> projected; + projected.reserve(mesh.vertices.size()); + for (const auto& vertex : mesh.vertices) { + const Vec3 WORLD = applyTransform(transform, vertex); + projected.push_back(camera.project(WORLD)); + } + + for (const auto& edge : mesh.edges) { + const auto& a_proj = projected[edge.first]; + const auto& b_proj = projected[edge.second]; + if (!a_proj.has_value() || !b_proj.has_value()) { + continue; + } + Rendering::linea(renderer, + static_cast(a_proj->screen.x), + static_cast(a_proj->screen.y), + static_cast(b_proj->screen.x), + static_cast(b_proj->screen.y), + brightness, + 0.0F, + color); + } + } + + auto makeOctahedron() -> Mesh3D { + // 6 vèrtexs als eixos: ±X, ±Y, ±Z. + Mesh3D mesh; + mesh.vertices = { + {.x = 1.0F, .y = 0.0F, .z = 0.0F}, // 0: +X + {.x = -1.0F, .y = 0.0F, .z = 0.0F}, // 1: -X + {.x = 0.0F, .y = 1.0F, .z = 0.0F}, // 2: +Y + {.x = 0.0F, .y = -1.0F, .z = 0.0F}, // 3: -Y + {.x = 0.0F, .y = 0.0F, .z = 1.0F}, // 4: +Z + {.x = 0.0F, .y = 0.0F, .z = -1.0F}, // 5: -Z + }; + // 12 arestes: cada vèrtex axial connecta amb els 4 vèrtexs no oposats. + mesh.edges = { + // "Equador" XY al voltant de Z. + {2, 0}, + {0, 3}, + {3, 1}, + {1, 2}, + // Piràmide superior (cap a +Z). + {2, 4}, + {0, 4}, + {3, 4}, + {1, 4}, + // Piràmide inferior (cap a -Z). + {2, 5}, + {0, 5}, + {3, 5}, + {1, 5}, + }; + return mesh; + } + + auto extrudeShape2D(const Shape& shape, float depth) -> Mesh3D { + Mesh3D mesh; + if (!shape.isValid()) { + return mesh; + } + + const float HALF = depth * 0.5F; + const Vec2 CENTRE = shape.getCenter(); + // Si depth <= 0, emetem només un pla (sense vèrtexs back ni connexions) + // per evitar arestes degenerades i acumulació additiva de brightness. + const bool FLAT = (depth <= 0.0F); + + for (const auto& primitive : shape.getPrimitives()) { + if (primitive.points.size() < 2) { + continue; + } + + const auto BASE = static_cast(mesh.vertices.size()); + const auto N = static_cast(primitive.points.size()); + + // Vèrtexs frontals (z = +HALF, o z = 0 si FLAT). + for (const auto& p : primitive.points) { + mesh.vertices.push_back(Vec3{ + .x = p.x - CENTRE.x, + .y = p.y - CENTRE.y, + .z = HALF, + }); + } + // Arestes "frontals": connecten punts consecutius de la polyline. + for (std::uint16_t i = 0; i + 1 < N; ++i) { + mesh.edges.emplace_back(BASE + i, BASE + i + 1); + } + + if (FLAT) { + continue; + } + + // Vèrtexs posteriors (z = -HALF) i arestes corresponents. + for (const auto& p : primitive.points) { + mesh.vertices.push_back(Vec3{ + .x = p.x - CENTRE.x, + .y = p.y - CENTRE.y, + .z = -HALF, + }); + } + for (std::uint16_t i = 0; i + 1 < N; ++i) { + mesh.edges.emplace_back(BASE + N + i, BASE + N + i + 1); + } + // Arestes de connexió front↔posterior per cada vèrtex. + // Per polylines tancades (primer == últim punt), el bucle igualment + // genera N connexions; el parell duplicat (primer i últim) cau en una + // línia idèntica sense efecte visible. + for (std::uint16_t i = 0; i < N; ++i) { + mesh.edges.emplace_back(BASE + i, BASE + N + i); + } + } + + return mesh; + } + +} // namespace Graphics diff --git a/source/core/graphics/wireframe3d.hpp b/source/core/graphics/wireframe3d.hpp new file mode 100644 index 0000000..a57d9dd --- /dev/null +++ b/source/core/graphics/wireframe3d.hpp @@ -0,0 +1,60 @@ +// wireframe3d.hpp - Meshos 3D wireframe i utilitats per dibuixar-los +// © 2026 JailDesigner +// +// Mesh3D = llista de vèrtexs Vec3 + llista d'arestes (parells d'índexs). +// drawWireframe() aplica una Transform3D al mesh, projecta amb Camera3D i +// emet cada aresta com una línia 2D pel pipeline `Rendering::linea` (mateix +// pipeline que la resta del joc: glow verd via ColorOscillator si color.a==0). +// +// Sense depth buffer: el caller és responsable d'ordenar els meshos per +// profunditat decreixent si vol oclusió coherent (la pipeline és LINE_LIST +// amb alpha blend additiu). + +#pragma once + +#include + +#include +#include +#include + +#include "core/graphics/camera3d.hpp" +#include "core/graphics/shape.hpp" +#include "core/rendering/render_context.hpp" +#include "core/types.hpp" + +namespace Graphics { + + struct Mesh3D { + std::vector vertices; + std::vector> edges; + }; + + struct Transform3D { + Vec3 position{}; + // Euler en radians, aplicat en ordre Y (yaw) → X (pitch) → Z (roll). + Vec3 rotation_euler{}; + float scale{1.0F}; + }; + + // Aplica la Transform3D a un vèrtex local del mesh per obtenir-ne la posició + // mundial. Ordre: scale → rotate (Y,X,Z) → translate. + [[nodiscard]] auto applyTransform(const Transform3D& transform, const Vec3& local) -> Vec3; + + // Dibuixa el mesh en wireframe a través de la càmera donada. Cada aresta es + // projecta en CPU i s'emet via `Rendering::linea`. Les arestes amb algun extrem + // darrere del near plane es descarten per complet (clipping primitiu). + // - brightness: multiplicador aplicat al color de línia. + // - color: si alpha == 0, usa el color global del oscil·lador (glow verd). + void drawWireframe(Rendering::Renderer* renderer, const Camera3D& camera, const Mesh3D& mesh, const Transform3D& transform, float brightness = 1.0F, SDL_Color color = {.r = 0, .g = 0, .b = 0, .a = 0}); + + // Factory: octaedre regular amb 6 vèrtexs als eixos a distància 1 i 12 arestes. + // Pensat com a estrella 3D al starfield (escalable amb Transform3D::scale). + [[nodiscard]] auto makeOctahedron() -> Mesh3D; + + // Factory: extrusió en Z d'un shape 2D. Cada polyline genera dues còpies + // (z = +depth/2 i z = -depth/2) més arestes de connexió frontal↔posterior + // per cada vèrtex de la polyline. + [[nodiscard]] auto extrudeShape2D(const Shape& shape, float depth) -> Mesh3D; + +} // namespace Graphics diff --git a/source/core/system/scene_context.hpp b/source/core/system/scene_context.hpp index bd466e3..7cf3c30 100644 --- a/source/core/system/scene_context.hpp +++ b/source/core/system/scene_context.hpp @@ -9,16 +9,16 @@ namespace SceneManager { -// Context de transición entre escenes -// Conté l'escena destinació i opciones específiques per aquella escena -class SceneContext { - public: + // Context de transición entre escenes + // Conté l'escena destinació i opciones específiques per aquella escena + class SceneContext { + public: // Tipo de escena del juego enum class SceneType : std::uint8_t { LOGO, // Pantalla de start (logo JAILGAMES) - TITLE, // Pantalla de título con menú - GAME, // Juego principal (Asteroids) - EXIT // Salir del programa + TITLE, // Pantalla de título (3D) + GAME, // Juego principal (Asteroids) + EXIT // Salir del programa }; // Opciones específiques para cada escena @@ -70,14 +70,14 @@ class SceneContext { return match_config_; } - private: - SceneType next_scene_{SceneType::LOGO}; // SceneType a la qual transicionar - Option option_{Option::NONE}; // Opción específica per l'escena - GameConfig::MatchConfig match_config_; // Configuración de match (jugadors active, mode) -}; + private: + SceneType next_scene_{SceneType::LOGO}; // SceneType a la qual transicionar + Option option_{Option::NONE}; // Opción específica per l'escena + GameConfig::MatchConfig match_config_; // Configuración de match (jugadors active, mode) + }; -// Variable global inline per gestionar l'escena actual (backward compatibility) -// Sincronitzada con context.nextScene() por el Director -inline SceneContext::SceneType actual = SceneContext::SceneType::LOGO; + // Variable global inline per gestionar l'escena actual (backward compatibility) + // Sincronitzada con context.nextScene() por el Director + inline SceneContext::SceneType actual = SceneContext::SceneType::LOGO; } // namespace SceneManager diff --git a/source/core/types.hpp b/source/core/types.hpp index e6433c4..ed75a6e 100644 --- a/source/core/types.hpp +++ b/source/core/types.hpp @@ -11,39 +11,39 @@ // y aggregate initialization clásica: // Vec2{1.0F, 2.0F} struct Vec2 { - float x{0.0F}; - float y{0.0F}; + float x{0.0F}; + float y{0.0F}; - constexpr auto operator+=(const Vec2& o) -> Vec2& { - x += o.x; - y += o.y; - return *this; - } - constexpr auto operator-=(const Vec2& o) -> Vec2& { - x -= o.x; - y -= o.y; - return *this; - } - constexpr auto operator*=(float s) -> Vec2& { - x *= s; - y *= s; - return *this; - } - constexpr auto operator/=(float s) -> Vec2& { - x /= s; - y /= s; - return *this; - } + constexpr auto operator+=(const Vec2& o) -> Vec2& { + x += o.x; + y += o.y; + return *this; + } + constexpr auto operator-=(const Vec2& o) -> Vec2& { + x -= o.x; + y -= o.y; + return *this; + } + constexpr auto operator*=(float s) -> Vec2& { + x *= s; + y *= s; + return *this; + } + constexpr auto operator/=(float s) -> Vec2& { + x /= s; + y /= s; + return *this; + } - [[nodiscard]] auto lengthSquared() const -> float { return (x * x) + (y * y); } - [[nodiscard]] auto length() const -> float { return std::sqrt(lengthSquared()); } - [[nodiscard]] auto dot(const Vec2& o) const -> float { return (x * o.x) + (y * o.y); } + [[nodiscard]] auto lengthSquared() const -> float { return (x * x) + (y * y); } + [[nodiscard]] auto length() const -> float { return std::sqrt(lengthSquared()); } + [[nodiscard]] auto dot(const Vec2& o) const -> float { return (x * o.x) + (y * o.y); } - // Devuelve el vector normalizado; si la magnitud es 0 devuelve {0,0}. - [[nodiscard]] auto normalized() const -> Vec2 { - const float L = length(); - return L > 0.0F ? Vec2{.x = x / L, .y = y / L} : Vec2{}; - } + // Devuelve el vector normalizado; si la magnitud es 0 devuelve {0,0}. + [[nodiscard]] auto normalized() const -> Vec2 { + const float L = length(); + return L > 0.0F ? Vec2{.x = x / L, .y = y / L} : Vec2{}; + } }; constexpr auto operator+(Vec2 a, const Vec2& b) -> Vec2 { @@ -70,3 +70,83 @@ constexpr auto operator-(const Vec2& v) -> Vec2 { return {.x = -v.x, .y = -v.y}; constexpr auto operator==(const Vec2& a, const Vec2& b) -> bool { return a.x == b.x && a.y == b.y; } + +// Vector 3D cartesià. Mateix patró d'aggregate que Vec2 per suportar +// designated initializers: Vec3{.x = 1.0F, .y = 2.0F, .z = 3.0F}. +// Convenció de mà dreta: X dreta, Y amunt, Z davant càmera. +struct Vec3 { + float x{0.0F}; + float y{0.0F}; + float z{0.0F}; + + constexpr auto operator+=(const Vec3& o) -> Vec3& { + x += o.x; + y += o.y; + z += o.z; + return *this; + } + constexpr auto operator-=(const Vec3& o) -> Vec3& { + x -= o.x; + y -= o.y; + z -= o.z; + return *this; + } + constexpr auto operator*=(float s) -> Vec3& { + x *= s; + y *= s; + z *= s; + return *this; + } + constexpr auto operator/=(float s) -> Vec3& { + x /= s; + y /= s; + z /= s; + return *this; + } + + [[nodiscard]] auto lengthSquared() const -> float { + return (x * x) + (y * y) + (z * z); + } + [[nodiscard]] auto length() const -> float { return std::sqrt(lengthSquared()); } + [[nodiscard]] auto dot(const Vec3& o) const -> float { + return (x * o.x) + (y * o.y) + (z * o.z); + } + [[nodiscard]] auto cross(const Vec3& o) const -> Vec3 { + return Vec3{ + .x = (y * o.z) - (z * o.y), + .y = (z * o.x) - (x * o.z), + .z = (x * o.y) - (y * o.x), + }; + } + [[nodiscard]] auto normalized() const -> Vec3 { + const float L = length(); + return L > 0.0F ? Vec3{.x = x / L, .y = y / L, .z = z / L} : Vec3{}; + } +}; + +constexpr auto operator+(Vec3 a, const Vec3& b) -> Vec3 { + a += b; + return a; +} +constexpr auto operator-(Vec3 a, const Vec3& b) -> Vec3 { + a -= b; + return a; +} +constexpr auto operator*(Vec3 v, float s) -> Vec3 { + v *= s; + return v; +} +constexpr auto operator*(float s, Vec3 v) -> Vec3 { + v *= s; + return v; +} +constexpr auto operator/(Vec3 v, float s) -> Vec3 { + v /= s; + return v; +} +constexpr auto operator-(const Vec3& v) -> Vec3 { + return {.x = -v.x, .y = -v.y, .z = -v.z}; +} +constexpr auto operator==(const Vec3& a, const Vec3& b) -> bool { + return a.x == b.x && a.y == b.y && a.z == b.z; +} diff --git a/source/game/scenes/title_scene.cpp b/source/game/scenes/title_scene.cpp index 1c76878..c3f90fa 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,95 @@ #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). + 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 +115,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 +163,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 +204,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 +227,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 +250,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 +261,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 +269,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 +304,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 +317,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 +425,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 +457,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 +519,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/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