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