Merge branch 'feat/title-3d': escena del títol migrada a 3D real
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,92 @@
|
||||
// camera3d.cpp - Implementació de la càmera 3D amb projecció en CPU
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "core/graphics/camera3d.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
Camera3D::Camera3D(const Vec3& position, const Vec3& target, const Vec3& up_world, float fov_y_rad, float viewport_w, float viewport_h, float near_plane, float far_plane)
|
||||
: position_(position),
|
||||
target_(target),
|
||||
up_world_(up_world),
|
||||
fov_y_rad_(fov_y_rad),
|
||||
viewport_w_(viewport_w),
|
||||
viewport_h_(viewport_h),
|
||||
near_(near_plane),
|
||||
far_(far_plane) {
|
||||
recomputeBasis();
|
||||
recomputeFocal();
|
||||
}
|
||||
|
||||
void Camera3D::setPosition(const Vec3& p) {
|
||||
position_ = p;
|
||||
recomputeBasis();
|
||||
}
|
||||
|
||||
void Camera3D::setTarget(const Vec3& t) {
|
||||
target_ = t;
|
||||
recomputeBasis();
|
||||
}
|
||||
|
||||
void Camera3D::setUpWorld(const Vec3& u) {
|
||||
up_world_ = u;
|
||||
recomputeBasis();
|
||||
}
|
||||
|
||||
void Camera3D::setViewport(float w, float h) {
|
||||
viewport_w_ = w;
|
||||
viewport_h_ = h;
|
||||
recomputeFocal();
|
||||
}
|
||||
|
||||
void Camera3D::setFovY(float fov_y_rad) {
|
||||
fov_y_rad_ = fov_y_rad;
|
||||
recomputeFocal();
|
||||
}
|
||||
|
||||
void Camera3D::recomputeBasis() {
|
||||
// Forward = del position cap al target.
|
||||
forward_ = (target_ - position_).normalized();
|
||||
// Right = up_world × forward (convenció right-handed amb Y up,
|
||||
// mirant cap a +Z → right cau a +X). L'invers (forward × up_world)
|
||||
// donava la base mirall i invertia l'eix X de la projecció.
|
||||
right_ = up_world_.cross(forward_).normalized();
|
||||
// Up ortogonal real = forward × right (manté la mà dreta).
|
||||
up_ = forward_.cross(right_).normalized();
|
||||
}
|
||||
|
||||
void Camera3D::recomputeFocal() {
|
||||
// Focal length en píxels: (viewport_height / 2) / tan(fov_y / 2).
|
||||
// Assumeix píxels quadrats (focal_x == focal_y).
|
||||
const float HALF_FOV = fov_y_rad_ * 0.5F;
|
||||
const float TAN_HALF = std::tan(HALF_FOV);
|
||||
focal_ = (TAN_HALF > 0.0F) ? ((viewport_h_ * 0.5F) / TAN_HALF) : 0.0F;
|
||||
centre_x_ = viewport_w_ * 0.5F;
|
||||
centre_y_ = viewport_h_ * 0.5F;
|
||||
}
|
||||
|
||||
auto Camera3D::project(const Vec3& world) const -> std::optional<ProjectedPoint> {
|
||||
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
|
||||
@@ -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 <optional>
|
||||
|
||||
#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<ProjectedPoint>;
|
||||
|
||||
[[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
|
||||
@@ -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 <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
|
||||
#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<float>(rand()) / static_cast<float>(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<float>(rand()) / static_cast<float>(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<float>(rand()) / static_cast<float>(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::size_t>(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<float>(rand()) / static_cast<float>(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<std::size_t> 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
|
||||
|
||||
@@ -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 <SDL3/SDL.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#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<Estrella> estrelles_;
|
||||
std::vector<CapaConfig> capes_; // Configuración de las 3 capes
|
||||
std::shared_ptr<Shape> shape_estrella_;
|
||||
Rendering::Renderer* renderer_;
|
||||
const Camera3D* camera_;
|
||||
std::vector<Star> 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
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// wireframe3d.cpp - Implementació dels meshos 3D wireframe
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "core/graphics/wireframe3d.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
#include "core/rendering/line_renderer.hpp"
|
||||
|
||||
namespace Graphics {
|
||||
|
||||
auto applyTransform(const Transform3D& transform, const Vec3& local) -> Vec3 {
|
||||
// 1. Escala uniforme.
|
||||
Vec3 v{
|
||||
.x = local.x * transform.scale,
|
||||
.y = local.y * transform.scale,
|
||||
.z = local.z * transform.scale,
|
||||
};
|
||||
|
||||
// Ordre X → Y → Z: amb aquest ordre, una rotació pitch+yaw pot dur el
|
||||
// vector local (0,-1,0) a qualsevol direcció mundial — necessari perquè
|
||||
// les naus calculen pitch+yaw look-at per alinear-se amb el seu path.
|
||||
// L'ordre invers (Y→X) no permet X arbitrari en vectors sobre l'eix Y.
|
||||
|
||||
// 2. Rotació X (pitch): Y i Z.
|
||||
const float CX = std::cos(transform.rotation_euler.x);
|
||||
const float SX = std::sin(transform.rotation_euler.x);
|
||||
{
|
||||
const float NY = (v.y * CX) - (v.z * SX);
|
||||
const float NZ = (v.y * SX) + (v.z * CX);
|
||||
v.y = NY;
|
||||
v.z = NZ;
|
||||
}
|
||||
|
||||
// 3. Rotació Y (yaw): X i Z.
|
||||
const float CY = std::cos(transform.rotation_euler.y);
|
||||
const float SY = std::sin(transform.rotation_euler.y);
|
||||
{
|
||||
const float NX = (v.x * CY) + (v.z * SY);
|
||||
const float NZ = (-v.x * SY) + (v.z * CY);
|
||||
v.x = NX;
|
||||
v.z = NZ;
|
||||
}
|
||||
|
||||
// 4. Rotació Z (roll): X i Y.
|
||||
const float CZ = std::cos(transform.rotation_euler.z);
|
||||
const float SZ = std::sin(transform.rotation_euler.z);
|
||||
{
|
||||
const float NX = (v.x * CZ) - (v.y * SZ);
|
||||
const float NY = (v.x * SZ) + (v.y * CZ);
|
||||
v.x = NX;
|
||||
v.y = NY;
|
||||
}
|
||||
|
||||
// 5. Translació final.
|
||||
v.x += transform.position.x;
|
||||
v.y += transform.position.y;
|
||||
v.z += transform.position.z;
|
||||
return v;
|
||||
}
|
||||
|
||||
void drawWireframe(Rendering::Renderer* renderer, const Camera3D& camera, const Mesh3D& mesh, const Transform3D& transform, float brightness, SDL_Color color) {
|
||||
if (renderer == nullptr || mesh.edges.empty() || mesh.vertices.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Projecta tots els vèrtexs un cop; cau-en si queden darrere del near.
|
||||
std::vector<std::optional<Camera3D::ProjectedPoint>> 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<int>(a_proj->screen.x),
|
||||
static_cast<int>(a_proj->screen.y),
|
||||
static_cast<int>(b_proj->screen.x),
|
||||
static_cast<int>(b_proj->screen.y),
|
||||
brightness,
|
||||
0.0F,
|
||||
color);
|
||||
}
|
||||
}
|
||||
|
||||
auto makeOctahedron() -> Mesh3D {
|
||||
// 6 vèrtexs als eixos: ±X, ±Y, ±Z.
|
||||
Mesh3D mesh;
|
||||
mesh.vertices = {
|
||||
{.x = 1.0F, .y = 0.0F, .z = 0.0F}, // 0: +X
|
||||
{.x = -1.0F, .y = 0.0F, .z = 0.0F}, // 1: -X
|
||||
{.x = 0.0F, .y = 1.0F, .z = 0.0F}, // 2: +Y
|
||||
{.x = 0.0F, .y = -1.0F, .z = 0.0F}, // 3: -Y
|
||||
{.x = 0.0F, .y = 0.0F, .z = 1.0F}, // 4: +Z
|
||||
{.x = 0.0F, .y = 0.0F, .z = -1.0F}, // 5: -Z
|
||||
};
|
||||
// 12 arestes: cada vèrtex axial connecta amb els 4 vèrtexs no oposats.
|
||||
mesh.edges = {
|
||||
// "Equador" XY al voltant de Z.
|
||||
{2, 0},
|
||||
{0, 3},
|
||||
{3, 1},
|
||||
{1, 2},
|
||||
// Piràmide superior (cap a +Z).
|
||||
{2, 4},
|
||||
{0, 4},
|
||||
{3, 4},
|
||||
{1, 4},
|
||||
// Piràmide inferior (cap a -Z).
|
||||
{2, 5},
|
||||
{0, 5},
|
||||
{3, 5},
|
||||
{1, 5},
|
||||
};
|
||||
return mesh;
|
||||
}
|
||||
|
||||
auto extrudeShape2D(const Shape& shape, float depth) -> Mesh3D {
|
||||
Mesh3D mesh;
|
||||
if (!shape.isValid()) {
|
||||
return mesh;
|
||||
}
|
||||
|
||||
const float HALF = depth * 0.5F;
|
||||
const Vec2 CENTRE = shape.getCenter();
|
||||
// Si depth <= 0, emetem només un pla (sense vèrtexs back ni connexions)
|
||||
// per evitar arestes degenerades i acumulació additiva de brightness.
|
||||
const bool FLAT = (depth <= 0.0F);
|
||||
|
||||
for (const auto& primitive : shape.getPrimitives()) {
|
||||
if (primitive.points.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto BASE = static_cast<std::uint16_t>(mesh.vertices.size());
|
||||
const auto N = static_cast<std::uint16_t>(primitive.points.size());
|
||||
|
||||
// Vèrtexs frontals (z = +HALF, o z = 0 si FLAT).
|
||||
for (const auto& p : primitive.points) {
|
||||
mesh.vertices.push_back(Vec3{
|
||||
.x = p.x - CENTRE.x,
|
||||
.y = p.y - CENTRE.y,
|
||||
.z = HALF,
|
||||
});
|
||||
}
|
||||
// Arestes "frontals": connecten punts consecutius de la polyline.
|
||||
for (std::uint16_t i = 0; i + 1 < N; ++i) {
|
||||
mesh.edges.emplace_back(BASE + i, BASE + i + 1);
|
||||
}
|
||||
|
||||
if (FLAT) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Vèrtexs posteriors (z = -HALF) i arestes corresponents.
|
||||
for (const auto& p : primitive.points) {
|
||||
mesh.vertices.push_back(Vec3{
|
||||
.x = p.x - CENTRE.x,
|
||||
.y = p.y - CENTRE.y,
|
||||
.z = -HALF,
|
||||
});
|
||||
}
|
||||
for (std::uint16_t i = 0; i + 1 < N; ++i) {
|
||||
mesh.edges.emplace_back(BASE + N + i, BASE + N + i + 1);
|
||||
}
|
||||
// Arestes de connexió front↔posterior per cada vèrtex.
|
||||
// Per polylines tancades (primer == últim punt), el bucle igualment
|
||||
// genera N connexions; el parell duplicat (primer i últim) cau en una
|
||||
// línia idèntica sense efecte visible.
|
||||
for (std::uint16_t i = 0; i < N; ++i) {
|
||||
mesh.edges.emplace_back(BASE + i, BASE + N + i);
|
||||
}
|
||||
}
|
||||
|
||||
return mesh;
|
||||
}
|
||||
|
||||
} // namespace Graphics
|
||||
@@ -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 <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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<Vec3> vertices;
|
||||
std::vector<std::pair<std::uint16_t, std::uint16_t>> 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
|
||||
@@ -9,16 +9,16 @@
|
||||
|
||||
namespace SceneManager {
|
||||
|
||||
// Context de transición entre escenes
|
||||
// Conté l'escena destinació i opciones específiques per aquella escena
|
||||
class SceneContext {
|
||||
public:
|
||||
// Context de transición entre escenes
|
||||
// Conté l'escena destinació i opciones específiques per aquella escena
|
||||
class SceneContext {
|
||||
public:
|
||||
// Tipo de escena del juego
|
||||
enum class SceneType : std::uint8_t {
|
||||
LOGO, // Pantalla de start (logo JAILGAMES)
|
||||
TITLE, // Pantalla de título con menú
|
||||
GAME, // Juego principal (Asteroids)
|
||||
EXIT // Salir del programa
|
||||
TITLE, // Pantalla de título (3D)
|
||||
GAME, // Juego principal (Asteroids)
|
||||
EXIT // Salir del programa
|
||||
};
|
||||
|
||||
// Opciones específiques para cada escena
|
||||
@@ -70,14 +70,14 @@ class SceneContext {
|
||||
return match_config_;
|
||||
}
|
||||
|
||||
private:
|
||||
SceneType next_scene_{SceneType::LOGO}; // SceneType a la qual transicionar
|
||||
Option option_{Option::NONE}; // Opción específica per l'escena
|
||||
GameConfig::MatchConfig match_config_; // Configuración de match (jugadors active, mode)
|
||||
};
|
||||
private:
|
||||
SceneType next_scene_{SceneType::LOGO}; // SceneType a la qual transicionar
|
||||
Option option_{Option::NONE}; // Opción específica per l'escena
|
||||
GameConfig::MatchConfig match_config_; // Configuración de match (jugadors active, mode)
|
||||
};
|
||||
|
||||
// Variable global inline per gestionar l'escena actual (backward compatibility)
|
||||
// Sincronitzada con context.nextScene() por el Director
|
||||
inline SceneContext::SceneType actual = SceneContext::SceneType::LOGO;
|
||||
// Variable global inline per gestionar l'escena actual (backward compatibility)
|
||||
// Sincronitzada con context.nextScene() por el Director
|
||||
inline SceneContext::SceneType actual = SceneContext::SceneType::LOGO;
|
||||
|
||||
} // namespace SceneManager
|
||||
|
||||
+110
-30
@@ -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;
|
||||
}
|
||||
|
||||
+130
-344
@@ -1,4 +1,4 @@
|
||||
// title_scene.cpp - Implementació de l'escena de título
|
||||
// title_scene.cpp - Implementació de l'escena de títol 3D real
|
||||
// © 2026 JailDesigner
|
||||
|
||||
#include "title_scene.hpp"
|
||||
@@ -18,115 +18,95 @@
|
||||
#include "core/system/scene_context.hpp"
|
||||
#include "project.h"
|
||||
|
||||
// Using declarations per simplificar el codi
|
||||
using SceneManager::SceneContext;
|
||||
using SceneType = SceneContext::SceneType;
|
||||
using Option = SceneContext::Option;
|
||||
|
||||
namespace {
|
||||
|
||||
// Botons per iniciar partida des de MAIN (només START).
|
||||
constexpr std::array<InputAction, 1> 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<Graphics::Camera3D>(
|
||||
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<float>(Defaults::Game::WIDTH),
|
||||
static_cast<float>(Defaults::Game::HEIGHT)};
|
||||
static_cast<float>(Defaults::Game::HEIGHT));
|
||||
|
||||
starfield_ = std::make_unique<Graphics::Starfield>(
|
||||
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<Title::ShipAnimator>(sdl_.getRenderer());
|
||||
ship_animator_ = std::make_unique<Title::ShipAnimator>(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<std::string> fitxers_orni = {
|
||||
const std::vector<std::string> FITXERS_ORNI = {
|
||||
"title/letra_o.shp",
|
||||
"title/letra_r.shp",
|
||||
"title/letra_n.shp",
|
||||
"title/letra_i.shp"};
|
||||
|
||||
// Pas 1: Carregar formes i calcular amplades per "ORNI"
|
||||
float ancho_total_orni = 0.0F;
|
||||
|
||||
for (const auto& file : fitxers_orni) {
|
||||
for (const auto& file : FITXERS_ORNI) {
|
||||
auto shape = ShapeLoader::load(file);
|
||||
if (!shape || !shape->isValid()) {
|
||||
std::cerr << "[TitleScene] Error carregant " << file << '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calcular bounding box de la shape (trobar ancho i altura)
|
||||
float min_x = FLT_MAX;
|
||||
float max_x = -FLT_MAX;
|
||||
float min_y = FLT_MAX;
|
||||
float max_y = -FLT_MAX;
|
||||
|
||||
for (const auto& prim : shape->getPrimitives()) {
|
||||
for (const auto& point : prim.points) {
|
||||
min_x = std::min(min_x, point.x);
|
||||
@@ -135,72 +115,46 @@ void TitleScene::initTitle() {
|
||||
max_y = std::max(max_y, point.y);
|
||||
}
|
||||
}
|
||||
|
||||
float ancho_sin_escalar = max_x - min_x;
|
||||
float altura_sin_escalar = max_y - min_y;
|
||||
|
||||
// Escalar ancho, altura i offset con LOGO_SCALE
|
||||
float ancho = ancho_sin_escalar * Defaults::Title::Layout::LOGO_SCALE;
|
||||
float altura = altura_sin_escalar * Defaults::Title::Layout::LOGO_SCALE;
|
||||
float offset_centre = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
|
||||
lletres_orni_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ancho, altura, offset_centre});
|
||||
|
||||
ancho_total_orni += ancho;
|
||||
const float ANCHO = (max_x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
const float ALTURA = (max_y - min_y) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
lletres_orni_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ANCHO, ALTURA, OFFSET_CENTRE});
|
||||
ancho_total_orni += ANCHO;
|
||||
}
|
||||
ancho_total_orni += ESPAI_ENTRE_LLETRES * static_cast<float>(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<std::string> fitxers_attack = {
|
||||
const std::vector<std::string> FITXERS_ATTACK = {
|
||||
"title/letra_a.shp",
|
||||
"title/letra_t.shp",
|
||||
"title/letra_t.shp", // T repetida
|
||||
"title/letra_a.shp", // A repetida
|
||||
"title/letra_t.shp",
|
||||
"title/letra_a.shp",
|
||||
"title/letra_c.shp",
|
||||
"title/letra_k.shp",
|
||||
"title/letra_exclamacion.shp"};
|
||||
|
||||
// Pas 1: Carregar formes i calcular amplades per "ATTACK!"
|
||||
float ancho_total_attack = 0.0F;
|
||||
|
||||
for (const auto& file : fitxers_attack) {
|
||||
for (const auto& file : FITXERS_ATTACK) {
|
||||
auto shape = ShapeLoader::load(file);
|
||||
if (!shape || !shape->isValid()) {
|
||||
std::cerr << "[TitleScene] Error carregant " << file << '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calcular bounding box de la shape (trobar ancho i altura)
|
||||
float min_x = FLT_MAX;
|
||||
float max_x = -FLT_MAX;
|
||||
float min_y = FLT_MAX;
|
||||
float max_y = -FLT_MAX;
|
||||
|
||||
for (const auto& prim : shape->getPrimitives()) {
|
||||
for (const auto& point : prim.points) {
|
||||
min_x = std::min(min_x, point.x);
|
||||
@@ -209,54 +163,34 @@ void TitleScene::initTitle() {
|
||||
max_y = std::max(max_y, point.y);
|
||||
}
|
||||
}
|
||||
|
||||
float ancho_sin_escalar = max_x - min_x;
|
||||
float altura_sin_escalar = max_y - min_y;
|
||||
|
||||
// Escalar ancho, altura i offset con LOGO_SCALE
|
||||
float ancho = ancho_sin_escalar * Defaults::Title::Layout::LOGO_SCALE;
|
||||
float altura = altura_sin_escalar * Defaults::Title::Layout::LOGO_SCALE;
|
||||
float offset_centre = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
|
||||
lletres_attack_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ancho, altura, offset_centre});
|
||||
|
||||
ancho_total_attack += ancho;
|
||||
const float ANCHO = (max_x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
const float ALTURA = (max_y - min_y) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
|
||||
lletres_attack_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ANCHO, ALTURA, OFFSET_CENTRE});
|
||||
ancho_total_attack += ANCHO;
|
||||
}
|
||||
ancho_total_attack += ESPAI_ENTRE_LLETRES * static_cast<float>(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<std::string> FITXERS = {
|
||||
"logo/letra_j.shp",
|
||||
"logo/letra_a.shp",
|
||||
@@ -270,17 +204,14 @@ void TitleScene::inicialitzarJailgames() {
|
||||
|
||||
constexpr float SCALE = Defaults::Title::Layout::JAILGAMES_SCALE;
|
||||
|
||||
// Pas 1: carregar formes i calcular amplada/altura escalades.
|
||||
float ancho_total = 0.0F;
|
||||
float altura_max = 0.0F;
|
||||
|
||||
for (const auto& file : FITXERS) {
|
||||
auto shape = ShapeLoader::load(file);
|
||||
if (!shape || !shape->isValid()) {
|
||||
std::cerr << "[TitleScene] Error carregant " << file << '\n';
|
||||
continue;
|
||||
}
|
||||
|
||||
float min_x = FLT_MAX;
|
||||
float max_x = -FLT_MAX;
|
||||
float min_y = FLT_MAX;
|
||||
@@ -296,24 +227,18 @@ void TitleScene::inicialitzarJailgames() {
|
||||
const float ANCHO = (max_x - min_x) * SCALE;
|
||||
const float ALTURA = (max_y - min_y) * SCALE;
|
||||
const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * SCALE;
|
||||
|
||||
lletres_jailgames_.push_back({shape, {.x = 0.0F, .y = 0.0F},
|
||||
ANCHO, ALTURA, OFFSET_CENTRE});
|
||||
|
||||
lletres_jailgames_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ANCHO, ALTURA, OFFSET_CENTRE});
|
||||
ancho_total += ANCHO;
|
||||
altura_max = std::max(altura_max, ALTURA);
|
||||
}
|
||||
|
||||
// Espaiat entre lletres (proporcional a la escala, para que no quede pegado).
|
||||
constexpr float ESPAI_JAILGAMES = ESPAI_ENTRE_LLETRES * SCALE;
|
||||
if (!lletres_jailgames_.empty()) {
|
||||
ancho_total += ESPAI_JAILGAMES * static_cast<float>(lletres_jailgames_.size() - 1);
|
||||
}
|
||||
|
||||
// Pas 2: centrar horizontalmente y colocar JUST encima de la línea de copyright.
|
||||
const float Y_COPYRIGHT = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS;
|
||||
const float Y_COPY = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS;
|
||||
const float GAP = Defaults::Game::HEIGHT * Defaults::Title::Layout::JAILGAMES_COPYRIGHT_GAP;
|
||||
const float Y_CENTRE = Y_COPYRIGHT - GAP - (altura_max / 2.0F);
|
||||
const float Y_CENTRE = Y_COPY - GAP - (altura_max / 2.0F);
|
||||
const float X_INICIAL = (Defaults::Game::WIDTH - ancho_total) / 2.0F;
|
||||
|
||||
float x_actual = X_INICIAL;
|
||||
@@ -325,15 +250,9 @@ void TitleScene::inicialitzarJailgames() {
|
||||
}
|
||||
|
||||
void TitleScene::dibuixarPeuTitol(float spacing) const {
|
||||
// Logo JAILGAMES pequeño sobre el copyright.
|
||||
for (const auto& lletra : lletres_jailgames_) {
|
||||
Rendering::renderShape(sdl_.getRenderer(), lletra.shape,
|
||||
lletra.position, 0.0F,
|
||||
Defaults::Title::Layout::JAILGAMES_SCALE,
|
||||
1.0F);
|
||||
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::JAILGAMES_SCALE, 1.0F);
|
||||
}
|
||||
|
||||
// Copyright en una sola línea, centrado, en mayúsculas.
|
||||
std::string copyright = Project::COPYRIGHT;
|
||||
for (char& c : copyright) {
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
@@ -342,8 +261,7 @@ void TitleScene::dibuixarPeuTitol(float spacing) const {
|
||||
}
|
||||
const float Y_COPY = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS;
|
||||
const float CENTRE_X = Defaults::Game::WIDTH / 2.0F;
|
||||
text_.renderCentered(copyright, {.x = CENTRE_X, .y = Y_COPY},
|
||||
Defaults::Title::Layout::COPYRIGHT_SCALE, spacing);
|
||||
text_.renderCentered(copyright, {.x = CENTRE_X, .y = Y_COPY}, Defaults::Title::Layout::COPYRIGHT_SCALE, spacing);
|
||||
}
|
||||
|
||||
auto TitleScene::isFinished() const -> bool {
|
||||
@@ -351,12 +269,9 @@ auto TitleScene::isFinished() const -> bool {
|
||||
}
|
||||
|
||||
void TitleScene::update(float delta_time) {
|
||||
// Actualitzar starfield (siempre active)
|
||||
if (starfield_) {
|
||||
starfield_->update(delta_time);
|
||||
}
|
||||
|
||||
// Actualitzar naves (cuando visibles)
|
||||
if (ship_animator_ &&
|
||||
(estat_actual_ == TitleState::STARFIELD_FADE_IN ||
|
||||
estat_actual_ == TitleState::STARFIELD ||
|
||||
@@ -389,19 +304,12 @@ void TitleScene::update(float delta_time) {
|
||||
|
||||
void TitleScene::updateStarfieldFadeInState(float delta_time) {
|
||||
temps_acumulat_ += delta_time;
|
||||
|
||||
// Calcular progrés del fade (0.0 → 1.0)
|
||||
float progress = std::min(1.0F, temps_acumulat_ / DURACIO_FADE_IN);
|
||||
|
||||
// Lerp brightness de 0.0 a BRIGHTNESS_STARFIELD
|
||||
float brightness_actual = progress * BRIGHTNESS_STARFIELD;
|
||||
starfield_->setBrightness(brightness_actual);
|
||||
|
||||
// Transición a STARFIELD cuando el fade es completa
|
||||
const float PROGRESS = std::min(1.0F, temps_acumulat_ / DURACIO_FADE_IN);
|
||||
starfield_->setBrightness(PROGRESS * BRIGHTNESS_STARFIELD);
|
||||
if (temps_acumulat_ >= DURACIO_FADE_IN) {
|
||||
estat_actual_ = TitleState::STARFIELD;
|
||||
temps_acumulat_ = 0.0F; // Reset timer per al següent state
|
||||
starfield_->setBrightness(BRIGHTNESS_STARFIELD); // Assegurar value final
|
||||
temps_acumulat_ = 0.0F;
|
||||
starfield_->setBrightness(BRIGHTNESS_STARFIELD);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,147 +317,100 @@ void TitleScene::updateStarfieldState(float delta_time) {
|
||||
temps_acumulat_ += delta_time;
|
||||
if (temps_acumulat_ >= DURACIO_INIT) {
|
||||
estat_actual_ = TitleState::MAIN;
|
||||
temps_estat_main_ = 0.0F; // Reset timer al entrar a MAIN
|
||||
animacio_activa_ = false; // Comença estàtic
|
||||
factor_lerp_ = 0.0F; // Sin animación aún
|
||||
|
||||
// Naves esperaran ENTRANCE_DELAY antes de entrar (no start aquí)
|
||||
temps_estat_main_ = 0.0F;
|
||||
animacio_activa_ = false;
|
||||
factor_lerp_ = 0.0F;
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene::updateMainState(float delta_time) {
|
||||
temps_estat_main_ += delta_time;
|
||||
|
||||
// Iniciar animación de entrada de naves después del delay
|
||||
if (temps_estat_main_ >= Defaults::Title::Ships::ENTRANCE_DELAY &&
|
||||
ship_animator_ && !ship_animator_->isVisible()) {
|
||||
ship_animator_->setVisible(true);
|
||||
ship_animator_->startEntryAnimation();
|
||||
}
|
||||
|
||||
// Fase 1: Estàtic (0-10s)
|
||||
if (temps_estat_main_ < DELAY_INICI_ANIMACIO) {
|
||||
factor_lerp_ = 0.0F;
|
||||
animacio_activa_ = false;
|
||||
}
|
||||
// Fase 2: Lerp (10-12s)
|
||||
else if (temps_estat_main_ < DELAY_INICI_ANIMACIO + DURACIO_LERP) {
|
||||
float temps_lerp = temps_estat_main_ - DELAY_INICI_ANIMACIO;
|
||||
factor_lerp_ = temps_lerp / DURACIO_LERP; // 0.0 → 1.0 linealment
|
||||
} else if (temps_estat_main_ < DELAY_INICI_ANIMACIO + DURACIO_LERP) {
|
||||
const float TEMPS_LERP = temps_estat_main_ - DELAY_INICI_ANIMACIO;
|
||||
factor_lerp_ = TEMPS_LERP / DURACIO_LERP;
|
||||
animacio_activa_ = true;
|
||||
}
|
||||
// Fase 3: Animación completa (12s+)
|
||||
else {
|
||||
} else {
|
||||
factor_lerp_ = 1.0F;
|
||||
animacio_activa_ = true;
|
||||
}
|
||||
|
||||
// Actualitzar animación del logo
|
||||
updateLogoAnimation(delta_time);
|
||||
}
|
||||
|
||||
void TitleScene::updatePlayerJoinPhaseState(float delta_time) {
|
||||
temps_acumulat_ += delta_time;
|
||||
|
||||
// Continuar animación orbital durante la transición
|
||||
updateLogoAnimation(delta_time);
|
||||
|
||||
// [NOU] Continuar comprovant si l'altre player quiere unir-se durante la transición ("late join")
|
||||
bool p1_actiu_abans = match_config_.jugador1_actiu;
|
||||
bool p2_actiu_abans = match_config_.jugador2_actiu;
|
||||
const bool P1_ABANS = match_config_.jugador1_actiu;
|
||||
const bool P2_ABANS = match_config_.jugador2_actiu;
|
||||
|
||||
if (checkStartGameButtonPressed()) {
|
||||
// Updates match_config_ if pressed, logs are in the method
|
||||
context_.setMatchConfig(match_config_);
|
||||
|
||||
// Trigger animación de salida per la ship que acaba de unir-se
|
||||
triggerExitForJoinedPlayers(p1_actiu_abans, p2_actiu_abans, "late join - ");
|
||||
|
||||
// Reproducir so de START cuando el segon player s'uneix
|
||||
triggerExitForJoinedPlayers(P1_ABANS, P2_ABANS, "late join - ");
|
||||
Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME);
|
||||
|
||||
// Reiniciar el timer per allargar el time de transición
|
||||
temps_acumulat_ = 0.0F;
|
||||
|
||||
std::cout << "[TitleScene] Segon player s'ha unit - so i timer reiniciats\n";
|
||||
}
|
||||
|
||||
if (temps_acumulat_ >= DURACIO_TRANSITION) {
|
||||
// Transición a pantalla negra
|
||||
estat_actual_ = TitleState::BLACK_SCREEN;
|
||||
temps_acumulat_ = 0.0F;
|
||||
std::cout << "[TitleScene] Passant a BLACK_SCREEN\n";
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene::updateBlackScreenState(float delta_time) {
|
||||
temps_acumulat_ += delta_time;
|
||||
|
||||
// No animation, no input checking - just wait
|
||||
if (temps_acumulat_ >= DURACIO_BLACK_SCREEN) {
|
||||
// Transición a escena GAME (el Director detecta isFinished()).
|
||||
context_.setNextScene(SceneType::GAME);
|
||||
std::cout << "[TitleScene] Canviant a escena GAME\n";
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene::handleSkipInput() {
|
||||
// Verificar botones de skip (FIRE/THRUST/START) para saltar escenas ANTES de MAIN
|
||||
if (estat_actual_ != TitleState::STARFIELD_FADE_IN && estat_actual_ != TitleState::STARFIELD) {
|
||||
return;
|
||||
}
|
||||
if (!checkSkipButtonPressed()) {
|
||||
return;
|
||||
}
|
||||
// Saltar a MAIN
|
||||
estat_actual_ = TitleState::MAIN;
|
||||
starfield_->setBrightness(BRIGHTNESS_STARFIELD);
|
||||
temps_estat_main_ = 0.0F;
|
||||
// Naves esperaran ENTRANCE_DELAY antes de entrar (no start aquí)
|
||||
}
|
||||
|
||||
void TitleScene::handleStartInput() {
|
||||
// Verificar boton START para start match desde MAIN
|
||||
if (estat_actual_ != TitleState::MAIN) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Guardar state anterior per detectar qui ha premut START AQUEST frame
|
||||
bool p1_actiu_abans = match_config_.jugador1_actiu;
|
||||
bool p2_actiu_abans = match_config_.jugador2_actiu;
|
||||
const bool P1_ABANS = match_config_.jugador1_actiu;
|
||||
const bool P2_ABANS = match_config_.jugador2_actiu;
|
||||
|
||||
if (!checkStartGameButtonPressed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Si START es prem durante el delay (naves aún invisibles), saltar-las a FLOATING
|
||||
if (ship_animator_ && !ship_animator_->isVisible()) {
|
||||
ship_animator_->setVisible(true);
|
||||
ship_animator_->skipToFloatingState();
|
||||
}
|
||||
|
||||
// Configurar match antes de canviar de escena
|
||||
context_.setMatchConfig(match_config_);
|
||||
std::cout << "[TitleScene] Configuración de match - P1: "
|
||||
<< (match_config_.jugador1_actiu ? "ACTIU" : "INACTIU")
|
||||
<< ", P2: "
|
||||
<< (match_config_.jugador2_actiu ? "ACTIU" : "INACTIU")
|
||||
<< '\n';
|
||||
|
||||
// El setNextScene a GAME se hace al final de BLACK_SCREEN para no
|
||||
// saltar la animación de salida (isFinished() lo recoge entonces).
|
||||
estat_actual_ = TitleState::PLAYER_JOIN_PHASE;
|
||||
temps_acumulat_ = 0.0F;
|
||||
|
||||
// Trigger animación de salida NOMÉS per las naves que han premut START
|
||||
triggerExitForJoinedPlayers(p1_actiu_abans, p2_actiu_abans, "");
|
||||
triggerExitForJoinedPlayers(P1_ABANS, P2_ABANS, "");
|
||||
|
||||
Audio::get()->fadeOutMusic(MUSIC_FADE);
|
||||
Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME);
|
||||
}
|
||||
|
||||
void TitleScene::triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active,
|
||||
const char* log_prefix) {
|
||||
void TitleScene::triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix) {
|
||||
if (ship_animator_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -564,42 +425,30 @@ void TitleScene::triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_act
|
||||
}
|
||||
|
||||
void TitleScene::updateLogoAnimation(float delta_time) {
|
||||
// Solo calcular i aplicar offsets si l'animación está activa
|
||||
if (animacio_activa_) {
|
||||
// Acumular time escalat
|
||||
temps_animacio_ += delta_time * factor_lerp_;
|
||||
if (!animacio_activa_) {
|
||||
return;
|
||||
}
|
||||
temps_animacio_ += delta_time * factor_lerp_;
|
||||
|
||||
// Usar amplituds i freqüències completes
|
||||
float amplitude_x_actual = ORBIT_AMPLITUDE_X;
|
||||
float amplitude_y_actual = ORBIT_AMPLITUDE_Y;
|
||||
float frequency_x_actual = ORBIT_FREQUENCY_X;
|
||||
float frequency_y_actual = ORBIT_FREQUENCY_Y;
|
||||
const float TWO_PI = 2.0F * Defaults::Math::PI;
|
||||
const float OFFSET_X = ORBIT_AMPLITUDE_X * std::sin(TWO_PI * ORBIT_FREQUENCY_X * temps_animacio_);
|
||||
const float OFFSET_Y = ORBIT_AMPLITUDE_Y * std::sin((TWO_PI * ORBIT_FREQUENCY_Y * temps_animacio_) + ORBIT_PHASE_OFFSET);
|
||||
|
||||
// Calcular offset orbital
|
||||
float offset_x = amplitude_x_actual * std::sin(2.0F * Defaults::Math::PI * frequency_x_actual * temps_animacio_);
|
||||
float offset_y = amplitude_y_actual * std::sin((2.0F * Defaults::Math::PI * frequency_y_actual * temps_animacio_) + ORBIT_PHASE_OFFSET);
|
||||
|
||||
// Aplicar offset a todas las lletres de "ORNI"
|
||||
for (size_t i = 0; i < lletres_orni_.size(); ++i) {
|
||||
lletres_orni_[i].position.x = posicions_originals_orni_[i].x + static_cast<int>(std::round(offset_x));
|
||||
lletres_orni_[i].position.y = posicions_originals_orni_[i].y + static_cast<int>(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<int>(std::round(offset_x));
|
||||
lletres_attack_[i].position.y = posicions_originals_attack_[i].y + static_cast<int>(std::round(offset_y));
|
||||
}
|
||||
for (std::size_t i = 0; i < lletres_orni_.size(); ++i) {
|
||||
lletres_orni_[i].position.x = posicions_originals_orni_[i].x + std::round(OFFSET_X);
|
||||
lletres_orni_[i].position.y = posicions_originals_orni_[i].y + std::round(OFFSET_Y);
|
||||
}
|
||||
for (std::size_t i = 0; i < lletres_attack_.size(); ++i) {
|
||||
lletres_attack_[i].position.x = posicions_originals_attack_[i].x + std::round(OFFSET_X);
|
||||
lletres_attack_[i].position.y = posicions_originals_attack_[i].y + std::round(OFFSET_Y);
|
||||
}
|
||||
}
|
||||
|
||||
void TitleScene::draw() {
|
||||
// Dibuixar starfield de fons (en todos los estats excepte BLACK_SCREEN)
|
||||
if (starfield_ && estat_actual_ != TitleState::BLACK_SCREEN) {
|
||||
starfield_->draw();
|
||||
}
|
||||
|
||||
// Dibuixar naves (después starfield, antes logo)
|
||||
if (ship_animator_ &&
|
||||
(estat_actual_ == TitleState::STARFIELD_FADE_IN ||
|
||||
estat_actual_ == TitleState::STARFIELD ||
|
||||
@@ -608,116 +457,59 @@ void TitleScene::draw() {
|
||||
ship_animator_->draw();
|
||||
}
|
||||
|
||||
// En los estats STARFIELD_FADE_IN i STARFIELD, solo mostrar starfield (sin text)
|
||||
if (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Estat MAIN i PLAYER_JOIN_PHASE: Dibuixar título i text (sobre el starfield)
|
||||
// BLACK_SCREEN: no draw res (fons negre ya está netejat)
|
||||
if (estat_actual_ == TitleState::MAIN || estat_actual_ == TitleState::PLAYER_JOIN_PHASE) {
|
||||
// === Calcular i renderizar ombra (solo si animación activa) ===
|
||||
if (animacio_activa_) {
|
||||
float temps_shadow = temps_animacio_ - SHADOW_DELAY;
|
||||
temps_shadow = std::max(temps_shadow, 0.0F); // Evitar time negatiu
|
||||
|
||||
// Usar amplituds i freqüències completes per l'ombra
|
||||
float amplitude_x_shadow = ORBIT_AMPLITUDE_X;
|
||||
float amplitude_y_shadow = ORBIT_AMPLITUDE_Y;
|
||||
float frequency_x_shadow = ORBIT_FREQUENCY_X;
|
||||
float frequency_y_shadow = ORBIT_FREQUENCY_Y;
|
||||
|
||||
// Calcular offset de l'ombra
|
||||
float shadow_offset_x = (amplitude_x_shadow * std::sin(2.0F * Defaults::Math::PI * frequency_x_shadow * temps_shadow)) + SHADOW_OFFSET_X;
|
||||
float shadow_offset_y = (amplitude_y_shadow * std::sin((2.0F * Defaults::Math::PI * frequency_y_shadow * temps_shadow) + ORBIT_PHASE_OFFSET)) + SHADOW_OFFSET_Y;
|
||||
|
||||
// === RENDERITZAR OMBRA PRIMER (darrera del logo principal) ===
|
||||
|
||||
// Ombra "ORNI"
|
||||
for (size_t i = 0; i < lletres_orni_.size(); ++i) {
|
||||
Vec2 pos_shadow;
|
||||
pos_shadow.x = posicions_originals_orni_[i].x + static_cast<int>(std::round(shadow_offset_x));
|
||||
pos_shadow.y = posicions_originals_orni_[i].y + static_cast<int>(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<int>(std::round(shadow_offset_x));
|
||||
pos_shadow.y = posicions_originals_attack_[i].y + static_cast<int>(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<float>; // 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<float>;
|
||||
mostrar_text = (std::sin(FASE) > 0.0F);
|
||||
}
|
||||
if (mostrar_text) {
|
||||
const std::string MAIN_TEXT = "PRESS START TO PLAY";
|
||||
const float MAIN_SCALE = Defaults::Title::Layout::PRESS_START_SCALE;
|
||||
const float CENTRE_X = Defaults::Game::WIDTH / 2.0F;
|
||||
const float CENTRE_Y = Defaults::Game::HEIGHT * Defaults::Title::Layout::PRESS_START_POS;
|
||||
text_.renderCentered(MAIN_TEXT, {.x = CENTRE_X, .y = CENTRE_Y}, MAIN_SCALE, SPACING);
|
||||
}
|
||||
|
||||
dibuixarPeuTitol(SPACING);
|
||||
}
|
||||
|
||||
auto TitleScene::checkSkipButtonPressed() -> bool {
|
||||
@@ -727,29 +519,23 @@ auto TitleScene::checkSkipButtonPressed() -> bool {
|
||||
auto TitleScene::checkStartGameButtonPressed() -> bool {
|
||||
auto* input = Input::get();
|
||||
bool any_pressed = false;
|
||||
|
||||
for (auto action : START_GAME_BUTTONS) {
|
||||
if (input->checkActionPlayer1(action, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
if (!match_config_.jugador1_actiu) {
|
||||
match_config_.jugador1_actiu = true;
|
||||
any_pressed = true;
|
||||
std::cout << "[TitleScene] P1 pressed START\n";
|
||||
}
|
||||
}
|
||||
if (input->checkActionPlayer2(action, Input::DO_NOT_ALLOW_REPEAT)) {
|
||||
if (!match_config_.jugador2_actiu) {
|
||||
match_config_.jugador2_actiu = true;
|
||||
any_pressed = true;
|
||||
std::cout << "[TitleScene] P2 pressed START\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return any_pressed;
|
||||
}
|
||||
|
||||
void TitleScene::handleEvent(const SDL_Event& event) {
|
||||
// La lógica de input se decide en update() consultando Input::checkAction;
|
||||
// aquí no hay eventos puntuales que procesar.
|
||||
(void)event;
|
||||
}
|
||||
|
||||
@@ -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 <memory>
|
||||
#include <vector>
|
||||
|
||||
#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<InputAction, 1> 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<Graphics::Shape> 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<Graphics::Shape> 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<Graphics::Starfield> starfield_; // Camp de estrelles de fons
|
||||
std::unique_ptr<Title::ShipAnimator> 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<Graphics::Camera3D> camera_;
|
||||
std::unique_ptr<Graphics::Starfield> starfield_;
|
||||
std::unique_ptr<Title::ShipAnimator> ship_animator_;
|
||||
TitleState estat_actual_{TitleState::STARFIELD_FADE_IN};
|
||||
float temps_acumulat_{0.0F};
|
||||
|
||||
// Lletres del título "ORNI ATTACK!"
|
||||
std::vector<LetraLogo> lletres_orni_; // Lletres de "ORNI" (línia 1)
|
||||
std::vector<LetraLogo> lletres_attack_; // Lletres de "ATTACK!" (línia 2)
|
||||
float y_attack_dinamica_; // Posición Y calculada dinàmicament per "ATTACK!"
|
||||
std::vector<LetraLogo> lletres_orni_;
|
||||
std::vector<LetraLogo> lletres_attack_;
|
||||
float y_attack_dinamica_{0.0F};
|
||||
|
||||
// Logo "JAILGAMES" pequeño sobre el copyright (esquinas inferiores del título).
|
||||
std::vector<LetraLogo> lletres_jailgames_;
|
||||
std::vector<LetraLogo> lletres_jailgames_;
|
||||
|
||||
// Estat de animación del logo
|
||||
float temps_animacio_{0.0F}; // Temps acumulat per animación orbital
|
||||
std::vector<Vec2> posicions_originals_orni_; // Posicions originals de "ORNI"
|
||||
std::vector<Vec2> posicions_originals_attack_; // Posicions originals de "ATTACK!"
|
||||
float temps_animacio_{0.0F};
|
||||
std::vector<Vec2> posicions_originals_orni_;
|
||||
std::vector<Vec2> 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);
|
||||
};
|
||||
|
||||
+295
-278
@@ -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
|
||||
|
||||
@@ -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 <SDL3/SDL.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#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<TitleShip, 2> 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<Graphics::Shape> 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<TitleShip, 2> ships_; // Naves P1 i P2
|
||||
const Graphics::Camera3D* camera_;
|
||||
std::array<TitleShip, 2> 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
|
||||
|
||||
Reference in New Issue
Block a user