refactor(title): la 3D és l'única — elimina backup 2D i renomena als noms canònics

This commit is contained in:
2026-05-22 12:04:16 +02:00
parent a29c2b9cc2
commit 54d3e683a1
16 changed files with 680 additions and 2321 deletions
+295 -278
View File
@@ -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