Compare commits

..

62 Commits

Author SHA1 Message Date
JailDesigner 274ce1ca63 Merge branch 'refactor/english-identifiers': identificadors valencians/castellans a anglès 2026-05-24 08:12:56 +02:00
JailDesigner 252e881e93 refactor: renombra jugador*/zona/radi/MARGE/origen/letra residuals a anglès 2026-05-24 08:09:41 +02:00
JailDesigner d36ad7d1c5 refactor(scenes): renombra ancho/altura/centre_punt residuals a anglès 2026-05-24 08:03:28 +02:00
JailDesigner 7305d2f5dc refactor(scenes): renombra identificadors valencians de logo/title a anglès 2026-05-24 08:00:40 +02:00
JailDesigner 4cfad053f0 refactor(effects): renombra temps_vida/temps_max a elapsed_time/max_lifetime 2026-05-24 07:59:14 +02:00
JailDesigner 807f71ffa7 refactor(defaults): renombra VELOCITAT/CANVI_ANGLE/MAX_BALES a anglès 2026-05-24 07:57:12 +02:00
JailDesigner d12f24d798 refactor(enemy): renombra esta_/animacio_/timer_invulnerabilitat_ a anglès 2026-05-24 07:56:35 +02:00
JailDesigner f9d2539a45 refactor(enemy): renombra drotacio/rotacio/FACTOR_HERENCIA a anglès 2026-05-24 07:52:21 +02:00
JailDesigner 87bfccd14f refactor(enemy): renombra palpitacio* a pulse* 2026-05-24 07:46:07 +02:00
JailDesigner e5e3729215 refactor(enemies): renombra QUADRAT/MOLINILLO a SQUARE/PINWHEEL 2026-05-24 07:40:54 +02:00
JailDesigner 6210985548 Merge branch 'fix/shaders-glslc-optional': glslc opcional si els headers SPV ja estan al repo 2026-05-23 12:55:51 +02:00
JailDesigner 20250a0d6d fix(cmake): glslc opcional si els headers SPV ja estan commiteats al repo 2026-05-23 12:55:48 +02:00
JailDesigner e5616f7c3a Merge branch 'tweak/misc-adjustments': retocs varis (paleta, glow, audio, física, destell del títol) 2026-05-22 23:48:59 +02:00
JailDesigner 3b1e469a4f feat(title): destell hiperespacial al VP quan la nau desapareix (sparkle 4-puntes còncau) 2026-05-22 23:46:56 +02:00
JailDesigner 70ca19eb87 fix(wounded-chain): amplifier 1.25 perquè la cadena agafi el contacte post-rebot 2026-05-22 23:32:28 +02:00
JailDesigner 7e52eaeddb tweak(friendly-fire): la bala empeny la nau abans de morir → els debris hereten la inèrcia 2026-05-22 23:24:42 +02:00
JailDesigner d618b6d561 feat(audio): so propi per a la nau a HURT (hurt.wav, separat del HIT de bala) 2026-05-22 23:20:18 +02:00
JailDesigner e954d4ea59 tweak(playfield): rejilla violeta synthwave + brillos +5%; starfield unificat al color del títol 2026-05-22 23:10:06 +02:00
JailDesigner b1ee23cd20 tweak(stage-messages): missatges level start/completed amb color ambre del PRESS START 2026-05-22 23:02:23 +02:00
JailDesigner d86b10c14e tweak(collision): impuls extra a l'enemic en el moment que mata la nau (factor 0.3·mass·vel) 2026-05-22 22:59:27 +02:00
JailDesigner 1ea38d4f6a fix(ship-death): debris hereten inèrcia (captura velocitat abans del markHit) i comparteixen dispersió amb enemics 2026-05-22 22:47:02 +02:00
JailDesigner 26bd5a9efa tweak(playfield): el grid principal es dibuixa sobre el subgrid a les interseccions 2026-05-22 22:43:32 +02:00
JailDesigner 4b0d85c010 tweak(palette): colors neon purs per als 3 enemics (cyan/roig/magenta) 2026-05-22 22:39:04 +02:00
JailDesigner 149b485a9b Merge branch 'tweak/enemy-mix-stage1': ajustos d'enemics (mix stage 1, spawn col·lidible, ull al cuadrado) 2026-05-22 22:34:54 +02:00
JailDesigner 6b1f064cda tweak(cuadrado): ull amb pupil·la al centre del rombe 2026-05-22 22:34:54 +02:00
JailDesigner 1cef6a2c23 tweak(enemy): durant l'spawn ja poden ser abatuts i rebotar amb la nau (sense fer dany) 2026-05-22 22:27:44 +02:00
JailDesigner 007460dc51 tweak(stages): stage 1 amb mix dels 3 tipus d'enemic (34/33/33) 2026-05-22 22:12:17 +02:00
JailDesigner 10057a82de tweak(audio): amplifica hit.wav +6dB i puja canals simultanis a 50 2026-05-22 22:09:03 +02:00
JailDesigner 73fa5bf1d1 Merge branch 'tweak/firework-glow': halo neon per a fireworks amb color propi 2026-05-22 21:57:36 +02:00
JailDesigner c32b564da1 feat(firework): halo neon per partícula amb color de glow propi (explosió enemic: línia blanca + halo daurat) 2026-05-22 21:57:11 +02:00
JailDesigner 7b9b5ce569 Merge branch 'tweak/pentagon-design': halo neon proporcional i pentàgon doble 2026-05-22 21:38:29 +02:00
JailDesigner f0b3a1fbc4 feat(render): halo neon proporcional al bounding_radius de la shape (opt-out a text) 2026-05-22 21:35:01 +02:00
JailDesigner 869b4374ba tweak(pentagon): pentàgon doble concentric (interior rotat 36°) 2026-05-22 20:11:29 +02:00
JailDesigner ea192cd9de tweak(debug): l'overlay arranca ocult sempre; F11 segueix alternant-lo 2026-05-22 19:53:26 +02:00
JailDesigner 5d30f6be68 Merge branch 'tweak/playfield-grid': ones d'aigua + starfield parallax al fons 2026-05-22 19:52:07 +02:00
JailDesigner a342d79b86 feat(starfield): mou estrelles amb la mitjana de velocitats de les naus 2026-05-22 19:51:40 +02:00
JailDesigner 1db7368c9f feat(starfield): capa parallax al fons del playfield amb tint blanc-cyan 2026-05-22 19:46:57 +02:00
JailDesigner 88b002b277 feat(playfield): ones d'aigua a la rejilla per explosions i pas de nau 2026-05-22 19:22:09 +02:00
JailDesigner 044a3a3bbf tweak(playfield): subdivisions de 5 a 4 a la subgraella 2026-05-22 18:56:24 +02:00
JailDesigner 49070aa843 Merge branch 'fix/bullet-collision-swept': col·lisió bales swept + debris 2026-05-22 18:43:46 +02:00
JailDesigner 18e05e36e6 feat(bullet): debris en trencar-se amb so HIT mogut des d'enemy.herir() 2026-05-22 18:42:23 +02:00
JailDesigner bf79eecca0 fix(bullet): col·lisió swept, sense grace_timer, mor al border visual 2026-05-22 18:24:54 +02:00
JailDesigner b80216dce1 Merge branch 'feat/ship-hurt-state': estat HURT a la nau 2026-05-22 17:32:04 +02:00
JailDesigner 87138f9a1f feat(ship): la nau entra a HURT al xocar amb un enemic, mor en un segon impacte 2026-05-22 17:30:33 +02:00
JailDesigner c6560514d8 Merge branch 'feat/title-intro-sequence': intro coreografiada al títol 2026-05-22 14:05:57 +02:00
JailDesigner 839f73e1ef feat(title): intro amb path Z (zoom+pivot al VP) en lloc d'offset Y
El logo i el footer ara entren simulant un moviment 3D des de l'usuari
cap al VP: arrenquen grans i a la posició projectada extrema (factor
d'escala SCALE_START > 1, pivot al centre de pantalla) i convergeixen
a la seva mida i posició finals. Substitueix l'offset Y lineal anterior.
2026-05-22 14:03:28 +02:00
JailDesigner 2ca2062011 feat(title): intro coreografiada amb logo, footer i naus escalonats
Logo cau des de dalt; quan aterra, JAILGAMES i COPYRIGHT pugen des de
baix amb stagger pam-pam; després arrenquen les naus i, en aterrar
elles, apareix PRESS START. Magic numbers a Defaults::Title::Sequence.
2026-05-22 13:51:09 +02:00
JailDesigner 03209ee23b Merge branch 'feat/title-neon-palette': paleta neon synthwave a títol 2026-05-22 13:25:18 +02:00
JailDesigner c61299f17f feat(title): paleta neon synthwave per element a l'escena de títol 2026-05-22 13:04:11 +02:00
JailDesigner 880af293ef log: primer missatge 'Game start', últim 'Bye!' 2026-05-22 12:50:53 +02:00
JailDesigner 67c59992c9 Merge branch 'feat/sdl-callbacks': migració a SDL_MAIN_USE_CALLBACKS 2026-05-22 12:48:39 +02:00
JailDesigner be3d696f60 feat(main): activa SDL_MAIN_USE_CALLBACKS
main.cpp queda amb les 4 callbacks de SDL3: AppInit construeix el
Director, AppEvent enruta cada event a handleEvent(), AppIterate crida
iterate(), AppQuit reabsorbeix la propietat amb unique_ptr.
El Director::run() i el bucle while interns desapareixen; el bootstrap
de SDLManager/Audio/Context/DebugOverlay/Notifier viu ara al final del
constructor. SDL_Quit() ja no es crida explícitament — SDL ho fa
després de SDL_AppQuit.
2026-05-22 12:45:12 +02:00
JailDesigner 6b8f6a267d refactor(director): migra la persistència ConfigYaml al Director
main.cpp queda només amb 'Director director(argc, argv); return director.run()'.
El Director crida ConfigYaml::* directament; l'struct ConfigPersistence
desapareix de engine_config.hpp. La separació core/game es relaxa al
Director, que és EL programa, no part del motor.
2026-05-22 12:41:05 +02:00
JailDesigner 120b8ada38 refactor(director): extreu iterate/handleEvent/advanceScene del runFrameLoop
run() ara delega a iterate() i handleEvent() per cada frame.
runFrameLoop desapareix; la seva lògica es divideix entre els tres
nous mètodes. La primera escena es construeix lazy via advanceScene()
dins d'iterate(). Cap canvi de comportament visible.
2026-05-22 12:38:16 +02:00
JailDesigner 8bb052981d refactor(director): locals de run() a membres unique_ptr
Preparació per a SDL_MAIN_USE_CALLBACKS: SDLManager, SceneContext,
DebugOverlay i l'escena actual ja viuen com a membres del Director.
El flux de run() és idèntic; només canvia el storage.
2026-05-22 12:35:19 +02:00
JailDesigner 7fc8e48596 Merge branch 'feat/title-3d': escena del títol migrada a 3D real 2026-05-22 12:12:22 +02:00
JailDesigner ff518195f8 fix(title): comentari trencat per la substitució sed del cleanup 2026-05-22 12:06:48 +02:00
JailDesigner 54d3e683a1 refactor(title): la 3D és l'única — elimina backup 2D i renomena als noms canònics 2026-05-22 12:04:16 +02:00
JailDesigner a29c2b9cc2 fix(ship-3d): exit convergeix al VP sense travessar-lo (sense creuament entre naus) 2026-05-22 11:57:16 +02:00
JailDesigner 85e7e70767 feat(title-3d): horitzó ampliat (starfield Z=1500, naus exiting travessen el VP) 2026-05-22 11:50:26 +02:00
JailDesigner 3f10c61e22 tweak(ship-3d): SHIP_FLOAT_SCALE a 2.0 2026-05-22 11:40:47 +02:00
JailDesigner 5de9a5003b tweak(ship-3d): descans més amunt i naus més grans (FLOAT_SCALE 1.5, TARGET_DIST 480) 2026-05-22 11:30:54 +02:00
81 changed files with 3304 additions and 3938 deletions
+12 -6
View File
@@ -135,9 +135,8 @@ add_dependencies(${PROJECT_NAME} resource_pack)
# --- COMPILACIÓ DE SHADERS GLSL → SPIR-V (headers C++ embedits) --- # --- COMPILACIÓ DE SHADERS GLSL → SPIR-V (headers C++ embedits) ---
# Compila els shaders .glsl a SPIR-V i els converteix en headers C++ embedits # Compila els shaders .glsl a SPIR-V i els converteix en headers C++ embedits
# (source/core/rendering/gpu/spv/*.h). Aquests headers es commiteen al repo: # (source/core/rendering/gpu/spv/*.h). Aquests headers es commiteen al repo,
# en macOS no cal glslc (els headers ja existeixen). En Linux/Windows glslc # així que glslc només és necessari quan canvien els .glsl o falten headers.
# és obligatori per regenerar els headers en cada canvi del GLSL.
# #
# Per a macOS hi ha a més els headers MSL escrits a mà a source/core/rendering/gpu/msl/. # Per a macOS hi ha a més els headers MSL escrits a mà a source/core/rendering/gpu/msl/.
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/shaders") set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/shaders")
@@ -156,6 +155,13 @@ set(ALL_SHADER_SOURCES
"${SHADERS_DIR}/postfx.frag.glsl" "${SHADERS_DIR}/postfx.frag.glsl"
"${SHADERS_DIR}/bloom.frag.glsl" "${SHADERS_DIR}/bloom.frag.glsl"
) )
set(ALL_SHADER_HEADERS_PRESENT TRUE)
foreach(_spv_header IN LISTS ALL_SHADER_HEADERS)
if(NOT EXISTS "${_spv_header}")
set(ALL_SHADER_HEADERS_PRESENT FALSE)
break()
endif()
endforeach()
find_program(GLSLC_EXE NAMES glslc HINTS ${Vulkan_GLSLC_EXECUTABLE}) find_program(GLSLC_EXE NAMES glslc HINTS ${Vulkan_GLSLC_EXECUTABLE})
if(GLSLC_EXE) if(GLSLC_EXE)
add_custom_command( add_custom_command(
@@ -172,10 +178,10 @@ if(GLSLC_EXE)
add_custom_target(shaders DEPENDS ${ALL_SHADER_HEADERS}) add_custom_target(shaders DEPENDS ${ALL_SHADER_HEADERS})
add_dependencies(${PROJECT_NAME} shaders) add_dependencies(${PROJECT_NAME} shaders)
message(STATUS "Shaders: glslc trobat (${GLSLC_EXE}); headers SPV es regeneraran si canvia el GLSL") message(STATUS "Shaders: glslc trobat (${GLSLC_EXE}); headers SPV es regeneraran si canvia el GLSL")
elseif(APPLE) elseif(ALL_SHADER_HEADERS_PRESENT)
message(STATUS "Shaders: glslc no trobat en macOS — s'usaran els headers SPV ja commiteats") message(STATUS "Shaders: glslc no trobat — s'usaran els headers SPV ja commiteats al repo")
else() else()
message(FATAL_ERROR "glslc no trobat: instal·la 'shaderc' o 'vulkan-sdk' per compilar shaders SPIR-V (obligatori a Linux/Windows)") message(FATAL_ERROR "glslc no trobat i falten headers SPV: instal·la 'shaderc' o 'vulkan-sdk' per generar-los")
endif() endif()
# --- STATIC ANALYSIS / FORMAT TARGETS --- # --- STATIC ANALYSIS / FORMAT TARGETS ---
+5 -1
View File
@@ -1,7 +1,11 @@
# enemy_pentagon.shp - ORNI enemic (pentàgon regular, radi=20) # enemy_pentagon.shp - ORNI enemic (pentàgon doble concentric, radi exterior=20)
name: enemy_pentagon name: enemy_pentagon
scale: 1.0 scale: 1.0
center: 0, 0 center: 0, 0
# Pentàgon exterior (vèrtex apuntant amunt, radi 20)
polyline: 0,-20 19.02,-6.18 11.76,16.18 -11.76,16.18 -19.02,-6.18 0,-20 polyline: 0,-20 19.02,-6.18 11.76,16.18 -11.76,16.18 -19.02,-6.18 0,-20
# Pentàgon interior (radi 10, rotat 36° → vèrtex apuntant a les arestes exteriors)
polyline: 5.88,-8.09 9.51,3.09 0,10 -9.51,3.09 -5.88,-8.09 5.88,-8.09
+8 -1
View File
@@ -1,7 +1,14 @@
# enemy_square.shp - ORNI enemic (quadrat regular, radi=20) # enemy_square.shp - ORNI enemic (rombe, radi=20) + ull amb pupil·la al centre
name: enemy_square name: enemy_square
scale: 1.0 scale: 1.0
center: 0, 0 center: 0, 0
# Rombe exterior
polyline: 0,-20 20,0 0,20 -20,0 0,-20 polyline: 0,-20 20,0 0,20 -20,0 0,-20
# Ull (dos arcs units, forma d'almetlla). Amplada 20px, altura 8px.
polyline: -10,0 -5,-3 0,-4 5,-3 10,0 5,3 0,4 -5,3 -10,0
# Pupil·la (octàgon, radi 2) al centre
polyline: 0,-2 1.41,-1.41 2,0 1.41,1.41 0,2 -1.41,1.41 -2,0 -1.41,-1.41 0,-2
-28
View File
@@ -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
-21
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
# title_flash.shp - Sparkle 4-puntes amb costats còncaus (Atari-style)
# 4 puntes als cardinals (radi 30) i valls còncaus als 45° (corba Bezier
# quadràtica amb control point ±8). 5 punts per arc subdividint la corba.
name: title_flash
scale: 1.0
center: 0, 0
polyline: 0,-30 3.76,-21.76 8.64,-14.64 14.64,-8.64 21.76,-3.76 30,0 21.76,3.76 14.64,8.64 8.64,14.64 3.76,21.76 0,30 -3.76,21.76 -8.64,14.64 -14.64,8.64 -21.76,3.76 -30,0 -21.76,-3.76 -14.64,-8.64 -8.64,-14.64 -3.76,-21.76 0,-30
Binary file not shown.
Binary file not shown.
+4 -4
View File
@@ -7,7 +7,7 @@ metadata:
description: "Progressive difficulty curve from novice to expert" description: "Progressive difficulty curve from novice to expert"
stages: stages:
# STAGE 1: Tutorial - Only pentagons, slow speed # STAGE 1: Tutorial - Mix de tots els tipus, velocitat lenta
- stage_id: 1 - stage_id: 1
total_enemies: 50 total_enemies: 50
spawn_config: spawn_config:
@@ -15,9 +15,9 @@ stages:
initial_delay: 0.3 initial_delay: 0.3
spawn_interval: 0.4 spawn_interval: 0.4
enemy_distribution: enemy_distribution:
pentagon: 100 pentagon: 34
cuadrado: 0 cuadrado: 33
molinillo: 0 molinillo: 33
difficulty_multipliers: difficulty_multipliers:
speed_multiplier: 0.7 speed_multiplier: 0.7
rotation_multiplier: 0.8 rotation_multiplier: 0.8
+1 -1
View File
@@ -46,7 +46,7 @@ namespace Ja {
}; };
// --- Constants --- // --- Constants ---
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20; inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 50;
inline constexpr int MAX_GROUPS = 2; inline constexpr int MAX_GROUPS = 2;
// Cap superior de canals que poden estar simultàniament reproduint un so // Cap superior de canals que poden estar simultàniament reproduint un so
// con efecte (eco/reverb). Si está al límit, las noves crides con efecte // con efecte (eco/reverb). Si está al límit, las noves crides con efecte
-11
View File
@@ -12,7 +12,6 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <functional>
#include <string> #include <string>
namespace Config { namespace Config {
@@ -67,14 +66,4 @@ namespace Config {
bool console{false}; bool console{false};
}; };
// Capa de persistència delegada cap a l'EngineConfig. Permet al Director
// orquestrar init/load/save sense conèixer cap esquema concret (YAML,
// SQLite, ...) ni la capa que el conté (`game/config_yaml.cpp`).
struct ConfigPersistence {
std::function<void()> init_defaults; // Restaura valors per defecte
std::function<void(const std::string& path)> set_path; // Indica on guardar
std::function<bool()> load; // Llegeix path → EngineConfig
std::function<bool()> save; // Escriu EngineConfig → path
};
} // namespace Config } // namespace Config
+1
View File
@@ -26,6 +26,7 @@
#include "core/defaults/playfield.hpp" #include "core/defaults/playfield.hpp"
#include "core/defaults/rendering.hpp" #include "core/defaults/rendering.hpp"
#include "core/defaults/ship.hpp" #include "core/defaults/ship.hpp"
#include "core/defaults/starfield_parallax.hpp"
#include "core/defaults/title.hpp" #include "core/defaults/title.hpp"
#include "core/defaults/trail.hpp" #include "core/defaults/trail.hpp"
#include "core/defaults/window.hpp" #include "core/defaults/window.hpp"
+1
View File
@@ -39,6 +39,7 @@ namespace Defaults::Sound {
constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa
constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit
constexpr const char* HIT = "effects/hit.wav"; // Enemic ferit (primer impacte → HURT) constexpr const char* HIT = "effects/hit.wav"; // Enemic ferit (primer impacte → HURT)
constexpr const char* HURT = "effects/hurt.wav"; // Nau pròpia entra a HURT
constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
constexpr const char* LOGO = "effects/logo.wav"; // Logo constexpr const char* LOGO = "effects/logo.wav"; // Logo
+42
View File
@@ -5,6 +5,48 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
namespace Defaults::FX::Glow {
// Neon glow per outline gruixut, aplicat automàticament per renderShape.
// Els gruixos d'halo són RÀTIOS del bounding_radius de la shape (escalat
// per scale), de manera que un pentàgon (radius 20) té halo gros i una bala
// (radius 3) té halo subtil. El core (últim pass) usa el gruix de línia
// global (1.5px) — no escala amb la shape.
//
// Cap superior: si la shape és molt gran (logos del títol, intro), el
// bounding_radius es satura a aquest valor — així cap shape té més
// glow que el pentàgon (referència de gameplay).
constexpr float MAX_REFERENCE_RADIUS = 20.0F;
struct Pass {
float thickness_ratio; // % del bounding_radius*scale. <0 → usa core (gruix global)
float alpha;
};
constexpr Pass PASSES[] = {
{.thickness_ratio = 0.55F, .alpha = 0.07F},
{.thickness_ratio = 0.35F, .alpha = 0.14F},
{.thickness_ratio = 0.20F, .alpha = 0.28F},
{.thickness_ratio = -1.0F, .alpha = 1.0F}, // core: línia "real"
};
// Glow per a línies "raw" (sense shape). Gruixos absoluts (px), no
// ratios — una línia individual no té bounding radius. Útil per a
// partícules de firework, sparks, etc.
namespace Line {
struct Pass {
float thickness; // px. <0 → usa el thickness passat pel caller (core)
float alpha;
};
constexpr Pass PASSES[] = {
{.thickness = 18.0F, .alpha = 0.10F},
{.thickness = 12.0F, .alpha = 0.20F},
{.thickness = 6.0F, .alpha = 0.40F},
{.thickness = -1.0F, .alpha = 1.0F}, // core: línia "real"
};
} // namespace Line
} // namespace Defaults::FX::Glow
namespace Defaults::FX::Firework { namespace Defaults::FX::Firework {
// Color per defecte. La caller pot fer override (p.ex. heretar del pare), // Color per defecte. La caller pot fer override (p.ex. heretar del pare),
+40 -40
View File
@@ -1,4 +1,4 @@
// enemies.hpp - Configuració per tipus d'enemic (Pentagon/Cuadrado/Molinillo), spawn i scoring // enemies.hpp - Configuració per tipus d'enemic (Pentagon/Square/Molinillo), spawn i scoring
// © 2026 JailDesigner // © 2026 JailDesigner
#pragma once #pragma once
@@ -17,57 +17,57 @@ namespace Defaults::Enemies {
// Pentagon (esquivador - zigzag evasion) // Pentagon (esquivador - zigzag evasion)
namespace Pentagon { namespace Pentagon {
constexpr float VELOCITAT = 35.0F; // px/s (slightly slower) constexpr float SPEED = 35.0F; // px/s (slightly slower)
constexpr float MASS = 5.0F; // Masa estándar constexpr float MASS = 5.0F; // Masa estándar
constexpr float CANVI_ANGLE_PROB = 0.20F; // 20% per wall hit (frequent zigzag) constexpr float ANGLE_CHANGE_PROB = 0.20F; // 20% per wall hit (frequent zigzag)
constexpr float CANVI_ANGLE_MAX = 1.0F; // Max random angle change (rad) constexpr float ANGLE_CHANGE_MAX = 1.0F; // Max random angle change (rad)
constexpr float ZIGZAG_PROB_PER_SECOND = 0.8F; // Probabilidad de zigzag por segundo constexpr float ZIGZAG_PROB_PER_SECOND = 0.8F; // Probabilidad de zigzag por segundo
constexpr float DROTACIO_MIN = 0.75F; // Min visual rotation (rad/s) [+50%] constexpr float ROTATION_DELTA_MIN = 0.75F; // Min visual rotation (rad/s) [+50%]
constexpr float DROTACIO_MAX = 3.75F; // Max visual rotation (rad/s) [+50%] constexpr float ROTATION_DELTA_MAX = 3.75F; // Max visual rotation (rad/s) [+50%]
constexpr const char* SHAPE_FILE = "enemy_pentagon.shp"; constexpr const char* SHAPE_FILE = "enemy_pentagon.shp";
} // namespace Pentagon } // namespace Pentagon
// Cuadrado (perseguidor - tracks player) // Square (perseguidor - tracks player)
namespace Cuadrado { namespace Square {
constexpr float VELOCITAT = 40.0F; // px/s (medium speed) constexpr float SPEED = 40.0F; // px/s (medium speed)
constexpr float MASS = 8.0F; // Más pesado, "tanque" constexpr float MASS = 8.0F; // Más pesado, "tanque"
constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0) constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0)
constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates
constexpr float DROTACIO_MIN = 0.3F; // Slow rotation [+50%] constexpr float ROTATION_DELTA_MIN = 0.3F; // Slow rotation [+50%]
constexpr float DROTACIO_MAX = 1.5F; // [+50%] constexpr float ROTATION_DELTA_MAX = 1.5F; // [+50%]
constexpr const char* SHAPE_FILE = "enemy_square.shp"; constexpr const char* SHAPE_FILE = "enemy_square.shp";
} // namespace Cuadrado } // namespace Square
// Molinillo (agressiu - fast straight lines, proximity spin-up) // Molinillo (agressiu - fast straight lines, proximity spin-up)
namespace Molinillo { namespace Pinwheel {
constexpr float VELOCITAT = 50.0F; // px/s (fastest) constexpr float SPEED = 50.0F; // px/s (fastest)
constexpr float MASS = 4.0F; // Más liviano, ágil constexpr float MASS = 4.0F; // Más liviano, ágil
constexpr float CANVI_ANGLE_PROB = 0.05F; // 5% per wall hit (rare direction change) constexpr float ANGLE_CHANGE_PROB = 0.05F; // 5% per wall hit (rare direction change)
constexpr float CANVI_ANGLE_MAX = 0.3F; // Small angle adjustments constexpr float ANGLE_CHANGE_MAX = 0.3F; // Small angle adjustments
constexpr float DROTACIO_MIN = 3.0F; // Base rotation (rad/s) [+50%] constexpr float ROTATION_DELTA_MIN = 3.0F; // Base rotation (rad/s) [+50%]
constexpr float DROTACIO_MAX = 6.0F; // [+50%] constexpr float ROTATION_DELTA_MAX = 6.0F; // [+50%]
constexpr float DROTACIO_PROXIMITY_MULTIPLIER = 3.0F; // Spin-up multiplier when near ship constexpr float ROTATION_DELTA_PROXIMITY_MULTIPLIER = 3.0F; // Spin-up multiplier when near ship
constexpr float PROXIMITY_DISTANCE = 100.0F; // Distance threshold (px) constexpr float PROXIMITY_DISTANCE = 100.0F; // Distance threshold (px)
constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp"; constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp";
} // namespace Molinillo } // namespace Pinwheel
// Animation parameters (shared) // Animation parameters (shared)
namespace Animation { namespace Animation {
// Palpitation // Palpitation
constexpr float PALPITACIO_TRIGGER_PROB = 0.01F; // 1% chance per second constexpr float PULSE_TRIGGER_PROB = 0.01F; // 1% chance per second
constexpr float PALPITACIO_DURACIO_MIN = 1.0F; // Min duration (seconds) constexpr float PULSE_DURATION_MIN = 1.0F; // Min duration (seconds)
constexpr float PALPITACIO_DURACIO_MAX = 3.0F; // Max duration (seconds) constexpr float PULSE_DURATION_MAX = 3.0F; // Max duration (seconds)
constexpr float PALPITACIO_AMPLITUD_MIN = 0.08F; // Min scale variation constexpr float PULSE_AMPLITUD_MIN = 0.08F; // Min scale variation
constexpr float PALPITACIO_AMPLITUD_MAX = 0.20F; // Max scale variation constexpr float PULSE_AMPLITUD_MAX = 0.20F; // Max scale variation
constexpr float PALPITACIO_FREQ_MIN = 1.5F; // Min frequency (Hz) constexpr float PULSE_FREQ_MIN = 1.5F; // Min frequency (Hz)
constexpr float PALPITACIO_FREQ_MAX = 3.0F; // Max frequency (Hz) constexpr float PULSE_FREQ_MAX = 3.0F; // Max frequency (Hz)
// Rotation acceleration // Rotation acceleration
constexpr float ROTACIO_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent] constexpr float ROTATION_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent]
constexpr float ROTACIO_ACCEL_DURACIO_MIN = 3.0F; // Min transition time constexpr float ROTATION_ACCEL_DURATION_MIN = 3.0F; // Min transition time
constexpr float ROTACIO_ACCEL_DURACIO_MAX = 8.0F; // Max transition time constexpr float ROTATION_ACCEL_DURATION_MAX = 8.0F; // Max transition time
constexpr float ROTACIO_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic] constexpr float ROTATION_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic]
constexpr float ROTACIO_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic] constexpr float ROTATION_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic]
} // namespace Animation } // namespace Animation
// Wounded state (entre primer impacto y explosión) // Wounded state (entre primer impacto y explosión)
@@ -93,9 +93,9 @@ namespace Defaults::Enemies {
// Scoring system (puntuación per type de enemy) // Scoring system (puntuación per type de enemy)
namespace Scoring { namespace Scoring {
constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s) constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s)
constexpr int QUADRAT_SCORE = 150; // Cuadrado (perseguidor, 40 px/s) constexpr int SQUARE_SCORE = 150; // Square (perseguidor, 40 px/s)
constexpr int MOLINILLO_SCORE = 200; // Molinillo (agressiu, 50 px/s) constexpr int PINWHEEL_SCORE = 200; // Molinillo (agressiu, 50 px/s)
} // namespace Scoring } // namespace Scoring
} // namespace Defaults::Enemies } // namespace Defaults::Enemies
+1 -1
View File
@@ -6,7 +6,7 @@
namespace Defaults::Entities { namespace Defaults::Entities {
constexpr int MAX_ORNIS = 15; constexpr int MAX_ORNIS = 15;
constexpr int MAX_BALES = 50; constexpr int MAX_BULLETS = 50;
constexpr float SHIP_RADIUS = 12.0F; constexpr float SHIP_RADIUS = 12.0F;
constexpr float ENEMY_RADIUS = 20.0F; constexpr float ENEMY_RADIUS = 20.0F;
+10 -5
View File
@@ -15,15 +15,20 @@ namespace Defaults::Game {
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
// Valores centinela del temporitzador de mort per-jugador. // Valores centinela del temporitzador de mort per-jugador.
constexpr float HIT_TIMER_INACTIVE_PLAYER = 999.0F; // Jugador permanentment inactiu constexpr float HIT_TIMER_INACTIVE_PLAYER = 999.0F; // Jugador permanentment inactiu
constexpr float HIT_TIMER_TRIGGER_DEATH = 0.001F; // Trigger inicial post-impacte (>0 sense disparar regla) constexpr float HIT_TIMER_TRIGGER_DEATH = 0.001F; // Trigger inicial post-impacte (>0 sense disparar regla)
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 0.80F; // 80% hitbox (generous) // Ha de ser ≥ 1.0F: PhysicsWorld separa els cossos al contacte exacte (dist == suma de radis),
// així que un amplificador < 1 fa que el check de gameplay no es dispari mai. Marge petit
// (1.05F) per tolerar floating-point i petites separacions post-impuls.
constexpr float COLLISION_SHIP_ENEMY_AMPLIFIER = 1.05F;
constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous) constexpr float COLLISION_BULLET_ENEMY_AMPLIFIER = 1.15F; // 115% hitbox (generous)
// Wounded chain: el rebot físic separa els cossos abans que arribi
// la detecció gameplay; amplier generós perquè el toc compti.
constexpr float COLLISION_WOUNDED_CHAIN_AMPLIFIER = 1.25F;
// Friendly fire system // Friendly fire system
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%) constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
constexpr float BULLET_GRACE_PERIOD = 0.2F; // Inmunidad post-disparo (s)
constexpr float BULLET_SPEED = 700.0F; // Velocidad escalar (px/s). Pascal: 7 px/frame × 20 FPS constexpr float BULLET_SPEED = 700.0F; // Velocidad escalar (px/s). Pascal: 7 px/frame × 20 FPS
// Transición LEVEL_START (mensajes aleatorios PRE-level) // Transición LEVEL_START (mensajes aleatorios PRE-level)
@@ -54,7 +59,7 @@ namespace Defaults::Game {
constexpr float INIT_HUD_SHIP2_RATIO_INIT = 0.20F; constexpr float INIT_HUD_SHIP2_RATIO_INIT = 0.20F;
constexpr float INIT_HUD_SHIP2_RATIO_END = 1.0F; constexpr float INIT_HUD_SHIP2_RATIO_END = 1.0F;
// Posición inicial de la nave en INIT_HUD (75% de altura de zona de juego) // Posición inicial de la nave en INIT_HUD (75% de altura de zone de juego)
constexpr float INIT_HUD_SHIP_START_Y_RATIO = 0.75F; // 75% desde el top de PLAYAREA constexpr float INIT_HUD_SHIP_START_Y_RATIO = 0.75F; // 75% desde el top de PLAYAREA
// Spawn positions (distribución horizontal para 2 jugadores) // Spawn positions (distribución horizontal para 2 jugadores)
+6 -6
View File
@@ -14,11 +14,11 @@ namespace Defaults::Palette {
// brillantor perceptual sota el bloom (sense alterar la identitat de color). // brillantor perceptual sota el bloom (sense alterar la identitat de color).
// El canal dominant es manté a 255 a cada color per maximitzar la saturació // El canal dominant es manté a 255 a cada color per maximitzar la saturació
// visible quan el halo s'expandeix. // visible quan el halo s'expandeix.
constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro
constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser
constexpr SDL_Color PENTAGON = {.r = 155, .g = 195, .b = 255, .a = 255}; // Azul "esquivador" constexpr SDL_Color PENTAGON = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cyan pur "esquivador"
constexpr SDL_Color QUADRAT = {.r = 255, .g = 140, .b = 140, .a = 255}; // Rojo "tank" constexpr SDL_Color SQUARE = {.r = 255, .g = 0, .b = 0, .a = 255}; // Roig pur "tank"
constexpr SDL_Color MOLINILLO = {.r = 255, .g = 160, .b = 255, .a = 255}; // Magenta agresivo constexpr SDL_Color PINWHEEL = {.r = 255, .g = 0, .b = 255, .a = 255}; // Magenta pur "agressiu"
constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido
} // namespace Defaults::Palette } // namespace Defaults::Palette
+20 -12
View File
@@ -18,16 +18,24 @@ namespace Defaults::Physics {
constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic
} // namespace Bullet } // namespace Bullet
// Ship → enemy: impuls explícit aplicat a l'enemic en el moment exacte
// que la nau mor per col·lisió amb ell (afegit per damunt del rebot
// natural de PhysicsWorld, que ja és present però subtil amb la
// damping de la nau).
namespace Ship {
constexpr float DEATH_IMPACT_MOMENTUM_FACTOR = 0.3F;
} // namespace Ship
// Explosions (debris physics) // Explosions (debris physics)
namespace Debris { namespace Debris {
constexpr float VELOCITAT_BASE = 80.0F; // Velocidad inicial (px/s) constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
constexpr float VARIACIO_VELOCITAT = 40.0F; // ±variació aleatòria (px/s) constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s)
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²) constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
constexpr float ROTACIO_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s) constexpr float ROTATION_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
constexpr float ROTACIO_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s) constexpr float ROTATION_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION) constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION)
constexpr float SHRINK_RATE = 1.0F; // Reducció de mida (1.0 = encoge a 0 al final del min_lifetime) constexpr float SHRINK_RATE = 1.0F; // Reducció de mida (1.0 = encoge a 0 al final del min_lifetime)
// Política de mort: passat el min_lifetime, el fragment mor quan la // Política de mort: passat el min_lifetime, el fragment mor quan la
// seva velocity cau per sota d'aquest llindar. Així els fragments // seva velocity cau per sota d'aquest llindar. Així els fragments
@@ -40,9 +48,9 @@ namespace Defaults::Physics {
constexpr float RESTITUTION_BOUNDS = 0.7F; constexpr float RESTITUTION_BOUNDS = 0.7F;
// Herència de velocity angular (trayectorias curvas) // Herència de velocity angular (trayectorias curvas)
constexpr float FACTOR_HERENCIA_MIN = 0.7F; // Mínimo 70% del drotacio heredat constexpr float INHERITANCE_FACTOR_MIN = 0.7F; // Mínimo 70% del drotacio heredat
constexpr float FACTOR_HERENCIA_MAX = 1.0F; // Màxim 100% del drotacio heredat constexpr float INHERITANCE_FACTOR_MAX = 1.0F; // Màxim 100% del drotacio heredat
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²) constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
// Velocity heredada de la nau a l'explosió (80% del feel original). // Velocity heredada de la nau a l'explosió (80% del feel original).
constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F; constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F;
@@ -60,7 +68,7 @@ namespace Defaults::Physics {
// Angular velocity sin for trajectory inheritance // Angular velocity sin for trajectory inheritance
// Excess above this threshold is converted to tangential linear velocity // Excess above this threshold is converted to tangential linear velocity
// Prevents "vortex trap" problem with high-rotation enemies // Prevents "vortex trap" problem with high-rotation enemies
constexpr float VELOCITAT_ROT_MAX = 1.5F; // rad/s (~86°/s) constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s)
} // namespace Debris } // namespace Debris
} // namespace Defaults::Physics } // namespace Defaults::Physics
+34 -17
View File
@@ -3,16 +3,21 @@
#pragma once #pragma once
#include <SDL3/SDL.h>
namespace Defaults::Playfield { namespace Defaults::Playfield {
// Estructura de la graella (cel·les omplen tota la PLAYAREA) // Estructura de la graella (cel·les omplen tota la PLAYAREA)
constexpr int COLUMNS = 16; // cell_w = PLAYAREA.w / 16 constexpr int COLUMNS = 16; // cell_w = PLAYAREA.w / 16
constexpr int ROWS = 8; // cell_h = PLAYAREA.h / 8 constexpr int ROWS = 8; // cell_h = PLAYAREA.h / 8
constexpr int SUBDIVISIONS = 5; // cada cel·la principal es divideix en N subcel·les constexpr int SUBDIVISIONS = 4; // cada cel·la principal es divideix en N subcel·les
// Brillo respecte al color global (border = 1.0) // Brillo respecte al color global (border = 1.0)
constexpr float GRID_BRIGHTNESS = 0.15F; constexpr float GRID_BRIGHTNESS = 0.20F;
constexpr float SUBGRID_BRIGHTNESS = 0.05F; constexpr float SUBGRID_BRIGHTNESS = 0.10F;
// Color de la rejilla (lila/violeta synthwave). Es modula amb brillantor.
constexpr SDL_Color GRID_COLOR = {.r = 160, .g = 80, .b = 255, .a = 255};
// Animació de creació amb timer intern del Playfield. // Animació de creació amb timer intern del Playfield.
// L'animació total cobreix tot l'INIT_HUD (3 s). Cada línia es pinta en // L'animació total cobreix tot l'INIT_HUD (3 s). Cada línia es pinta en
@@ -25,20 +30,32 @@ namespace Defaults::Playfield {
constexpr float HEAD_LENGTH_PX = 8.0F; // longitud en píxels lògics del tram brillant constexpr float HEAD_LENGTH_PX = 8.0F; // longitud en píxels lògics del tram brillant
constexpr float HEAD_BRIGHTNESS = 0.0F; // brillo del cap (= border) constexpr float HEAD_BRIGHTNESS = 0.0F; // brillo del cap (= border)
// Orbit (oscil·lació transversal de la línia quan la nau hi passa a prop). // Ripples: deformacions circulars que travessen la graella com ones d'aigua.
constexpr float ORBIT_AMPLITUDE_MAX_PX = 3.0F; // desplaçament transversal màxim // Cada ripple desplaça radialment cap a fora els vèrtexs de les línies que
constexpr float ORBIT_DECAY_PER_S = 4.0F; // decaiment de l'amplitud (px/s) // travessa, amb una envoltant que decau a les vores de l'anell i amb el temps.
constexpr float ORBIT_FREQ_HZ = 8.0F; // freqüència del sin namespace Ripple {
constexpr float ORBIT_PROXIMITY_PX = 12.0F; // distància max de la línia per excitar-la constexpr int POOL_SIZE = 32;
constexpr float ORBIT_SHIP_SPEED_THRESHOLD = 60.0F; // velocitat mínima per excitar (px/s)
// Pulse (reacció a fireworks: punt brillant que es propaga al llarg de la // Ones grans (explosions / fireworks).
// línia a partir del punt de spawn). constexpr float BIG_AMPLITUDE_PX = 10.0F;
constexpr int MAX_PULSES_PER_LINE = 2; constexpr float BIG_SPEED_PX_S = 320.0F;
constexpr float PULSE_LIFETIME_S = 1.0F; // temps total fins desaparèixer constexpr float BIG_LIFETIME_S = 1.4F;
constexpr float PULSE_SPREAD_PER_S = 300.0F; // px/s de propagació (cap a cada extrem) constexpr float BIG_THICKNESS_PX = 40.0F;
constexpr unsigned char PULSE_COLOR_R = 180;
constexpr unsigned char PULSE_COLOR_G = 230; // Ones petites (pas de nau, cadència estil trail).
constexpr unsigned char PULSE_COLOR_B = 255; constexpr float SMALL_AMPLITUDE_PX = 2.5F;
constexpr float SMALL_SPEED_PX_S = 160.0F;
constexpr float SMALL_LIFETIME_S = 0.55F;
constexpr float SMALL_THICKNESS_PX = 18.0F;
// Cadència "soltar gotetes" per nau (patró TrailManager).
constexpr float SHIP_COOLDOWN_S = 0.10F;
constexpr float SHIP_COOLDOWN_JITTER_S = 0.03F;
constexpr float SHIP_SPEED_THRESHOLD_PX_S = 80.0F;
// Subdivisió de línies quan estan dins una ripple.
constexpr int MAIN_SEGMENTS = 24; // línies principals
constexpr int SUB_SEGMENTS = 12; // sub-graella
} // namespace Ripple
} // namespace Defaults::Playfield } // namespace Defaults::Playfield
+6
View File
@@ -24,4 +24,10 @@ namespace Defaults::Ship {
constexpr float VISUAL_PUSH_DIVISOR = 33.33F; // SPEED / DIVISOR = empuje visual constexpr float VISUAL_PUSH_DIVISOR = 33.33F; // SPEED / DIVISOR = empuje visual
constexpr float VISUAL_SCALE_DIVISOR = 12.0F; // SCALE = 1 + (PUSH / DIVISOR) constexpr float VISUAL_SCALE_DIVISOR = 12.0F; // SCALE = 1 + (PUSH / DIVISOR)
// Estat "ferit": entre primera col·lisió amb enemic i recuperació o segona col·lisió mortal.
namespace Hurt {
constexpr float DURATION = 15.0F; // Segons en estat ferit (provisional)
constexpr float BLINK_HZ = 10.0F; // Freqüència parpelleig color normal ↔ ferit
} // namespace Hurt
} // namespace Defaults::Ship } // namespace Defaults::Ship
@@ -0,0 +1,36 @@
// starfield_parallax.hpp - Capa de fons del playfield: estrelles 2D amb parallax
// © 2026 JailDesigner
//
// 3 capes de profunditat. Cada capa té estrelles amb brillantor, mida i
// factor parallax propis. Les més properes són més brillants i grans i es
// mouen més ràpid quan el món es desplaça; les més llunyanes són tènues i
// petites i amb prou feines es mouen.
#pragma once
namespace Defaults::StarfieldParallax {
namespace Far {
constexpr int COUNT = 60;
constexpr float BRIGHTNESS = 0.15F;
constexpr float PARALLAX_FACTOR = 0.15F; // multiplicador sobre world_velocity
constexpr int SIZE_PX = 1; // 1 px (punt)
} // namespace Far
namespace Mid {
constexpr int COUNT = 50;
constexpr float BRIGHTNESS = 0.30F;
constexpr float PARALLAX_FACTOR = 0.35F;
constexpr int SIZE_PX = 2; // creu de 3x3 (extensió ±1)
} // namespace Mid
namespace Near {
constexpr int COUNT = 40;
constexpr float BRIGHTNESS = 0.55F;
constexpr float PARALLAX_FACTOR = 0.70F;
constexpr int SIZE_PX = 3; // creu de 5x5 (extensió ±2)
} // namespace Near
constexpr int TOTAL_COUNT = Far::COUNT + Mid::COUNT + Near::COUNT;
} // namespace Defaults::StarfieldParallax
+46 -5
View File
@@ -3,6 +3,8 @@
#pragma once #pragma once
#include <SDL3/SDL.h>
#include <cmath> #include <cmath>
#include "core/defaults/game.hpp" #include "core/defaults/game.hpp"
@@ -66,7 +68,7 @@ namespace Defaults::Title {
constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotante: scale base constexpr float FLOATING_SCALE = 1.0F * SHIP_BASE_SCALE; // Flotante: scale base
// Offset de entrada (ajustat automáticoament a l'scale) // Offset de entrada (ajustat automáticoament a l'scale)
// Fórmula: (radi màxim de la ship * scale de entrada) + margen // Fórmula: (radius màxim de la ship * scale de entrada) + margen
constexpr float ENTRY_OFFSET = (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN; constexpr float ENTRY_OFFSET = (SHIP_MAX_RADIUS * ENTRY_SCALE_START) + ENTRY_OFFSET_MARGIN;
// Vec2 de fuga (centro para l'animación de salida) // Vec2 de fuga (centro para l'animación de salida)
@@ -79,7 +81,7 @@ namespace Defaults::Title {
// Durades de animación // Durades de animación
constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons) constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons)
constexpr float EXIT_DURATION = 1.0F; // Salida (segons) constexpr float EXIT_DURATION = 1.5F; // Salida (segons)
// Flotació (oscil·lació reduïda y diferenciada per ship) // Flotació (oscil·lació reduïda y diferenciada per ship)
constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels) constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels)
@@ -94,9 +96,6 @@ namespace Defaults::Title {
constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament
constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s después constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s después
// Delay global antes de start l'animación de entrada al state MAIN
constexpr float ENTRANCE_DELAY = 5.0F; // Temps de espera antes que las naves entrin
// Multiplicadors de freqüència para cada ship (variació sutil ±12%) // Multiplicadors de freqüència para cada ship (variació sutil ±12%)
constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta
constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida
@@ -126,4 +125,46 @@ namespace Defaults::Title {
constexpr float TEXT_SPACING = 2.0F; constexpr float TEXT_SPACING = 2.0F;
} // namespace Layout } // namespace Layout
// Coreografia de la seqüència d'entrada al state MAIN.
// Tots els elements (logo, footer, naus, press start) entren ordenadament
// segons aquests thresholds. Vegeu title_scene.cpp/updateMainState.
//
// Per al logo i el footer, l'efecte simula un moviment 3D des de l'usuari
// cap al VP: el text arrenca gran i a la posició projectada extrema (com
// si estigués prop de la càmera, fora de pantalla) i acaba a la seva
// posició final amb escala normal (com si hagués aterrat al VP). Pivot:
// centre de pantalla (= projecció del VP 3D).
namespace Sequence {
// Factor d'escala inicial. >1 = sprite gran a l'inici (prop de l'usuari).
// La posició inicial es deriva: pivot=centre, delta multiplicat per aquest factor.
constexpr float LOGO_INTRO_SCALE_START = 2.5F;
constexpr float FOOTER_INTRO_SCALE_START = 2.5F;
// Durades de les animacions d'entrada (segons).
constexpr float LOGO_ENTRY_DURATION = 1.2F;
constexpr float JAILGAMES_ENTRY_DURATION = 0.7F;
constexpr float COPYRIGHT_ENTRY_DURATION = 0.7F;
// Stagger "pam-pam" entre l'arrencada de JAILGAMES i la de COPYRIGHT.
constexpr float COPYRIGHT_STAGGER = 0.18F;
// Delays entre etapes.
constexpr float SHIPS_DELAY_AFTER_FOOTER = 0.20F;
constexpr float PRESS_START_DELAY_AFTER_SHIPS = 0.40F;
} // namespace Sequence
// Paleta neon de l'escena de títol (cian + magenta synthwave).
// alpha = 255 (sentinela "color vàlid") fa que el pipeline ignori
// el color global de l'oscil·lador per a aquesta crida.
namespace Colors {
constexpr SDL_Color LOGO_MAIN = {.r = 80, .g = 240, .b = 255, .a = 255}; // Cian elèctric
constexpr SDL_Color LOGO_SHADOW = {.r = 255, .g = 60, .b = 180, .a = 255}; // Magenta neon (offset)
constexpr SDL_Color SHIP_P1 = {.r = 255, .g = 100, .b = 200, .a = 255}; // Rosa hot
constexpr SDL_Color SHIP_P2 = {.r = 160, .g = 120, .b = 255, .a = 255}; // Violeta elèctric
constexpr SDL_Color STARFIELD = {.r = 200, .g = 220, .b = 255, .a = 255}; // Blanc-blau gel
constexpr SDL_Color PRESS_START = {.r = 255, .g = 200, .b = 70, .a = 255}; // Ambre neon
constexpr SDL_Color JAILGAMES_LOGO = {.r = 120, .g = 220, .b = 200, .a = 255}; // Teal suau
constexpr SDL_Color COPYRIGHT = {.r = 140, .g = 180, .b = 200, .a = 255}; // Gris-cian apagat
} // namespace Colors
} // namespace Defaults::Title } // namespace Defaults::Title
+10 -10
View File
@@ -23,12 +23,12 @@ namespace Graphics {
} }
void Border::bumpAt(Vec2 contact_point, float strength) { void Border::bumpAt(Vec2 contact_point, float strength) {
const SDL_FRect& zona = Defaults::Zones::PLAYAREA; const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
const std::array<float, SIDE_COUNT> DISTANCES = { const std::array<float, SIDE_COUNT> DISTANCES = {
/* TOP */ std::abs(contact_point.y - zona.y), /* TOP */ std::abs(contact_point.y - zone.y),
/* RIGHT */ std::abs((zona.x + zona.w) - contact_point.x), /* RIGHT */ std::abs((zone.x + zone.w) - contact_point.x),
/* BOTTOM */ std::abs((zona.y + zona.h) - contact_point.y), /* BOTTOM */ std::abs((zone.y + zone.h) - contact_point.y),
/* LEFT */ std::abs(contact_point.x - zona.x)}; /* LEFT */ std::abs(contact_point.x - zone.x)};
int closest_idx = 0; int closest_idx = 0;
float closest_dist = DISTANCES[0]; float closest_dist = DISTANCES[0];
@@ -71,11 +71,11 @@ namespace Graphics {
} // namespace } // namespace
void Border::draw() const { void Border::draw() const {
const SDL_FRect& zona = Defaults::Zones::PLAYAREA; const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
const int X1 = static_cast<int>(zona.x); const int X1 = static_cast<int>(zone.x);
const int Y1 = static_cast<int>(zona.y); const int Y1 = static_cast<int>(zone.y);
const int X2 = static_cast<int>(zona.x + zona.w); const int X2 = static_cast<int>(zone.x + zone.w);
const int Y2 = static_cast<int>(zona.y + zona.h); const int Y2 = static_cast<int>(zone.y + zone.h);
const int OFF_TOP = static_cast<int>(sides_[SIDE_TOP].displacement_px); const int OFF_TOP = static_cast<int>(sides_[SIDE_TOP].displacement_px);
const int OFF_RIGHT = static_cast<int>(sides_[SIDE_RIGHT].displacement_px); const int OFF_RIGHT = static_cast<int>(sides_[SIDE_RIGHT].displacement_px);
+218 -179
View File
@@ -5,8 +5,8 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdint>
#include <cstdlib> #include <cstdlib>
#include <limits>
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/rendering/line_renderer.hpp" #include "core/rendering/line_renderer.hpp"
@@ -21,20 +21,38 @@ namespace Graphics {
return 1.0F - (INV * INV * INV); return 1.0F - (INV * INV * INV);
} }
// Lerp del color base actual (oscil·lador) cap a un color destí en auto randUniform(float min_v, float max_v) -> float {
// funció de f ∈ [0, 1]. Alpha > 0 perquè line_renderer l'usi directe. const float NORM = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
auto lerpColor(SDL_Color target, float f) -> SDL_Color { return min_v + (NORM * (max_v - min_v));
const float CLAMPED = std::clamp(f, 0.0F, 1.0F); }
const SDL_Color BASE = Rendering::getLineColor();
const auto LERP_U8 = [&](unsigned char a, unsigned char b) { // Desplaçament radial acumulat al punt (px, py) sumant totes les ripples
const float OUT = (static_cast<float>(a) * (1.0F - CLAMPED)) + (static_cast<float>(b) * CLAMPED); // que el toquen. Retorna {dx, dy} a sumar a la posició original.
return static_cast<unsigned char>(OUT); auto computeRippleDisplacement(float px, float py, const Playfield::Ripple* const* hits, int n_hits) -> Vec2 {
}; float dx_total = 0.0F;
return SDL_Color{ float dy_total = 0.0F;
.r = LERP_U8(BASE.r, target.r), for (int i = 0; i < n_hits; i++) {
.g = LERP_U8(BASE.g, target.g), const auto& r = *hits[i];
.b = LERP_U8(BASE.b, target.b), const float RADIUS = r.age_s * r.speed_px_s;
.a = 255}; const float THICKNESS = r.thickness_px;
const float DX = px - r.center.x;
const float DY = py - r.center.y;
const float D = std::sqrt((DX * DX) + (DY * DY));
if (D < 0.001F) {
continue; // centre exacte: no hi ha direcció radial
}
const float PHASE = (D - RADIUS) / THICKNESS;
if (std::fabs(PHASE) >= 1.0F) {
continue; // fora de l'anell d'aquesta ripple
}
const float ENVELOPE = std::cos(PHASE * Defaults::Math::PI * 0.5F);
const float AMP_EFF = r.amplitude_px * (1.0F - (r.age_s / r.lifetime_s));
const float UX = DX / D;
const float UY = DY / D;
dx_total += UX * AMP_EFF * ENVELOPE;
dy_total += UY * AMP_EFF * ENVELOPE;
}
return Vec2{.x = dx_total, .y = dy_total};
} }
} // namespace } // namespace
@@ -46,101 +64,86 @@ namespace Graphics {
void Playfield::update(float delta_time) { void Playfield::update(float delta_time) {
elapsed_s_ += delta_time; elapsed_s_ += delta_time;
for (auto& ripple : ripples_) {
// Decau l'orbit i avança la fase del sin per cada línia. if (!ripple.active) {
const float ORBIT_DELTA_PHASE = Defaults::Playfield::ORBIT_FREQ_HZ * 2.0F * Defaults::Math::PI * delta_time; continue;
const float ORBIT_DEC = Defaults::Playfield::ORBIT_DECAY_PER_S * delta_time; }
for (auto& line : lines_) { ripple.age_s += delta_time;
line.orbit_phase += ORBIT_DELTA_PHASE; if (ripple.age_s >= ripple.lifetime_s) {
line.orbit_amplitude = std::max(0.0F, line.orbit_amplitude - ORBIT_DEC); ripple.active = false;
// Avança els pulses; els desactiva quan acaben de vida.
for (auto& pulse : line.pulses) {
if (!pulse.active) {
continue;
}
pulse.age_s += delta_time;
if (pulse.age_s >= Defaults::Playfield::PULSE_LIFETIME_S) {
pulse.active = false;
}
} }
} }
} }
void Playfield::spawnPulseAt(Line& line, float center_t) { auto Playfield::findFreeRipple() -> Ripple* {
for (auto& pulse : line.pulses) { Ripple* oldest = nullptr;
if (!pulse.active) { for (auto& ripple : ripples_) {
pulse.active = true; if (!ripple.active) {
pulse.age_s = 0.0F; return &ripple;
pulse.center_t = std::clamp(center_t, 0.0F, 1.0F); }
return; if (oldest == nullptr || ripple.age_s > oldest->age_s) {
oldest = &ripple;
} }
} }
// Cap slot lliure: substituïm el més vell. return oldest; // pool ple: substituïm la més vella
Pulse* oldest = line.pulses.data();
for (auto& pulse : line.pulses) {
if (pulse.age_s > oldest->age_s) {
oldest = &pulse;
}
}
oldest->active = true;
oldest->age_s = 0.0F;
oldest->center_t = std::clamp(center_t, 0.0F, 1.0F);
} }
void Playfield::notifyFireworkSpawn(Vec2 pos) { void Playfield::spawnBig(Vec2 pos) {
// Línia vertical més propera (per posició x) i horitzontal més propera (per y). Ripple* r = findFreeRipple();
Line* closest_v = nullptr; if (r == nullptr) {
Line* closest_h = nullptr;
float min_dx = std::numeric_limits<float>::max();
float min_dy = std::numeric_limits<float>::max();
for (auto& line : lines_) {
if (line.is_vertical) {
const float DX = std::abs(pos.x - line.start.x);
if (DX < min_dx) {
min_dx = DX;
closest_v = &line;
}
} else {
const float DY = std::abs(pos.y - line.start.y);
if (DY < min_dy) {
min_dy = DY;
closest_h = &line;
}
}
}
if (closest_v != nullptr) {
const float LINE_LEN = closest_v->end.y - closest_v->start.y;
const float CENTER_T = (LINE_LEN > 0.0F) ? (pos.y - closest_v->start.y) / LINE_LEN : 0.5F;
spawnPulseAt(*closest_v, CENTER_T);
}
if (closest_h != nullptr) {
const float LINE_LEN = closest_h->end.x - closest_h->start.x;
const float CENTER_T = (LINE_LEN > 0.0F) ? (pos.x - closest_h->start.x) / LINE_LEN : 0.5F;
spawnPulseAt(*closest_h, CENTER_T);
}
}
void Playfield::notifyShipPass(Vec2 pos, float speed_px_s) {
if (speed_px_s < Defaults::Playfield::ORBIT_SHIP_SPEED_THRESHOLD) {
return; return;
} }
const float MAX_DIST = Defaults::Playfield::ORBIT_PROXIMITY_PX; r->center = pos;
for (auto& line : lines_) { r->age_s = 0.0F;
// Distància perpendicular del punt a la línia (que és horitzontal o vertical). r->lifetime_s = Defaults::Playfield::Ripple::BIG_LIFETIME_S;
const float DIST = line.is_vertical r->speed_px_s = Defaults::Playfield::Ripple::BIG_SPEED_PX_S;
? std::abs(pos.x - line.start.x) r->amplitude_px = Defaults::Playfield::Ripple::BIG_AMPLITUDE_PX;
: std::abs(pos.y - line.start.y); r->thickness_px = Defaults::Playfield::Ripple::BIG_THICKNESS_PX;
if (DIST < MAX_DIST) { r->active = true;
line.orbit_amplitude = Defaults::Playfield::ORBIT_AMPLITUDE_MAX_PX; }
}
void Playfield::spawnSmall(Vec2 pos) {
Ripple* r = findFreeRipple();
if (r == nullptr) {
return;
} }
r->center = pos;
r->age_s = 0.0F;
r->lifetime_s = Defaults::Playfield::Ripple::SMALL_LIFETIME_S;
r->speed_px_s = Defaults::Playfield::Ripple::SMALL_SPEED_PX_S;
r->amplitude_px = Defaults::Playfield::Ripple::SMALL_AMPLITUDE_PX;
r->thickness_px = Defaults::Playfield::Ripple::SMALL_THICKNESS_PX;
r->active = true;
}
void Playfield::notifyExplosion(Vec2 pos) {
spawnBig(pos);
}
void Playfield::notifyShipMoving(std::uint8_t player_id, Vec2 pos, float speed_px_s, float delta_time) {
if (player_id >= ship_ripple_cooldown_.size()) {
return;
}
if (speed_px_s < Defaults::Playfield::Ripple::SHIP_SPEED_THRESHOLD_PX_S) {
ship_ripple_cooldown_[player_id] = 0.0F;
return;
}
ship_ripple_cooldown_[player_id] -= delta_time;
if (ship_ripple_cooldown_[player_id] > 0.0F) {
return;
}
spawnSmall(pos);
const float JITTER = randUniform(
-Defaults::Playfield::Ripple::SHIP_COOLDOWN_JITTER_S,
Defaults::Playfield::Ripple::SHIP_COOLDOWN_JITTER_S);
ship_ripple_cooldown_[player_id] =
Defaults::Playfield::Ripple::SHIP_COOLDOWN_S + JITTER;
} }
void Playfield::buildLines() { void Playfield::buildLines() {
const SDL_FRect& zona = Defaults::Zones::PLAYAREA; const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
const float CELL_W = zona.w / static_cast<float>(Defaults::Playfield::COLUMNS); const float CELL_W = zone.w / static_cast<float>(Defaults::Playfield::COLUMNS);
const float CELL_H = zona.h / static_cast<float>(Defaults::Playfield::ROWS); const float CELL_H = zone.h / static_cast<float>(Defaults::Playfield::ROWS);
const float SUB_W = CELL_W / static_cast<float>(Defaults::Playfield::SUBDIVISIONS); const float SUB_W = CELL_W / static_cast<float>(Defaults::Playfield::SUBDIVISIONS);
const float SUB_H = CELL_H / static_cast<float>(Defaults::Playfield::SUBDIVISIONS); const float SUB_H = CELL_H / static_cast<float>(Defaults::Playfield::SUBDIVISIONS);
const int SUB_VERTS = Defaults::Playfield::COLUMNS * Defaults::Playfield::SUBDIVISIONS; const int SUB_VERTS = Defaults::Playfield::COLUMNS * Defaults::Playfield::SUBDIVISIONS;
@@ -151,38 +154,32 @@ namespace Graphics {
// Verticals: posicions i ∈ [1, SUB_VERTS-1]. // Verticals: posicions i ∈ [1, SUB_VERTS-1].
for (int i = 1; i < SUB_VERTS; i++) { for (int i = 1; i < SUB_VERTS; i++) {
const float X = zona.x + (static_cast<float>(i) * SUB_W); const float X = zone.x + (static_cast<float>(i) * SUB_W);
const bool IS_MAIN = (i % Defaults::Playfield::SUBDIVISIONS) == 0; const bool IS_MAIN = (i % Defaults::Playfield::SUBDIVISIONS) == 0;
const float BRIGHTNESS = IS_MAIN const float BRIGHTNESS = IS_MAIN
? Defaults::Playfield::GRID_BRIGHTNESS ? Defaults::Playfield::GRID_BRIGHTNESS
: Defaults::Playfield::SUBGRID_BRIGHTNESS; : Defaults::Playfield::SUBGRID_BRIGHTNESS;
verticals.push_back(Line{ verticals.push_back(Line{
.start = {.x = X, .y = zona.y}, .start = {.x = X, .y = zone.y},
.end = {.x = X, .y = zona.y + zona.h}, .end = {.x = X, .y = zone.y + zone.h},
.brightness = BRIGHTNESS, .brightness = BRIGHTNESS,
.spawn_time_s = 0.0F, .spawn_time_s = 0.0F,
.is_vertical = true, .is_vertical = true});
.orbit_amplitude = 0.0F,
.orbit_phase = 0.0F,
.pulses = {}});
} }
// Horitzontals: posicions j ∈ [1, SUB_HORIZ-1]. // Horitzontals: posicions j ∈ [1, SUB_HORIZ-1].
for (int j = 1; j < SUB_HORIZ; j++) { for (int j = 1; j < SUB_HORIZ; j++) {
const float Y = zona.y + (static_cast<float>(j) * SUB_H); const float Y = zone.y + (static_cast<float>(j) * SUB_H);
const bool IS_MAIN = (j % Defaults::Playfield::SUBDIVISIONS) == 0; const bool IS_MAIN = (j % Defaults::Playfield::SUBDIVISIONS) == 0;
const float BRIGHTNESS = IS_MAIN const float BRIGHTNESS = IS_MAIN
? Defaults::Playfield::GRID_BRIGHTNESS ? Defaults::Playfield::GRID_BRIGHTNESS
: Defaults::Playfield::SUBGRID_BRIGHTNESS; : Defaults::Playfield::SUBGRID_BRIGHTNESS;
horizontals.push_back(Line{ horizontals.push_back(Line{
.start = {.x = zona.x, .y = Y}, .start = {.x = zone.x, .y = Y},
.end = {.x = zona.x + zona.w, .y = Y}, .end = {.x = zone.x + zone.w, .y = Y},
.brightness = BRIGHTNESS, .brightness = BRIGHTNESS,
.spawn_time_s = 0.0F, .spawn_time_s = 0.0F,
.is_vertical = false, .is_vertical = false});
.orbit_amplitude = 0.0F,
.orbit_phase = 0.0F,
.pulses = {}});
} }
// Ona diagonal: la línia esquerra/superior naix a t=0 i les següents // Ona diagonal: la línia esquerra/superior naix a t=0 i les següents
@@ -199,13 +196,39 @@ namespace Graphics {
lines_.clear(); lines_.clear();
lines_.reserve(verticals.size() + horizontals.size()); lines_.reserve(verticals.size() + horizontals.size());
// El spawn_time_s s'assigna per índex espacial perquè la diagonal de
// l'ona de creixement avanci uniformement. L'ordre dins lines_, en
// canvi, ha de garantir que el grid principal (més brillant) es
// dibuixi DESPRÉS del subgrid: així a les interseccions guanya el
// principal i no queden tallades pel subgrid.
for (int i = 0; i < NUM_V; i++) { for (int i = 0; i < NUM_V; i++) {
verticals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_V; verticals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_V;
lines_.push_back(verticals[i]);
} }
for (int i = 0; i < NUM_H; i++) { for (int i = 0; i < NUM_H; i++) {
horizontals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_H; horizontals[i].spawn_time_s = static_cast<float>(i) * INTERVAL_H;
lines_.push_back(horizontals[i]); }
// Passada 1: subgrid (verticals + horitzontals).
for (const auto& v : verticals) {
if (v.brightness < Defaults::Playfield::GRID_BRIGHTNESS) {
lines_.push_back(v);
}
}
for (const auto& h : horizontals) {
if (h.brightness < Defaults::Playfield::GRID_BRIGHTNESS) {
lines_.push_back(h);
}
}
// Passada 2: grid principal (verticals + horitzontals).
for (const auto& v : verticals) {
if (v.brightness >= Defaults::Playfield::GRID_BRIGHTNESS) {
lines_.push_back(v);
}
}
for (const auto& h : horizontals) {
if (h.brightness >= Defaults::Playfield::GRID_BRIGHTNESS) {
lines_.push_back(h);
}
} }
} }
@@ -215,90 +238,106 @@ namespace Graphics {
} }
void Playfield::draw() const { void Playfield::draw() const {
// Recollir ripples actives (punters per accés ràpid al hot loop).
std::array<const Ripple*, Defaults::Playfield::Ripple::POOL_SIZE> active{};
int n_active = 0;
for (const auto& ripple : ripples_) {
if (ripple.active) {
active[n_active++] = &ripple;
}
}
for (const auto& line : lines_) { for (const auto& line : lines_) {
const float RAW_P = computeLineProgress(line); drawLine(line, active.data(), n_active);
if (RAW_P <= 0.0F) { }
}
void Playfield::drawLine(const Line& line, const Ripple* const* active, int n_active) const {
const float RAW_P = computeLineProgress(line);
if (RAW_P <= 0.0F) {
return;
}
const float P = easeOutCubic(RAW_P);
const float START_X = line.start.x;
const float START_Y = line.start.y;
const float DX = line.end.x - line.start.x;
const float DY = line.end.y - line.start.y;
const float END_X = START_X + (DX * P);
const float END_Y = START_Y + (DY * P);
// AABB de la porció visible de la línia + filtre de ripples.
const float LINE_MIN_X = std::min(START_X, END_X);
const float LINE_MAX_X = std::max(START_X, END_X);
const float LINE_MIN_Y = std::min(START_Y, END_Y);
const float LINE_MAX_Y = std::max(START_Y, END_Y);
std::array<const Ripple*, Defaults::Playfield::Ripple::POOL_SIZE> hits{};
int n_hits = 0;
for (int i = 0; i < n_active; i++) {
const auto& r = *active[i];
const float R_MAX = (r.age_s * r.speed_px_s) + r.thickness_px;
if ((r.center.x + R_MAX) < LINE_MIN_X || (r.center.x - R_MAX) > LINE_MAX_X ||
(r.center.y + R_MAX) < LINE_MIN_Y || (r.center.y - R_MAX) > LINE_MAX_Y) {
continue; continue;
} }
const float P = easeOutCubic(RAW_P); hits[n_hits++] = &r;
}
// Desplaçament perpendicular per orbit (verticals → x, horitzontals → y). if (n_hits == 0) {
const float ORBIT_OFFSET = line.orbit_amplitude * std::sin(line.orbit_phase); // Camí ràpid: una sola crida com abans.
const float ORBIT_DX = line.is_vertical ? ORBIT_OFFSET : 0.0F;
const float ORBIT_DY = line.is_vertical ? 0.0F : ORBIT_OFFSET;
const float START_X = line.start.x + ORBIT_DX;
const float START_Y = line.start.y + ORBIT_DY;
const float DX = line.end.x - line.start.x;
const float DY = line.end.y - line.start.y;
const float CURRENT_X = START_X + (DX * P);
const float CURRENT_Y = START_Y + (DY * P);
// Tram base (brillo de la línia).
Rendering::linea( Rendering::linea(
renderer_, renderer_,
static_cast<int>(START_X), static_cast<int>(START_X),
static_cast<int>(START_Y), static_cast<int>(START_Y),
static_cast<int>(CURRENT_X), static_cast<int>(END_X),
static_cast<int>(CURRENT_Y), static_cast<int>(END_Y),
line.brightness); line.brightness,
0.0F,
// Cap brillant mentre creix: l'últim tram de la línia es repinta més brillant. Defaults::Playfield::GRID_COLOR);
// Cap brillant mentre creix.
if (P < 1.0F) { if (P < 1.0F) {
const float LENGTH = std::sqrt((DX * DX) + (DY * DY)); const float LENGTH = std::sqrt((DX * DX) + (DY * DY));
if (LENGTH > 0.0F) { if (LENGTH > 0.0F) {
const float HEAD_T = std::max(0.0F, P - (Defaults::Playfield::HEAD_LENGTH_PX / LENGTH)); const float HEAD_T = std::max(0.0F, P - (Defaults::Playfield::HEAD_LENGTH_PX / LENGTH));
const float HEAD_X = START_X + (DX * HEAD_T);
const float HEAD_Y = START_Y + (DY * HEAD_T);
Rendering::linea( Rendering::linea(
renderer_, renderer_,
static_cast<int>(HEAD_X), static_cast<int>(START_X + (DX * HEAD_T)),
static_cast<int>(HEAD_Y), static_cast<int>(START_Y + (DY * HEAD_T)),
static_cast<int>(CURRENT_X), static_cast<int>(END_X),
static_cast<int>(CURRENT_Y), static_cast<int>(END_Y),
Defaults::Playfield::HEAD_BRIGHTNESS); Defaults::Playfield::HEAD_BRIGHTNESS,
0.0F,
Defaults::Playfield::GRID_COLOR);
} }
} }
return;
}
// Pulses: cada un és un segment brillant centrat a center_t que // Camí deformat: subdividir en N segments i desplaçar cada vèrtex.
// s'expandeix amb el temps i s'apaga. const bool IS_MAIN = line.brightness >= Defaults::Playfield::GRID_BRIGHTNESS;
const float LINE_LENGTH = std::sqrt((DX * DX) + (DY * DY)); const int N = IS_MAIN
if (LINE_LENGTH <= 0.0F) { ? Defaults::Playfield::Ripple::MAIN_SEGMENTS
continue; : Defaults::Playfield::Ripple::SUB_SEGMENTS;
} const Vec2 D0 = computeRippleDisplacement(START_X, START_Y, hits.data(), n_hits);
const SDL_Color PULSE_TARGET = { float prev_x = START_X + D0.x;
.r = Defaults::Playfield::PULSE_COLOR_R, float prev_y = START_Y + D0.y;
.g = Defaults::Playfield::PULSE_COLOR_G, for (int i = 1; i <= N; i++) {
.b = Defaults::Playfield::PULSE_COLOR_B, const float T = static_cast<float>(i) / static_cast<float>(N);
.a = 255}; const float X = START_X + (DX * P * T);
for (const auto& pulse : line.pulses) { const float Y = START_Y + (DY * P * T);
if (!pulse.active) { const Vec2 D = computeRippleDisplacement(X, Y, hits.data(), n_hits);
continue; const float NX = X + D.x;
} const float NY = Y + D.y;
const float HALF_WIDTH_T = (pulse.age_s * Defaults::Playfield::PULSE_SPREAD_PER_S) / LINE_LENGTH; Rendering::linea(
const float INTENSITY = std::max( renderer_,
0.0F, static_cast<int>(prev_x),
1.0F - (pulse.age_s / Defaults::Playfield::PULSE_LIFETIME_S)); static_cast<int>(prev_y),
const float T1 = std::clamp(pulse.center_t - HALF_WIDTH_T, 0.0F, 1.0F); static_cast<int>(NX),
const float T2 = std::clamp(pulse.center_t + HALF_WIDTH_T, 0.0F, 1.0F); static_cast<int>(NY),
if (T2 <= T1) { line.brightness,
continue; 0.0F,
} Defaults::Playfield::GRID_COLOR);
const float P1_X = START_X + (DX * T1); prev_x = NX;
const float P1_Y = START_Y + (DY * T1); prev_y = NY;
const float P2_X = START_X + (DX * T2);
const float P2_Y = START_Y + (DY * T2);
const SDL_Color SEG_COLOR = lerpColor(PULSE_TARGET, INTENSITY);
Rendering::linea(
renderer_,
static_cast<int>(P1_X),
static_cast<int>(P1_Y),
static_cast<int>(P2_X),
static_cast<int>(P2_Y),
1.0F,
0.0F,
SEG_COLOR);
}
} }
} }
+34 -24
View File
@@ -5,13 +5,16 @@
// rep un `creation_progress` global ∈ [0, 1] i cada línia computa quina porció // rep un `creation_progress` global ∈ [0, 1] i cada línia computa quina porció
// li toca dibuixar segons el seu slot a la timeline. // li toca dibuixar segons el seu slot a la timeline.
// //
// Disseny preparat per a futures capacitats: // Reaccions disponibles:
// - Línies "vives" que reaccionen a explosions / pas de la nau (reaction_intensity). // - Ripples: deformacions circulars (ones d'aigua) que travessen la graella.
// - Capes addicionals al fons (estrelles, gradients, scanlines). // Disparades per explosions (grans) i pas de la nau (petites, cadència estil
// trail). Cada vèrtex d'una línia afectada es desplaça radialment cap a fora
// amb una envoltant en cos(·) que decau a les vores de l'anell i amb el temps.
#pragma once #pragma once
#include <array> #include <array>
#include <cstdint>
#include <vector> #include <vector>
#include "core/defaults/playfield.hpp" #include "core/defaults/playfield.hpp"
@@ -24,44 +27,51 @@ namespace Graphics {
public: public:
explicit Playfield(Rendering::Renderer* renderer); explicit Playfield(Rendering::Renderer* renderer);
// Avança timers interns (creació + reaccions). // Avança timers interns (creació + ripples).
void update(float delta_time); void update(float delta_time);
// Pinta la graella. La porció dibuixada de cada línia depèn del timer intern. // Pinta la graella. La porció dibuixada de cada línia depèn del timer intern,
// i s'aplica deformació radial per cada ripple activa que afecti la línia.
void draw() const; void draw() const;
// Notifica que una nau ha passat per (pos) a velocitat (speed_px_s). // Notifica que una nau ha passat per (pos) a (speed_px_s). Genera ones
// Si està prop d'alguna línia i va prou ràpida, la línia entra en orbit. // petites darrere la nau a cadència regular amb jitter (estil TrailManager).
void notifyShipPass(Vec2 pos, float speed_px_s); void notifyShipMoving(std::uint8_t player_id, Vec2 pos, float speed_px_s, float delta_time);
// Notifica el spawn d'un firework a (pos). Les línies V i H més properes // Notifica una explosió a (pos): genera una ripple gran centrada al punt.
// generen un pulse brillant que es propaga. void notifyExplosion(Vec2 pos);
void notifyFireworkSpawn(Vec2 pos);
private: // Pública per accés des d'helpers a l'anonymous namespace del .cpp.
struct Pulse { struct Ripple {
bool active{false}; Vec2 center{};
float center_t{0.5F}; // posició al llarg de la línia (0..1)
float age_s{0.0F}; float age_s{0.0F};
float lifetime_s{0.0F};
float speed_px_s{0.0F};
float amplitude_px{0.0F};
float thickness_px{0.0F};
bool active{false};
}; };
private:
struct Line { struct Line {
Vec2 start; // top (verticals) o left (horitzontals) Vec2 start; // top (verticals) o left (horitzontals)
Vec2 end; // bottom (verticals) o right (horitzontals) Vec2 end; // bottom (verticals) o right (horitzontals)
float brightness; // base (GRID_BRIGHTNESS o SUBGRID_BRIGHTNESS) float brightness; // base (GRID_BRIGHTNESS o SUBGRID_BRIGHTNESS)
float spawn_time_s; // moment de naixement float spawn_time_s; // moment de naixement
bool is_vertical; // direcció (per saber el perpendicular de l'orbit) bool is_vertical; // direcció
float orbit_amplitude; // amplitud actual de l'orbit (px, ≥ 0)
float orbit_phase; // fase del sin (avança contínuament)
std::array<Pulse, Defaults::Playfield::MAX_PULSES_PER_LINE> pulses;
}; };
void buildLines(); void buildLines();
void drawLine(const Line& line, const Ripple* const* active, int n_active) const;
[[nodiscard]] auto computeLineProgress(const Line& line) const -> float; [[nodiscard]] auto computeLineProgress(const Line& line) const -> float;
static void spawnPulseAt(Line& line, float center_t); void spawnBig(Vec2 pos);
void spawnSmall(Vec2 pos);
auto findFreeRipple() -> Ripple*;
Rendering::Renderer* renderer_; Rendering::Renderer* renderer_;
std::vector<Line> lines_; std::vector<Line> lines_;
std::array<Ripple, Defaults::Playfield::Ripple::POOL_SIZE> ripples_{};
std::array<float, 2> ship_ripple_cooldown_{};
float elapsed_s_{0.0F}; float elapsed_s_{0.0F};
}; };
+138 -123
View File
@@ -4,156 +4,171 @@
#include "core/graphics/shape.hpp" #include "core/graphics/shape.hpp"
#include <algorithm> #include <algorithm>
#include <cmath>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <sstream> #include <sstream>
namespace Graphics { namespace Graphics {
Shape::Shape(const std::string& filepath) Shape::Shape(const std::string& filepath)
: center_({.x = 0.0F, .y = 0.0F}), : center_({.x = 0.0F, .y = 0.0F}),
nom_("unnamed") { nom_("unnamed") {
load(filepath); load(filepath);
}
auto Shape::load(const std::string& filepath) -> bool {
// Llegir file
std::ifstream file(filepath);
if (!file.is_open()) {
std::cerr << "[Shape] Error: no es pot obrir " << filepath << '\n';
return false;
} }
// Llegir todo el contingut auto Shape::load(const std::string& filepath) -> bool {
std::stringstream buffer; // Llegir file
buffer << file.rdbuf(); std::ifstream file(filepath);
std::string contingut = buffer.str(); if (!file.is_open()) {
file.close(); std::cerr << "[Shape] Error: no es pot obrir " << filepath << '\n';
return false;
// Parsejar
return parseFile(contingut);
}
auto Shape::parseFile(const std::string& contingut) -> bool {
std::istringstream iss(contingut);
std::string line;
while (std::getline(iss, line)) {
// Trim whitespace
line = trim(line);
// Skip comments and blanks
if (line.empty() || line[0] == '#') {
continue;
} }
// Parse command // Llegir todo el contingut
if (startsWith(line, "name:")) { std::stringstream buffer;
nom_ = trim(extractValue(line)); buffer << file.rdbuf();
} else if (startsWith(line, "scale:")) { std::string contingut = buffer.str();
try { file.close();
escala_defecte_ = std::stof(extractValue(line));
} catch (...) { // Parsejar
std::cerr << "[Shape] Warning: scale invàlida, usant 1.0" << '\n'; return parseFile(contingut);
escala_defecte_ = 1.0F; }
auto Shape::parseFile(const std::string& contingut) -> bool {
std::istringstream iss(contingut);
std::string line;
while (std::getline(iss, line)) {
// Trim whitespace
line = trim(line);
// Skip comments and blanks
if (line.empty() || line[0] == '#') {
continue;
} }
} else if (startsWith(line, "center:")) {
parseCenter(extractValue(line)); // Parse command
} else if (startsWith(line, "polyline:")) { if (startsWith(line, "name:")) {
auto points = parsePoints(extractValue(line)); nom_ = trim(extractValue(line));
if (points.size() >= 2) { } else if (startsWith(line, "scale:")) {
primitives_.push_back({PrimitiveType::POLYLINE, points}); try {
} else { escala_defecte_ = std::stof(extractValue(line));
std::cerr << "[Shape] Warning: polyline con menys de 2 points ignorada" } catch (...) {
<< '\n'; std::cerr << "[Shape] Warning: scale invàlida, usant 1.0" << '\n';
escala_defecte_ = 1.0F;
}
} else if (startsWith(line, "center:")) {
parseCenter(extractValue(line));
} else if (startsWith(line, "polyline:")) {
auto points = parsePoints(extractValue(line));
if (points.size() >= 2) {
primitives_.push_back({PrimitiveType::POLYLINE, points});
} else {
std::cerr << "[Shape] Warning: polyline con menys de 2 points ignorada"
<< '\n';
}
} else if (startsWith(line, "line:")) {
auto points = parsePoints(extractValue(line));
if (points.size() == 2) {
primitives_.push_back({PrimitiveType::LINE, points});
} else {
std::cerr << "[Shape] Warning: line ha de tenir exactament 2 points"
<< '\n';
}
} }
} else if (startsWith(line, "line:")) { // Comandes desconegudes ignorades silenciosament
auto points = parsePoints(extractValue(line)); }
if (points.size() == 2) {
primitives_.push_back({PrimitiveType::LINE, points}); if (primitives_.empty()) {
} else { std::cerr << "[Shape] Error: sin primitiva carregada" << '\n';
std::cerr << "[Shape] Warning: line ha de tenir exactament 2 points" return false;
<< '\n'; }
bounding_radius_ = computeBoundingRadius(primitives_, center_);
return true;
}
auto Shape::computeBoundingRadius(const std::vector<ShapePrimitive>& primitives,
const Vec2& center) -> float {
float max_dist_sq = 0.0F;
for (const auto& prim : primitives) {
for (const auto& p : prim.points) {
const float DX = p.x - center.x;
const float DY = p.y - center.y;
max_dist_sq = std::max(max_dist_sq, (DX * DX) + (DY * DY));
} }
} }
// Comandes desconegudes ignorades silenciosament return std::sqrt(max_dist_sq);
} }
if (primitives_.empty()) { // Helper: trim whitespace
std::cerr << "[Shape] Error: sin primitiva carregada" << '\n'; auto Shape::trim(const std::string& str) -> std::string {
return false; const char* whitespace = " \t\n\r";
} size_t start = str.find_first_not_of(whitespace);
if (start == std::string::npos) {
return true; return "";
}
// Helper: trim whitespace
auto Shape::trim(const std::string& str) -> std::string {
const char* whitespace = " \t\n\r";
size_t start = str.find_first_not_of(whitespace);
if (start == std::string::npos) {
return "";
}
size_t end = str.find_last_not_of(whitespace);
return str.substr(start, end - start + 1);
}
// Helper: startsWith
auto Shape::startsWith(const std::string& str,
const std::string& prefix) -> bool {
if (str.length() < prefix.length()) {
return false;
}
return str.starts_with(prefix);
}
// Helper: extract value after ':'
auto Shape::extractValue(const std::string& line) -> std::string {
size_t colon = line.find(':');
if (colon == std::string::npos) {
return "";
}
return line.substr(colon + 1);
}
// Helper: parse center "x, y"
void Shape::parseCenter(const std::string& value) {
std::string val = trim(value);
size_t comma = val.find(',');
if (comma != std::string::npos) {
try {
center_.x = std::stof(trim(val.substr(0, comma)));
center_.y = std::stof(trim(val.substr(comma + 1)));
} catch (...) {
std::cerr << "[Shape] Warning: centro invàlid, usant (0,0)" << '\n';
center_ = {.x = 0.0F, .y = 0.0F};
} }
size_t end = str.find_last_not_of(whitespace);
return str.substr(start, end - start + 1);
} }
}
// Helper: parse points "x1,y1 x2,y2 x3,y3" // Helper: startsWith
auto Shape::parsePoints(const std::string& str) -> std::vector<Vec2> { auto Shape::startsWith(const std::string& str,
std::vector<Vec2> points; const std::string& prefix) -> bool {
std::istringstream iss(trim(str)); if (str.length() < prefix.length()) {
std::string pair; return false;
}
return str.starts_with(prefix);
}
while (iss >> pair) { // Whitespace-separated // Helper: extract value after ':'
size_t comma = pair.find(','); auto Shape::extractValue(const std::string& line) -> std::string {
size_t colon = line.find(':');
if (colon == std::string::npos) {
return "";
}
return line.substr(colon + 1);
}
// Helper: parse center "x, y"
void Shape::parseCenter(const std::string& value) {
std::string val = trim(value);
size_t comma = val.find(',');
if (comma != std::string::npos) { if (comma != std::string::npos) {
try { try {
float x = std::stof(pair.substr(0, comma)); center_.x = std::stof(trim(val.substr(0, comma)));
float y = std::stof(pair.substr(comma + 1)); center_.y = std::stof(trim(val.substr(comma + 1)));
points.push_back({x, y});
} catch (...) { } catch (...) {
std::cerr << "[Shape] Warning: point invàlid ignorat: " << pair std::cerr << "[Shape] Warning: centro invàlid, usant (0,0)" << '\n';
<< '\n'; center_ = {.x = 0.0F, .y = 0.0F};
} }
} }
} }
return points; // Helper: parse points "x1,y1 x2,y2 x3,y3"
} auto Shape::parsePoints(const std::string& str) -> std::vector<Vec2> {
std::vector<Vec2> points;
std::istringstream iss(trim(str));
std::string pair;
while (iss >> pair) { // Whitespace-separated
size_t comma = pair.find(',');
if (comma != std::string::npos) {
try {
float x = std::stof(pair.substr(0, comma));
float y = std::stof(pair.substr(comma + 1));
points.push_back({x, y});
} catch (...) {
std::cerr << "[Shape] Warning: point invàlid ignorat: " << pair
<< '\n';
}
}
}
return points;
}
} // namespace Graphics } // namespace Graphics
+24 -17
View File
@@ -11,21 +11,21 @@
namespace Graphics { namespace Graphics {
// Tipo de primitiva dins de una shape // Tipo de primitiva dins de una shape
enum class PrimitiveType : std::uint8_t { enum class PrimitiveType : std::uint8_t {
POLYLINE, // Secuencia de points connectats POLYLINE, // Secuencia de points connectats
LINE // Línia individual (2 points) LINE // Línia individual (2 points)
}; };
// Primitiva individual (polyline o line) // Primitiva individual (polyline o line)
struct ShapePrimitive { struct ShapePrimitive {
PrimitiveType type; PrimitiveType type;
std::vector<Vec2> points; // 2+ points per polyline, exactament 2 per line std::vector<Vec2> points; // 2+ points per polyline, exactament 2 per line
}; };
// Clase Shape - representa una shape vectorial carregada desde .shp // Clase Shape - representa una shape vectorial carregada desde .shp
class Shape { class Shape {
public: public:
// Constructors // Constructors
Shape() = default; Shape() = default;
explicit Shape(const std::string& filepath); explicit Shape(const std::string& filepath);
@@ -42,18 +42,22 @@ class Shape {
} }
[[nodiscard]] auto getCenter() const -> const Vec2& { return center_; } [[nodiscard]] auto getCenter() const -> const Vec2& { return center_; }
[[nodiscard]] auto getDefaultScale() const -> float { return escala_defecte_; } [[nodiscard]] auto getDefaultScale() const -> float { return escala_defecte_; }
// Distància màx. del center_ al vèrtex més llunyà; ús: dimensionar
// efectes proporcionals a la mida de la shape (halos, glow).
[[nodiscard]] auto getBoundingRadius() const -> float { return bounding_radius_; }
[[nodiscard]] auto isValid() const -> bool { return !primitives_.empty(); } [[nodiscard]] auto isValid() const -> bool { return !primitives_.empty(); }
// Info de depuració // Info de depuració
[[nodiscard]] auto getName() const -> const std::string& { return nom_; } [[nodiscard]] auto getName() const -> const std::string& { return nom_; }
[[nodiscard]] auto getNumPrimitives() const -> size_t { return primitives_.size(); } [[nodiscard]] auto getNumPrimitives() const -> size_t { return primitives_.size(); }
private: private:
std::vector<ShapePrimitive> primitives_; std::vector<ShapePrimitive> primitives_;
Vec2 center_; // Centro/origin de la shape Vec2 center_; // Centro/origin de la shape
float escala_defecte_{1.0F}; // Escala per defecte (normalment 1.0). Inicializada para float escala_defecte_{1.0F}; // Escala per defecte (normalment 1.0). Inicializada para
// que el ctor por defecto no deje el campo indeterminado. // que el ctor por defecto no deje el campo indeterminado.
std::string nom_; // Nom de la shape (per depuració) float bounding_radius_{0.0F}; // Distància màx. del center_ al vèrtex més llunyà.
std::string nom_; // Nom de la shape (per depuració)
// Helpers privats per parsejar. Son estáticos: no necesitan estado // Helpers privats per parsejar. Son estáticos: no necesitan estado
// de instancia, trabajan sobre el string pasado por parámetro. // de instancia, trabajan sobre el string pasado por parámetro.
@@ -62,6 +66,9 @@ class Shape {
[[nodiscard]] static auto extractValue(const std::string& line) -> std::string; [[nodiscard]] static auto extractValue(const std::string& line) -> std::string;
void parseCenter(const std::string& value); void parseCenter(const std::string& value);
[[nodiscard]] static auto parsePoints(const std::string& str) -> std::vector<Vec2>; [[nodiscard]] static auto parsePoints(const std::string& str) -> std::vector<Vec2>;
}; [[nodiscard]] static auto computeBoundingRadius(
const std::vector<ShapePrimitive>& primitives,
const Vec2& center) -> float;
};
} // namespace Graphics } // namespace Graphics
+78 -137
View File
@@ -1,168 +1,109 @@
// starfield.cpp - Implementació del sistema de estrelles de fons // starfield.cpp - Implementació del starfield 3D
// © 2026 JailDesigner // © 2026 JailDesigner
#include "core/graphics/starfield.hpp" #include "core/graphics/starfield.hpp"
#include <algorithm>
#include <cmath> #include <cmath>
#include <cstdlib> #include <cstdlib>
#include <iostream>
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/rendering/shape_renderer.hpp"
namespace Graphics { namespace Graphics {
// Constructor namespace {
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;
}
// Configurar 3 capes con diferents velocitats i escales // Helper: número aleatori en [0, 1) usant rand()/RAND_MAX (mateixa convenció
// Capa 0: Fons llunyà (lenta, pequeña) // que la resta del joc — veure starfield.cpp).
capes_.push_back({20.0F, 0.3F, 0.8F, densitat / 3}); auto randFloat01() -> float {
return static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
}
// Capa 1: Profunditat mitjana auto randRange(float lo, float hi) -> float {
capes_.push_back({40.0F, 0.5F, 1.2F, densitat / 3}); return lo + (randFloat01() * (hi - lo));
}
// Capa 2: Primer pla (ràpida, grande) } // namespace
capes_.push_back({80.0F, 0.8F, 2.0F, densitat / 3});
// Calcular radi màxim (distancia del centro al racó més llunyà) Starfield::Starfield(Rendering::Renderer* renderer, const Camera3D* camera, int density)
float dx = std::max(punt_fuga_.x, area_.w - punt_fuga_.x); : renderer_(renderer),
float dy = std::max(punt_fuga_.y, area_.h - punt_fuga_.y); camera_(camera),
radi_max_ = std::sqrt((dx * dx) + (dy * dy)); octahedron_(makeOctahedron()) {
stars_.resize(static_cast<std::size_t>(std::max(0, density)));
// Inicialitzar estrelles con posicions distribuïdes (pre-omplir pantalla) for (auto& star : stars_) {
for (int capa_idx = 0; capa_idx < 3; capa_idx++) { // Pre-omplir amb estrelles distribuïdes per tot el rang Z, no només al far.
int num = capes_[capa_idx].num_estrelles; initStar(star, /*spawn_at_far=*/false);
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);
} }
} }
}
// Inicialitzar una estrella (nueva o regenerada) void Starfield::initStar(Star& star, bool spawn_at_far) {
void Starfield::initStar(Estrella& estrella) const { star.position.x = randRange(-HALF_SPAWN_X, HALF_SPAWN_X);
// Angle aleatori des del point de fuga hacia fuera star.position.y = randRange(-HALF_SPAWN_Y, HALF_SPAWN_Y);
estrella.angle = (static_cast<float>(rand()) / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI; 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 star.velocity_z = -randRange(MIN_VELOCITY_Z, MAX_VELOCITY_Z);
estrella.distancia_centre = 0.05F; 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 auto Starfield::computeBrightness(const Star& star) const -> float {
float radi = estrella.distancia_centre * radi_max_; // Lerp segons distància Z normalitzada [Z_NEAR_RESPAWN .. Z_FAR_SPAWN].
estrella.position.x = punt_fuga_.x + (radi * std::cos(estrella.angle)); const float SPAN = Z_FAR_SPAWN - Z_NEAR_RESPAWN;
estrella.position.y = punt_fuga_.y + (radi * std::sin(estrella.angle)); 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 void Starfield::update(float delta_time) {
auto Starfield::isOutsideArea(const Estrella& estrella) const -> bool { for (auto& star : stars_) {
return (estrella.position.x < area_.x || star.position.z += star.velocity_z * delta_time;
estrella.position.x > area_.x + area_.w || star.rot_phase_y += star.rot_speed_y * delta_time;
estrella.position.y < area_.y || star.rot_phase_x += star.rot_speed_x * delta_time;
estrella.position.y > area_.y + area_.h);
}
// Calcular scale dinàmica segons distancia del centro if (star.position.z < Z_NEAR_RESPAWN) {
auto Starfield::computeScale(const Estrella& estrella) const -> float { initStar(star, /*spawn_at_far=*/true);
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);
} }
} }
}
// Establir multiplicador de brightness void Starfield::draw() const {
void Starfield::setBrightness(float multiplier) { if (camera_ == nullptr || renderer_ == nullptr) {
multiplicador_brightness_ = std::max(0.0F, multiplier); // Evitar valors negatius 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 for (std::size_t idx : order) {
void Starfield::draw() { const Star& star = stars_[idx];
if (!shape_estrella_->isValid()) { const Transform3D TRANSFORM{
return; .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), color_);
}
} }
for (const auto& estrella : estrelles_) { void Starfield::setBrightness(float multiplier) {
// Calcular scale i brightness dinàmicament brightness_mult_ = std::max(0.0F, multiplier);
float scale = computeScale(estrella); }
float brightness = computeBrightness(estrella);
void Starfield::setColor(SDL_Color color) {
// Renderizar estrella sin rotación color_ = color;
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
);
} }
}
} // namespace Graphics } // namespace Graphics
+48 -59
View File
@@ -1,83 +1,72 @@
// 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 // © 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 #pragma once
#include "core/rendering/render_context.hpp"
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <memory>
#include <vector> #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" #include "core/types.hpp"
namespace Graphics { namespace Graphics {
// Configuración per cada capa de profunditat class Starfield {
struct CapaConfig { public:
float velocitat_base; // Velocidad base de esta capa (px/s) Starfield(Rendering::Renderer* renderer, const Camera3D* camera, int density = 200);
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
};
// 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 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); void setBrightness(float multiplier);
void setColor(SDL_Color color);
private: private:
// Estructura interna per cada estrella struct Star {
struct Estrella { Vec3 position{};
Vec2 position; // Posición actual float velocity_z{0.0F}; // Negatiu: cap a càmera
float angle; // Angle de movement (radians) float rot_phase_y{0.0F};
float distancia_centre; // Distancia normalitzada del centro (0.0-1.0) float rot_phase_x{0.0F};
int capa; // Índex de capa (0=lluny, 1=mitjà, 2=prop) float rot_speed_y{0.0F};
float rot_speed_x{0.0F};
float scale{1.0F};
}; };
// Inicialitzar una estrella (nueva o regenerada) static void initStar(Star& star, bool spawn_at_far);
void initStar(Estrella& estrella) const; [[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_; Rendering::Renderer* renderer_;
const Camera3D* camera_;
std::vector<Star> stars_;
Mesh3D octahedron_;
float brightness_mult_{1.0F};
SDL_Color color_{.r = 0, .g = 0, .b = 0, .a = 0}; // alpha=0 → usa color global
// Configuración // Volum de spawn / regeneració en l'espai 3D.
Vec2 punt_fuga_; // Vec2 de origin de las estrelles static constexpr float Z_NEAR_RESPAWN = 5.0F; // Si Z < aquest valor → regenera
SDL_FRect area_; // Àrea activa static constexpr float Z_FAR_SPAWN = 1500.0F; // Z de regeneració (lluny — més profunditat)
float radi_max_; // Distancia màxima del centro al límit de pantalla static constexpr float HALF_SPAWN_X = 900.0F; // X aleatori dins [-, +]
float multiplicador_brightness_{1.0F}; // Multiplicador de brightness (1.0 = default) 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 } // namespace Graphics
-105
View File
@@ -1,105 +0,0 @@
// starfield3d.cpp - Implementació del starfield 3D
// © 2026 JailDesigner
#include "core/graphics/starfield3d.hpp"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include "core/defaults.hpp"
namespace Graphics {
namespace {
// Helper: número aleatori en [0, 1) usant rand()/RAND_MAX (mateixa convenció
// que la resta del joc — veure starfield.cpp).
auto randFloat01() -> float {
return static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
}
auto randRange(float lo, float hi) -> float {
return lo + (randFloat01() * (hi - lo));
}
} // namespace
Starfield3D::Starfield3D(Rendering::Renderer* renderer, const Camera3D* camera, int density)
: renderer_(renderer),
camera_(camera),
octahedron_(makeOctahedron()) {
stars_.resize(static_cast<std::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);
}
}
void Starfield3D::initStar(Star& star, bool spawn_at_far) {
star.position.x = randRange(-HALF_SPAWN_X, HALF_SPAWN_X);
star.position.y = randRange(-HALF_SPAWN_Y, HALF_SPAWN_Y);
star.position.z = spawn_at_far
? Z_FAR_SPAWN
: randRange(Z_NEAR_RESPAWN, Z_FAR_SPAWN);
star.velocity_z = -randRange(MIN_VELOCITY_Z, MAX_VELOCITY_Z);
star.rot_phase_y = randFloat01() * Defaults::Math::PI * 2.0F;
star.rot_phase_x = randFloat01() * Defaults::Math::PI * 2.0F;
star.rot_speed_y = randRange(MIN_ROT_SPEED, MAX_ROT_SPEED);
star.rot_speed_x = randRange(MIN_ROT_SPEED, MAX_ROT_SPEED);
star.scale = STAR_BASE_SCALE + (randRange(-1.0F, 1.0F) * STAR_SCALE_JITTER);
}
auto Starfield3D::computeBrightness(const Star& star) const -> float {
// Lerp segons distància Z normalitzada [Z_NEAR_RESPAWN .. Z_FAR_SPAWN].
const float SPAN = Z_FAR_SPAWN - Z_NEAR_RESPAWN;
const float T_RAW = (star.position.z - Z_NEAR_RESPAWN) / SPAN;
const float T = std::clamp(T_RAW, 0.0F, 1.0F);
const float BRIGHTNESS = BRIGHTNESS_NEAR + (T * (BRIGHTNESS_FAR - BRIGHTNESS_NEAR));
return std::clamp(BRIGHTNESS * brightness_mult_, 0.0F, 1.0F);
}
void Starfield3D::update(float delta_time) {
for (auto& star : stars_) {
star.position.z += star.velocity_z * delta_time;
star.rot_phase_y += star.rot_speed_y * delta_time;
star.rot_phase_x += star.rot_speed_x * delta_time;
if (star.position.z < Z_NEAR_RESPAWN) {
initStar(star, /*spawn_at_far=*/true);
}
}
}
void Starfield3D::draw() const {
if (camera_ == nullptr || renderer_ == nullptr) {
return;
}
// Ordena de més lluny a més a prop perquè el blending additiu acumule
// brightness sense que els elements de davant queden tapats pels de
// darrere. Còpia d'índexs per no modificar l'ordre intern de stars_.
std::vector<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;
});
for (std::size_t idx : order) {
const Star& star = stars_[idx];
const Transform3D TRANSFORM{
.position = star.position,
.rotation_euler = Vec3{.x = star.rot_phase_x, .y = star.rot_phase_y, .z = 0.0F},
.scale = star.scale,
};
drawWireframe(renderer_, *camera_, octahedron_, TRANSFORM, computeBrightness(star));
}
}
void Starfield3D::setBrightness(float multiplier) {
brightness_mult_ = std::max(0.0F, multiplier);
}
} // namespace Graphics
-68
View File
@@ -1,68 +0,0 @@
// starfield3d.hpp - Camp de estrelles 3D real per a l'escena de títol
// © 2026 JailDesigner
//
// Equivalent 3D del `Graphics::Starfield`. Cada estrella és un octaedre
// wireframe situat en l'espai mundial (Vec3). Es desplacen cap a la càmera
// (Z disminueix); quan creuen el pla Z_NEAR_RESPAWN es regeneren a Z_FAR_SPAWN
// amb X/Y aleatori. Cada octaedre rota lentament sobre Y i X per donar volum.
#pragma once
#include <vector>
#include "core/graphics/camera3d.hpp"
#include "core/graphics/wireframe3d.hpp"
#include "core/rendering/render_context.hpp"
#include "core/types.hpp"
namespace Graphics {
class Starfield3D {
public:
Starfield3D(Rendering::Renderer* renderer, const Camera3D* camera, int density = 200);
void update(float delta_time);
void draw() const;
void setBrightness(float multiplier);
private:
struct Star {
Vec3 position{};
float velocity_z{0.0F}; // Negatiu: cap a càmera
float rot_phase_y{0.0F};
float rot_phase_x{0.0F};
float rot_speed_y{0.0F};
float rot_speed_x{0.0F};
float scale{1.0F};
};
static void initStar(Star& star, bool spawn_at_far);
[[nodiscard]] auto computeBrightness(const Star& star) const -> float;
Rendering::Renderer* renderer_;
const Camera3D* camera_;
std::vector<Star> stars_;
Mesh3D octahedron_;
float brightness_mult_{1.0F};
// Volum de spawn / regeneració en l'espai 3D.
static constexpr float Z_NEAR_RESPAWN = 5.0F; // Si Z < aquest valor → regenera
static constexpr float Z_FAR_SPAWN = 800.0F; // Z de regeneració (lluny)
static constexpr float HALF_SPAWN_X = 600.0F; // X aleatori dins [-, +]
static constexpr float HALF_SPAWN_Y = 360.0F; // Y aleatori dins [-, +]
// Mida i moviment.
static constexpr float STAR_BASE_SCALE = 1.8F;
static constexpr float STAR_SCALE_JITTER = 0.6F;
static constexpr float MIN_VELOCITY_Z = 80.0F;
static constexpr float MAX_VELOCITY_Z = 200.0F;
static constexpr float MIN_ROT_SPEED = 0.2F;
static constexpr float MAX_ROT_SPEED = 0.8F;
// Brightness en funció de la distància Z (a prop = més brillant).
static constexpr float BRIGHTNESS_FAR = 0.15F;
static constexpr float BRIGHTNESS_NEAR = 1.0F;
};
} // namespace Graphics
+140
View File
@@ -0,0 +1,140 @@
// starfield_parallax.cpp - Implementació del starfield 2D amb parallax
// © 2026 JailDesigner
#include "core/graphics/starfield_parallax.hpp"
#include <cstdlib>
#include "core/defaults.hpp"
#include "core/rendering/line_renderer.hpp"
namespace Graphics {
namespace {
auto randUniform(float min_v, float max_v) -> float {
const float NORM = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
return min_v + (NORM * (max_v - min_v));
}
} // namespace
StarfieldParallax::StarfieldParallax(Rendering::Renderer* renderer)
: renderer_(renderer) {
buildStars();
}
void StarfieldParallax::buildStars() {
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
const float MIN_X = zone.x;
const float MAX_X = zone.x + zone.w;
const float MIN_Y = zone.y;
const float MAX_Y = zone.y + zone.h;
// Color únic per a totes les estrelles: el mateix blanc-blau gel
// del starfield del títol (Defaults::Title::Colors::STARFIELD).
const auto FILL_LAYER = [&](int layer, int count, int& idx) {
for (int i = 0; i < count; i++) {
stars_[idx++] = Star{
.x = randUniform(MIN_X, MAX_X),
.y = randUniform(MIN_Y, MAX_Y),
.layer = layer,
.color = Defaults::Title::Colors::STARFIELD};
}
};
int idx = 0;
FILL_LAYER(0, Defaults::StarfieldParallax::Far::COUNT, idx);
FILL_LAYER(1, Defaults::StarfieldParallax::Mid::COUNT, idx);
FILL_LAYER(2, Defaults::StarfieldParallax::Near::COUNT, idx);
}
auto StarfieldParallax::layerBrightness(int layer) -> float {
switch (layer) {
case 0:
return Defaults::StarfieldParallax::Far::BRIGHTNESS;
case 1:
return Defaults::StarfieldParallax::Mid::BRIGHTNESS;
case 2:
return Defaults::StarfieldParallax::Near::BRIGHTNESS;
default:
return 0.0F;
}
}
auto StarfieldParallax::layerParallax(int layer) -> float {
switch (layer) {
case 0:
return Defaults::StarfieldParallax::Far::PARALLAX_FACTOR;
case 1:
return Defaults::StarfieldParallax::Mid::PARALLAX_FACTOR;
case 2:
return Defaults::StarfieldParallax::Near::PARALLAX_FACTOR;
default:
return 0.0F;
}
}
auto StarfieldParallax::layerSize(int layer) -> int {
switch (layer) {
case 0:
return Defaults::StarfieldParallax::Far::SIZE_PX;
case 1:
return Defaults::StarfieldParallax::Mid::SIZE_PX;
case 2:
return Defaults::StarfieldParallax::Near::SIZE_PX;
default:
return 1;
}
}
void StarfieldParallax::update(float delta_time, Vec2 world_velocity) {
const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
const float MIN_X = zone.x;
const float MAX_X = zone.x + zone.w;
const float MIN_Y = zone.y;
const float MAX_Y = zone.y + zone.h;
const float W = zone.w;
const float H = zone.h;
for (auto& star : stars_) {
const float FACTOR = layerParallax(star.layer);
star.x += world_velocity.x * FACTOR * delta_time;
star.y += world_velocity.y * FACTOR * delta_time;
// Wraparound (PLAYAREA torica).
while (star.x < MIN_X) {
star.x += W;
}
while (star.x > MAX_X) {
star.x -= W;
}
while (star.y < MIN_Y) {
star.y += H;
}
while (star.y > MAX_Y) {
star.y -= H;
}
}
}
void StarfieldParallax::draw() const {
for (const auto& star : stars_) {
const float B = layerBrightness(star.layer);
const int SIZE = layerSize(star.layer);
const int X = static_cast<int>(star.x);
const int Y = static_cast<int>(star.y);
if (SIZE <= 1) {
// Punt d'1 px: línia degenerada horitzontal de 1 px.
Rendering::linea(renderer_, X, Y, X + 1, Y, B, 0.0F, star.color);
} else {
// Creu "+" amb extensió HALF des del centre en cada direcció.
const int HALF = SIZE - 1; // SIZE=2 → ±1 (creu 3x3); SIZE=3 → ±2 (creu 5x5)
Rendering::linea(renderer_, X - HALF, Y, X + HALF + 1, Y, B, 0.0F, star.color);
Rendering::linea(renderer_, X, Y - HALF, X, Y + HALF + 1, B, 0.0F, star.color);
}
}
}
} // namespace Graphics
@@ -0,0 +1,51 @@
// starfield_parallax.hpp - Capa més profunda del fons: estrelles 2D amb parallax
// © 2026 JailDesigner
//
// Estrelles 2D distribuïdes en 3 capes de profunditat. Cada capa té el seu
// factor parallax: el "món" es desplaça amb world_velocity i les estrelles
// d'una capa es mouen amb world_velocity * parallax_factor. Les més
// properes es mouen més (factor alt) → sensació de profunditat.
// Quan una estrella surt de PLAYAREA, reapareix per la banda oposada
// (wraparound).
#pragma once
#include <SDL3/SDL.h>
#include <array>
#include "core/defaults/starfield_parallax.hpp"
#include "core/rendering/render_context.hpp"
#include "core/types.hpp"
namespace Graphics {
class StarfieldParallax {
public:
explicit StarfieldParallax(Rendering::Renderer* renderer);
// Avança el desplaçament de les estrelles segons world_velocity (vector
// del moviment del món en px/s; típicament = -ship_velocity).
// world_velocity == {0, 0} → estrelles quietes.
void update(float delta_time, Vec2 world_velocity);
void draw() const;
private:
struct Star {
float x{0.0F};
float y{0.0F};
int layer{0}; // 0=Far, 1=Mid, 2=Near
SDL_Color color{}; // tint precomputat entre blanc i cyan
};
void buildStars();
static auto layerBrightness(int layer) -> float;
static auto layerParallax(int layer) -> float;
static auto layerSize(int layer) -> int;
Rendering::Renderer* renderer_;
std::array<Star, Defaults::StarfieldParallax::TOTAL_COUNT> stars_{};
};
} // namespace Graphics
+7 -6
View File
@@ -221,7 +221,8 @@ namespace Graphics {
// Ajustar X e Y para que position represente esquina superior izquierda // Ajustar X e Y para que position represente esquina superior izquierda
// (render_shape espera el centro, así que sumamos la mitad de ancho y altura) // (render_shape espera el centro, así que sumamos la mitad de ancho y altura)
Vec2 char_pos = {.x = current_x + (CHAR_WIDTH_SCALED / 2.0F), .y = position.y + (CHAR_HEIGHT_SCALED / 2.0F)}; Vec2 char_pos = {.x = current_x + (CHAR_WIDTH_SCALED / 2.0F), .y = position.y + (CHAR_HEIGHT_SCALED / 2.0F)};
Rendering::renderShape(renderer_, it->second, char_pos, 0.0F, scale, 1.0F, brightness, color); // Text opt-out del glow: HUD/marker s'ha de mantenir net.
Rendering::renderShape(renderer_, it->second, char_pos, 0.0F, scale, 1.0F, brightness, color, 0.0F, 1.0F, /*glow=*/false);
// Avanzar posición // Avanzar posición
current_x += CHAR_WIDTH_SCALED + SPACING_SCALED; current_x += CHAR_WIDTH_SCALED + SPACING_SCALED;
@@ -234,19 +235,19 @@ namespace Graphics {
} }
} }
void VectorText::renderCentered(const std::string& text, const Vec2& centre_punt, float scale, float spacing, float brightness, SDL_Color color) const { void VectorText::renderCentered(const std::string& text, const Vec2& centre_point, float scale, float spacing, float brightness, SDL_Color color) const {
// Calcular dimensions del text // Calcular dimensions del text
float text_width = getTextWidth(text, scale, spacing); float text_width = getTextWidth(text, scale, spacing);
float text_height = getTextHeight(scale); float text_height = getTextHeight(scale);
// Calcular posición de l'esquina superior izquierda // Calcular posición de l'esquina superior izquierda
// restant la meitat de las dimensions del point central // restant la meitat de las dimensions del point central
Vec2 posicio_esquerra = { Vec2 top_left_position = {
.x = centre_punt.x - (text_width / 2.0F), .x = centre_point.x - (text_width / 2.0F),
.y = centre_punt.y - (text_height / 2.0F)}; .y = centre_point.y - (text_height / 2.0F)};
// Delegar al método render() existent // Delegar al método render() existent
render(text, posicio_esquerra, scale, spacing, brightness, color); render(text, top_left_position, scale, spacing, brightness, color);
} }
auto VectorText::getTextWidth(const std::string& text, float scale, float spacing) -> float { auto VectorText::getTextWidth(const std::string& text, float scale, float spacing) -> float {
+2 -2
View File
@@ -31,12 +31,12 @@ namespace Graphics {
// Renderizar string centrado en un punto // Renderizar string centrado en un punto
// - text: cadena a renderizar // - text: cadena a renderizar
// - centre_punt: punto central del texto (no esquina superior izquierda) // - centre_point: punto central del texto (no esquina superior izquierda)
// - scale: factor de scale (1.0 = 20×40 px por carácter) // - scale: factor de scale (1.0 = 20×40 px por carácter)
// - spacing: espacio entre caracteres en píxeles (a scale 1.0) // - spacing: espacio entre caracteres en píxeles (a scale 1.0)
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness) // - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
// - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global // - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global
void renderCentered(const std::string& text, const Vec2& centre_punt, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const; void renderCentered(const std::string& text, const Vec2& centre_point, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
// Calcular ancho total de un string (útil para centrado). // Calcular ancho total de un string (útil para centrado).
// Es estático: no depende del estado del VectorText (el ancho viene de // Es estático: no depende del estado del VectorText (el ancho viene de
+50 -18
View File
@@ -3,30 +3,62 @@
#pragma once #pragma once
#include <algorithm>
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/types.hpp" #include "core/types.hpp"
namespace Physics { namespace Physics {
// Comprobación genèrica de colisión entre dues entidades // Comprobación genèrica de colisión entre dues entidades
inline auto checkCollision(const Entities::Entity& a, const Entities::Entity& b, float amplifier = 1.0F) -> bool { inline auto checkCollision(const Entities::Entity& a, const Entities::Entity& b, float amplifier = 1.0F) -> bool {
// Comprovar si ambdós són col·lisionables // Comprovar si ambdós són col·lisionables
if (!a.isCollidable() || !b.isCollidable()) { if (!a.isCollidable() || !b.isCollidable()) {
return false; return false;
}
// Calcular radius combinat (con amplificador per hitbox generós)
float suma_radis = (a.getCollisionRadius() + b.getCollisionRadius()) * amplifier;
float suma_radis_sq = suma_radis * suma_radis;
// Comprobación distancia al cuadrado (sin sqrt)
const Vec2& pos_a = a.getCenter();
const Vec2& pos_b = b.getCenter();
float dx = pos_a.x - pos_b.x;
float dy = pos_a.y - pos_b.y;
float dist_sq = (dx * dx) + (dy * dy);
return dist_sq <= suma_radis_sq;
} }
// Calcular radi combinat (con amplificador per hitbox generós) // Swept collision: una entitat mòbil (radius r_a) s'ha desplaçat de p0 a p1 aquest
float suma_radis = (a.getCollisionRadius() + b.getCollisionRadius()) * amplifier; // frame. Comprova si el segment expandit pel radius conjunt (r_a + radius de b, amb
float suma_radis_sq = suma_radis * suma_radis; // amplificador) toca el cercle de l'entity b. Equival al check discrete quan
// p0 == p1 (sense moviment). Evita tunneling a velocitats altes.
// Comprobación distancia al cuadrado (sin sqrt) inline auto checkCollisionSwept(const Vec2& p0, const Vec2& p1, float r_a, const Entities::Entity& b, float amplifier = 1.0F) -> bool {
const Vec2& pos_a = a.getCenter(); if (!b.isCollidable()) {
const Vec2& pos_b = b.getCenter(); return false;
float dx = pos_a.x - pos_b.x; }
float dy = pos_a.y - pos_b.y; const float SUM_R = (r_a + b.getCollisionRadius()) * amplifier;
float dist_sq = (dx * dx) + (dy * dy); const float SUM_R_SQ = SUM_R * SUM_R;
const Vec2& center_b = b.getCenter();
return dist_sq <= suma_radis_sq; const float DX_SEG = p1.x - p0.x;
} const float DY_SEG = p1.y - p0.y;
const float LEN_SQ = (DX_SEG * DX_SEG) + (DY_SEG * DY_SEG);
// Degenerat: punt-cercle (frame de spawn, o entitat parada).
if (LEN_SQ <= 0.0F) {
const float DX = p0.x - center_b.x;
const float DY = p0.y - center_b.y;
return ((DX * DX) + (DY * DY)) <= SUM_R_SQ;
}
// Projecció del centre sobre la recta del segment, clamp a [0,1] per acotar al segment.
const float T_RAW = (((center_b.x - p0.x) * DX_SEG) + ((center_b.y - p0.y) * DY_SEG)) / LEN_SQ;
const float T_CLAMPED = std::clamp(T_RAW, 0.0F, 1.0F);
const float CLOSEST_X = p0.x + (DX_SEG * T_CLAMPED);
const float CLOSEST_Y = p0.y + (DY_SEG * T_CLAMPED);
const float DX = CLOSEST_X - center_b.x;
const float DY = CLOSEST_Y - center_b.y;
return ((DX * DX) + (DY * DY)) <= SUM_R_SQ;
}
} // namespace Physics } // namespace Physics
+25 -2
View File
@@ -4,6 +4,7 @@
#include "core/rendering/line_renderer.hpp" #include "core/rendering/line_renderer.hpp"
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/defaults/effects.hpp"
namespace Rendering { namespace Rendering {
@@ -22,7 +23,8 @@ namespace Rendering {
int y2, int y2,
float brightness, float brightness,
float thickness, float thickness,
SDL_Color color) { SDL_Color color,
float alpha) {
if (renderer == nullptr) { if (renderer == nullptr) {
return; return;
} }
@@ -42,7 +44,28 @@ namespace Rendering {
const float W = (thickness > 0.0F) ? thickness : g_current_line_thickness; const float W = (thickness > 0.0F) ? thickness : g_current_line_thickness;
renderer->pushLine(FX1, FY1, FX2, FY2, W, R, G, B, 1.0F); renderer->pushLine(FX1, FY1, FX2, FY2, W, R, G, B, alpha);
}
void lineaGlow(Renderer* renderer,
int x1,
int y1,
int x2,
int y2,
float brightness,
float thickness,
SDL_Color color,
SDL_Color glow_color) {
// Color dels passes de halo: si glow_color té alpha>0, l'usem;
// altrament fem servir el color de la línia.
const SDL_Color HALO_COLOR = (glow_color.a > 0) ? glow_color : color;
for (const auto& pass : Defaults::FX::Glow::Line::PASSES) {
const bool IS_CORE = pass.thickness < 0.0F;
const float PASS_T = IS_CORE ? thickness : pass.thickness;
const SDL_Color PASS_C = IS_CORE ? color : HALO_COLOR;
linea(renderer, x1, y1, x2, y2, brightness, PASS_T, PASS_C, pass.alpha);
}
} }
void setLineColor(SDL_Color color) { g_current_line_color = color; } void setLineColor(SDL_Color color) { g_current_line_color = color; }
+21 -1
View File
@@ -17,9 +17,13 @@ namespace Rendering {
// Dibuja una línea entre dos puntos en coordenadas lógicas (1280×720). // Dibuja una línea entre dos puntos en coordenadas lógicas (1280×720).
// brightness: factor de brillo (0.0..1.0, default 1.0 = brillo máximo). // brightness: factor de brillo (0.0..1.0, default 1.0 = brillo máximo).
// Pre-multiplica el RGB del color (color dim sobre fons negre).
// thickness: grosor en píxeles lógicos. Si <= 0 usa g_current_line_thickness. // thickness: grosor en píxeles lógicos. Si <= 0 usa g_current_line_thickness.
// color: si alpha==0, se usa el color global del oscilador; si alpha>0 se // color: si alpha==0, se usa el color global del oscilador; si alpha>0 se
// usa este color directo (paleta semántica por entidad). // usa este color directo (paleta semántica por entidad).
// alpha: alpha que arriba al GPU (default 1.0 = opac, behavior original).
// Valors <1.0 fan que la línia es barregi de veritat sobre el dest
// en comptes de sobrepintar-lo (útil per halos translúcids).
void linea(Renderer* renderer, void linea(Renderer* renderer,
int x1, int x1,
int y1, int y1,
@@ -27,7 +31,23 @@ namespace Rendering {
int y2, int y2,
float brightness = 1.0F, float brightness = 1.0F,
float thickness = 0.0F, float thickness = 0.0F,
SDL_Color color = {0, 0, 0, 0}); SDL_Color color = {0, 0, 0, 0},
float alpha = 1.0F);
// Versió amb halo neon: dibuixa la línia amb diversos passos de gruix
// creixent i alfa decreixent (config a Defaults::FX::Glow::Line::PASSES).
// El core (últim pass) usa el thickness/alpha que passa el caller.
// glow_color: si alpha>0, els passes de halo usen aquest color en lloc
// del color de la línia (p.ex. línia blanca amb halo daurat).
void lineaGlow(Renderer* renderer,
int x1,
int y1,
int x2,
int y2,
float brightness = 1.0F,
float thickness = 0.0F,
SDL_Color color = {0, 0, 0, 0},
SDL_Color glow_color = {0, 0, 0, 0});
// Color global de las líneas (lo actualiza ColorOscillator vía SDLManager). // Color global de las líneas (lo actualiza ColorOscillator vía SDLManager).
void setLineColor(SDL_Color color); void setLineColor(SDL_Color color);
+81 -27
View File
@@ -3,31 +3,77 @@
#include "core/rendering/shape_renderer.hpp" #include "core/rendering/shape_renderer.hpp"
#include <algorithm>
#include <cmath> #include <cmath>
#include "core/defaults/effects.hpp"
#include "core/graphics/shape.hpp"
#include "core/rendering/line_renderer.hpp" #include "core/rendering/line_renderer.hpp"
namespace Rendering { namespace Rendering {
// Helper: transformar un point con rotación, scale i traslación // Helper: transformar un point con rotación, scale i traslación
static auto transformPoint(const Vec2& point, const Vec2& shape_centre, const Vec2& position, float angle, float scale) -> Vec2 { static auto transformPoint(const Vec2& point, const Vec2& shape_centre, const Vec2& position, float angle, float scale) -> Vec2 {
// 1. Centrar el point respecte al centro de la shape const float CENTERED_X = point.x - shape_centre.x;
float centered_x = point.x - shape_centre.x; const float CENTERED_Y = point.y - shape_centre.y;
float centered_y = point.y - shape_centre.y;
// 2. Aplicar scale al point const float SCALED_X = CENTERED_X * scale;
float scaled_x = centered_x * scale; const float SCALED_Y = CENTERED_Y * scale;
float scaled_y = centered_y * scale;
// 3. Aplicar rotación 2D (Z-axis) const float COS_A = std::cos(angle);
float cos_a = std::cos(angle); const float SIN_A = std::sin(angle);
float sin_a = std::sin(angle);
float rotated_x = (scaled_x * cos_a) - (scaled_y * sin_a); const float ROTATED_X = (SCALED_X * COS_A) - (SCALED_Y * SIN_A);
float rotated_y = (scaled_x * sin_a) + (scaled_y * cos_a); const float ROTATED_Y = (SCALED_X * SIN_A) + (SCALED_Y * COS_A);
// 4. Aplicar traslación a posición mundial return {.x = ROTATED_X + position.x, .y = ROTATED_Y + position.y};
return {.x = rotated_x + position.x, .y = rotated_y + position.y}; }
// Una passada de renderitzat: itera primitives de la shape i emet línies
// amb el thickness/alpha indicats. Es crida N vegades en glow mode (una
// per pass de halo + core), o 1 vegada quan glow=false.
static void renderSinglePass(Rendering::Renderer* renderer,
const std::shared_ptr<Graphics::Shape>& shape,
const Vec2& position,
float angle,
float scale,
float brightness,
SDL_Color color,
float thickness,
float alpha) {
const Vec2& shape_centre = shape->getCenter();
// Petita extensió a línies gruixudes per tapar forats entre segments.
// A vèrtex aguts (~108°) un valor alt produeix "espigues" — 15%.
const float EFFECTIVE_T = (thickness > 0.0F) ? thickness : getLineThickness();
const float EXTEND = (EFFECTIVE_T > 2.0F) ? (EFFECTIVE_T * 0.15F) : 0.0F;
for (const auto& primitive : shape->getPrimitives()) {
if (primitive.type == Graphics::PrimitiveType::POLYLINE) {
for (size_t i = 0; i < primitive.points.size() - 1; i++) {
Vec2 p1 = transformPoint(primitive.points[i], shape_centre, position, angle, scale);
Vec2 p2 = transformPoint(primitive.points[i + 1], shape_centre, position, angle, scale);
if (EXTEND > 0.0F) {
const float DX = p2.x - p1.x;
const float DY = p2.y - p1.y;
const float LEN = std::sqrt((DX * DX) + (DY * DY));
if (LEN > 1e-6F) {
const float UX = (DX / LEN) * EXTEND;
const float UY = (DY / LEN) * EXTEND;
p1.x -= UX;
p1.y -= UY;
p2.x += UX;
p2.y += UY;
}
}
linea(renderer, static_cast<int>(p1.x), static_cast<int>(p1.y), static_cast<int>(p2.x), static_cast<int>(p2.y), brightness, thickness, color, alpha);
}
} else if (primitive.points.size() >= 2) { // LINE
const Vec2 P1 = transformPoint(primitive.points[0], shape_centre, position, angle, scale);
const Vec2 P2 = transformPoint(primitive.points[1], shape_centre, position, angle, scale);
linea(renderer, static_cast<int>(P1.x), static_cast<int>(P1.y), static_cast<int>(P2.x), static_cast<int>(P2.y), brightness, thickness, color, alpha);
}
}
} }
void renderShape(Rendering::Renderer* renderer, void renderShape(Rendering::Renderer* renderer,
@@ -37,7 +83,10 @@ namespace Rendering {
float scale, float scale,
float progress, float progress,
float brightness, float brightness,
SDL_Color color) { SDL_Color color,
float thickness,
float alpha,
bool glow) {
if (!shape || !shape->isValid()) { if (!shape || !shape->isValid()) {
return; return;
} }
@@ -45,21 +94,26 @@ namespace Rendering {
return; return;
} }
const Vec2& shape_centre = shape->getCenter(); if (!glow) {
renderSinglePass(renderer, shape, position, angle, scale, brightness, color, thickness, alpha);
return;
}
for (const auto& primitive : shape->getPrimitives()) { // Glow: multi-pass amb halos translúcids proporcionals al tamany de
if (primitive.type == Graphics::PrimitiveType::POLYLINE) { // la shape. Cada pass amb thickness_ratio<0 usa el thickness/alpha
// POLYLINE: conectar puntos consecutivos. // que ha passat el caller (és el "core" / línia real). Saturem la
for (size_t i = 0; i < primitive.points.size() - 1; i++) { // mida de referència a MAX_REFERENCE_RADIUS perquè shapes molt
const Vec2 P1 = transformPoint(primitive.points[i], shape_centre, position, angle, scale); // grans (logos) no tinguin halo desproporcionat.
const Vec2 P2 = transformPoint(primitive.points[i + 1], shape_centre, position, angle, scale); const float RAW_REF = shape->getBoundingRadius() * scale;
linea(renderer, static_cast<int>(P1.x), static_cast<int>(P1.y), static_cast<int>(P2.x), static_cast<int>(P2.y), brightness, 0.0F, color); const float REFERENCE_SIZE = std::min(RAW_REF, Defaults::FX::Glow::MAX_REFERENCE_RADIUS);
} for (const auto& pass : Defaults::FX::Glow::PASSES) {
} else if (primitive.points.size() >= 2) { // LINE float pass_thickness = thickness;
const Vec2 P1 = transformPoint(primitive.points[0], shape_centre, position, angle, scale); float pass_alpha = alpha;
const Vec2 P2 = transformPoint(primitive.points[1], shape_centre, position, angle, scale); if (pass.thickness_ratio > 0.0F) {
linea(renderer, static_cast<int>(P1.x), static_cast<int>(P1.y), static_cast<int>(P2.x), static_cast<int>(P2.y), brightness, 0.0F, color); pass_thickness = REFERENCE_SIZE * pass.thickness_ratio;
pass_alpha = pass.alpha * alpha; // respecta el master alpha del caller
} }
renderSinglePass(renderer, shape, position, angle, scale, brightness, color, pass_thickness, pass_alpha);
} }
} }
+9 -1
View File
@@ -21,6 +21,11 @@ namespace Rendering {
// - scale: factor de scale (1.0 = mida original) // - scale: factor de scale (1.0 = mida original)
// - progress: progrés de l'animación (0.0-1.0, default 1.0 = tot visible) // - progress: progrés de l'animación (0.0-1.0, default 1.0 = tot visible)
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness) // - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
// - color: si alpha==0, usa oscil·lador global
// - thickness: gruix de línia. <=0 → usa global (g_current_line_thickness)
// - alpha: alpha que arriba al GPU (default 1.0 = opac). <1.0 = halo real
// - glow: si true, redibuixa la shape amb halos translúcids proporcionals
// al bounding_radius*scale (efecte neon). Si false, single-pass.
void renderShape(Rendering::Renderer* renderer, void renderShape(Rendering::Renderer* renderer,
const std::shared_ptr<Graphics::Shape>& shape, const std::shared_ptr<Graphics::Shape>& shape,
const Vec2& position, const Vec2& position,
@@ -28,6 +33,9 @@ namespace Rendering {
float scale = 1.0F, float scale = 1.0F,
float progress = 1.0F, float progress = 1.0F,
float brightness = 1.0F, float brightness = 1.0F,
SDL_Color color = {0, 0, 0, 0}); // alpha==0 → usa global oscilador SDL_Color color = {0, 0, 0, 0},
float thickness = 0.0F,
float alpha = 1.0F,
bool glow = true);
} // namespace Rendering } // namespace Rendering
+3 -3
View File
@@ -2,8 +2,8 @@
// © 2026 JailDesigner // © 2026 JailDesigner
// //
// Sistema global propiedad del Director. Se actualiza y dibuja cada frame // Sistema global propiedad del Director. Se actualiza y dibuja cada frame
// después de la escena (queda on top). En builds debug arranca visible, // después de la escena (queda on top). Arranca oculto sempre; F11 alterna
// en release oculto. F11 alterna visibilidad. // visibilidad durant l'execució.
#pragma once #pragma once
@@ -32,7 +32,7 @@ namespace System {
private: private:
Graphics::VectorText text_; Graphics::VectorText text_;
const Config::RenderingConfig* rendering_cfg_; const Config::RenderingConfig* rendering_cfg_;
bool visible_{true}; bool visible_{false};
// FPS counter — se actualiza cada FPS_UPDATE_INTERVAL segundos. // FPS counter — se actualiza cada FPS_UPDATE_INTERVAL segundos.
float fps_accumulator_{0.0F}; float fps_accumulator_{0.0F};
+157 -150
View File
@@ -21,10 +21,10 @@
#include "core/system/notifier.hpp" #include "core/system/notifier.hpp"
#include "core/utils/path_utils.hpp" #include "core/utils/path_utils.hpp"
#include "debug_overlay.hpp" #include "debug_overlay.hpp"
#include "game/config_yaml.hpp"
#include "game/scenes/game_scene.hpp" #include "game/scenes/game_scene.hpp"
#include "game/scenes/logo_scene.hpp" #include "game/scenes/logo_scene.hpp"
#include "game/scenes/title_scene.hpp" #include "game/scenes/title_scene.hpp"
#include "game/scenes/title_scene_3d.hpp"
#include "global_events.hpp" #include "global_events.hpp"
#include "project.h" #include "project.h"
#include "scene.hpp" #include "scene.hpp"
@@ -40,17 +40,15 @@ using SceneManager::SceneContext;
using SceneType = SceneContext::SceneType; using SceneType = SceneContext::SceneType;
// Constructor // Constructor
Director::Director(std::vector<std::string> const& args, Director::Director(int argc, char* argv[])
Config::EngineConfig& cfg, : cfg_(&ConfigYaml::engine_config) {
Config::ConfigPersistence persistence) std::cout << "Game start\n";
: cfg_(&cfg),
persistence_(std::move(persistence)) {
std::cout << "Orni Attack - Inici\n";
// Inicialitzar opciones con valors per defecte // Inicialitzar opciones con valors per defecte
persistence_.init_defaults(); ConfigYaml::init();
// Comprovar arguments del programa // Convertir arguments a std::vector<std::string> i comprovar-los
std::vector<std::string> args(argv, argv + argc);
executable_path_ = checkProgramArguments(args); executable_path_ = checkProgramArguments(args);
// Inicialitzar sistema de rutes // Inicialitzar sistema de rutes
@@ -96,10 +94,10 @@ Director::Director(std::vector<std::string> const& args,
createSystemFolder(std::string("jailgames/") + Project::NAME); createSystemFolder(std::string("jailgames/") + Project::NAME);
// Establir ruta del file de configuración // Establir ruta del file de configuración
persistence_.set_path(system_folder_ + "/config.yaml"); ConfigYaml::setConfigFile(system_folder_ + "/config.yaml");
// Carregar o crear configuración // Carregar o crear configuración
persistence_.load(); ConfigYaml::loadFromFile();
// Inicialitzar sistema de input // Inicialitzar sistema de input
Input::init("data/gamecontrollerdb.txt"); Input::init("data/gamecontrollerdb.txt");
@@ -117,22 +115,73 @@ Director::Director(std::vector<std::string> const& args,
} }
std::cout << '\n'; std::cout << '\n';
// === Bootstrap de finestra, audio i subsistemes de runtime ===
int initial_width = static_cast<int>(std::round(
Defaults::Window::WIDTH * cfg_->window.zoom_factor));
int initial_height = static_cast<int>(std::round(
Defaults::Window::HEIGHT * cfg_->window.zoom_factor));
sdl_ = std::make_unique<SDLManager>(initial_width, initial_height, cfg_->window.fullscreen, *cfg_, [] { ConfigYaml::saveToFile(); });
// CRÍTIC: forçar ocultació del cursor DESPRÉS d'inicialitzar SDL,
// perquè la creació de la finestra el reactiva.
if (!cfg_->window.fullscreen) {
Mouse::forceHide();
}
const Audio::Config AUDIO_CONFIG{
.enabled = Defaults::Audio::ENABLED,
.volume = Defaults::Audio::VOLUME,
.music_enabled = Defaults::Audio::MUSIC_ENABLED,
.music_volume = Defaults::Audio::MUSIC_VOLUME,
.sound_enabled = Defaults::Audio::SOUND_ENABLED,
.sound_volume = Defaults::Audio::SOUND_VOLUME,
};
Audio::init(AUDIO_CONFIG);
Audio::get()->applySettings(AUDIO_CONFIG);
AudioResource::getMusic("title.ogg");
AudioResource::getMusic("game.ogg");
if (cfg_->console) {
std::cout << "Música precacheada\n";
}
context_ = std::make_unique<SceneContext>();
#ifdef _DEBUG
context_->setNextScene(SceneType::TITLE);
#else
context_->setNextScene(SceneType::LOGO);
#endif
debug_overlay_ = std::make_unique<System::DebugOverlay>(
sdl_->getRenderer(),
cfg_->rendering);
System::Notifier::init(sdl_->getRenderer());
last_ticks_ms_ = SDL_GetTicks();
} }
Director::~Director() { Director::~Director() {
// Guardar opciones // Guardar opciones
persistence_.save(); ConfigYaml::saveToFile();
// Destruir subsistemes en ordre invers a la construcció. El Notifier
// referencia el renderer, així que ha de morir abans que sdl_.
// SDL_Quit() el crida SDL automàticament després de SDL_AppQuit; no
// l'hem de cridar nosaltres.
current_scene_.reset();
debug_overlay_.reset();
System::Notifier::destroy();
context_.reset();
sdl_.reset();
// Cleanup input
Input::destroy(); Input::destroy();
// Cleanup audio
Audio::destroy(); Audio::destroy();
// Cleanup SDL std::cout << "\nBye!\n";
SDL_Quit();
std::cout << "\nAdéu!\n";
} }
// Comprovar arguments del programa // Comprovar arguments del programa
@@ -145,8 +194,8 @@ auto Director::checkProgramArguments(std::vector<std::string> const& args)
cfg_->console = true; cfg_->console = true;
std::cout << "Mode consola activat\n"; std::cout << "Mode consola activat\n";
} else if (argument == "--reset-config") { } else if (argument == "--reset-config") {
persistence_.init_defaults(); ConfigYaml::init();
persistence_.save(); ConfigYaml::saveToFile();
std::cout << "Configuración restablida als valors per defecte\n"; std::cout << "Configuración restablida als valors per defecte\n";
} }
} }
@@ -218,91 +267,13 @@ void Director::createSystemFolder(const std::string& folder) {
} }
} }
// Bucle principal del juego
auto Director::run() -> int {
// Calculate initial size from saved zoom_factor
int initial_width = static_cast<int>(std::round(
Defaults::Window::WIDTH * cfg_->window.zoom_factor));
int initial_height = static_cast<int>(std::round(
Defaults::Window::HEIGHT * cfg_->window.zoom_factor));
// Crear gestor SDL amb la engine_config + callback de persistència
// per a quan toggleVSync (F4) muti vsync. Mantenim sdl_manager agnòstic.
SDLManager sdl(initial_width, initial_height, cfg_->window.fullscreen, *cfg_, [this] { persistence_.save(); });
// CRÍTIC: Forçar ocultació del cursor DESPRÉS de toda la inicialización SDL
// Això evita que SDL mostre el cursor automàticament durante la creació de la finestra
if (!cfg_->window.fullscreen) {
Mouse::forceHide();
}
// Inicializar sistema de audio (config inyectada desde Defaults)
const Audio::Config AUDIO_CONFIG{
.enabled = Defaults::Audio::ENABLED,
.volume = Defaults::Audio::VOLUME,
.music_enabled = Defaults::Audio::MUSIC_ENABLED,
.music_volume = Defaults::Audio::MUSIC_VOLUME,
.sound_enabled = Defaults::Audio::SOUND_ENABLED,
.sound_volume = Defaults::Audio::SOUND_VOLUME,
};
Audio::init(AUDIO_CONFIG);
Audio::get()->applySettings(AUDIO_CONFIG); // Aplicar volúmenes iniciales al motor
// Precachear música para evitar lag al empezar
AudioResource::getMusic("title.ogg");
AudioResource::getMusic("game.ogg");
if (cfg_->console) {
std::cout << "Música precacheada\n";
}
// Crear context de escenes
SceneContext context;
#ifdef _DEBUG
context.setNextScene(SceneType::TITLE);
#else
context.setNextScene(SceneType::LOGO);
#endif
// Overlay de debug (FPS + VSync). Vive en el Director porque es global
// a todas las escenas. Toggle con F11 (visible por defecto en _DEBUG).
System::DebugOverlay debug_overlay(sdl.getRenderer(), cfg_->rendering);
// Sistema de notificacions toast: singleton accessible des d'on calgui
// (F1-F5 a sdl_manager, ESC a global_events). El renderer ha de viure
// tant com el Notifier; el destruim explícitament abans de tornar.
System::Notifier::init(sdl.getRenderer());
// Bucle principal: construir escena → frame loop → destruir → siguiente.
while (context.nextScene() != SceneType::EXIT) {
SceneManager::actual = context.nextScene();
std::unique_ptr<Scene> scene = buildScene(context.nextScene(), sdl, context);
if (!scene) {
break;
}
runFrameLoop(*scene, sdl, context, debug_overlay);
}
SceneManager::actual = SceneType::EXIT;
System::Notifier::destroy();
return 0;
}
auto Director::buildScene(SceneType type, SDLManager& sdl, SceneContext& context) auto Director::buildScene(SceneType type, SDLManager& sdl, SceneContext& context)
-> std::unique_ptr<Scene> { -> std::unique_ptr<Scene> {
switch (type) { switch (type) {
case SceneType::LOGO: case SceneType::LOGO:
return std::make_unique<LogoScene>(sdl, context); return std::make_unique<LogoScene>(sdl, context);
case SceneType::TITLE: { case SceneType::TITLE:
// Env var ORNI_TITLE_3D=1 redirigeix la TITLE clàssica cap a la
// variant 3D real en proves; en qualsevol altre cas, la 2D.
const char* env = std::getenv("ORNI_TITLE_3D");
if (env != nullptr && env[0] == '1' && env[1] == '\0') {
return std::make_unique<TitleScene3D>(sdl, context);
}
return std::make_unique<TitleScene>(sdl, context); return std::make_unique<TitleScene>(sdl, context);
}
case SceneType::TITLE_3D:
return std::make_unique<TitleScene3D>(sdl, context);
case SceneType::GAME: case SceneType::GAME:
return std::make_unique<GameScene>(sdl, context); return std::make_unique<GameScene>(sdl, context);
case SceneType::EXIT: case SceneType::EXIT:
@@ -311,55 +282,91 @@ auto Director::buildScene(SceneType type, SDLManager& sdl, SceneContext& context
} }
} }
void Director::runFrameLoop(Scene& scene, SDLManager& sdl, SceneContext& context, System::DebugOverlay& debug_overlay) { auto Director::advanceScene() -> SDL_AppResult {
SDL_Event event; current_scene_.reset();
Uint64 last_time = SDL_GetTicks(); const SceneType NEXT = context_->nextScene();
if (NEXT == SceneType::EXIT) {
while (!scene.isFinished()) { SceneManager::actual = SceneType::EXIT;
// Delta time real, capeado a 50ms para evitar grandes saltos. return SDL_APP_SUCCESS;
const Uint64 NOW = SDL_GetTicks();
float delta_time = static_cast<float>(NOW - last_time) / 1000.0F;
last_time = NOW;
delta_time = std::min(delta_time, 0.05F);
Mouse::updateCursorVisibility();
Input::get()->update();
// Event loop: primero ventana, después globales, después F11
// (toggle del overlay), después escena.
while (SDL_PollEvent(&event)) {
if (sdl.handleWindowEvent(event)) {
continue;
}
if (GlobalEvents::handle(event, sdl, context)) {
continue;
}
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_F11) {
debug_overlay.toggle();
continue;
}
scene.handleEvent(event);
}
scene.update(delta_time);
debug_overlay.update(delta_time);
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->update(delta_time);
}
Audio::update();
// Si la swapchain no está disponible (ventana minimizada, etc.),
// saltarse draw+present ese frame: dibujar dejaría vértices
// colgando en el batch interno sin nadie que los presente.
if (!sdl.clear(0, 0, 0)) {
continue;
}
sdl.updateRenderingContext();
scene.draw();
debug_overlay.draw(); // sempre per damunt de l'escena
if (const auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->draw(); // toast: per damunt de tot
}
sdl.present();
} }
SceneManager::actual = NEXT;
current_scene_ = buildScene(NEXT, *sdl_, *context_);
if (!current_scene_) {
SceneManager::actual = SceneType::EXIT;
return SDL_APP_SUCCESS;
}
return SDL_APP_CONTINUE;
}
auto Director::handleEvent(const SDL_Event& event) -> SDL_AppResult {
// 1. Window events (resize, minimize, focus...)
if (sdl_->handleWindowEvent(event)) {
return SDL_APP_CONTINUE;
}
// 2. Events globals (F1-F6, ESC, QUIT, gamepad hotplug).
// GlobalEvents marca context_->nextScene() = EXIT en ESC doble o QUIT;
// activem la bandera per fer-ho fluir cap a SDL_APP_SUCCESS al pròxim tick.
if (GlobalEvents::handle(event, *sdl_, *context_)) {
if (context_->nextScene() == SceneType::EXIT) {
wants_quit_ = true;
}
return SDL_APP_CONTINUE;
}
// 3. F11 → toggle del debug overlay (cas especial fora de GlobalEvents).
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_F11) {
debug_overlay_->toggle();
return SDL_APP_CONTINUE;
}
// 4. Esdeveniment específic de l'escena actual.
if (current_scene_) {
current_scene_->handleEvent(event);
}
return SDL_APP_CONTINUE;
}
auto Director::iterate() -> SDL_AppResult {
if (wants_quit_) {
return SDL_APP_SUCCESS;
}
// Pivotar a la següent escena si l'actual ha acabat (o és la primera).
if (!current_scene_ || current_scene_->isFinished()) {
SDL_AppResult pivot = advanceScene();
if (pivot != SDL_APP_CONTINUE) {
return pivot;
}
}
// Delta time real, capeado a 50ms per evitar grans salts.
const Uint64 NOW = SDL_GetTicks();
float delta_time = static_cast<float>(NOW - last_ticks_ms_) / 1000.0F;
last_ticks_ms_ = NOW;
delta_time = std::min(delta_time, 0.05F);
Mouse::updateCursorVisibility();
Input::get()->update();
current_scene_->update(delta_time);
debug_overlay_->update(delta_time);
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->update(delta_time);
}
Audio::update();
// Si la swapchain no està disponible (finestra minimitzada, etc.),
// saltar-se draw+present aquest frame.
if (!sdl_->clear(0, 0, 0)) {
return SDL_APP_CONTINUE;
}
sdl_->updateRenderingContext();
current_scene_->draw();
debug_overlay_->draw(); // sempre per damunt de l'escena
if (const auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->draw(); // toast: per damunt de tot
}
sdl_->present();
return SDL_APP_CONTINUE;
} }
+32 -14
View File
@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <SDL3/SDL.h>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -15,22 +17,38 @@ namespace System {
class Director { class Director {
public: public:
// `cfg` ha de viure tant com el Director (típicament owned per main). // El Director és el programa: posseeix la configuració (via ConfigYaml)
// `persistence` encapsula init/load/save delegats a la capa concreta // i orquestra tots els subsistemes. main.cpp és pur tràmit que el
// (game/ConfigYaml::*). // construeix i delega cap a SDL.
Director(std::vector<std::string> const& args, Director(int argc, char* argv[]);
Config::EngineConfig& cfg,
Config::ConfigPersistence persistence);
~Director(); ~Director();
// Bucle principal del juego. // Una iteració del bucle: pivot d'escena si cal, delta time, update i
auto run() -> int; // render. Retorna SDL_APP_CONTINUE per seguir, SDL_APP_SUCCESS si vol
// sortir net, SDL_APP_FAILURE si no es pot recuperar.
auto iterate() -> SDL_AppResult;
// Enruta un sol esdeveniment cap a la cadena finestra → globals → F11 →
// escena. Si detecta sortida (ESC doble, QUIT) marca wants_quit_ perquè
// el següent iterate() retorni SDL_APP_SUCCESS.
auto handleEvent(const SDL_Event& event) -> SDL_AppResult;
private: private:
std::string executable_path_; std::string executable_path_;
std::string system_folder_; std::string system_folder_;
Config::EngineConfig* cfg_; Config::EngineConfig* cfg_{nullptr};
Config::ConfigPersistence persistence_;
// Subsistemes que viuen tant com el Director (abans eren locals de run()).
// Preparació per a la migració a SDL_MAIN_USE_CALLBACKS: amb les 4
// callbacks de SDL3 no hi ha un scope que englobi tot el bucle, així
// que cal que aquest estat sigui membre del Director.
std::unique_ptr<SDLManager> sdl_;
std::unique_ptr<SceneManager::SceneContext> context_;
std::unique_ptr<System::DebugOverlay> debug_overlay_;
std::unique_ptr<Scene> current_scene_;
Uint64 last_ticks_ms_{0};
bool wants_quit_{false};
auto checkProgramArguments(std::vector<std::string> const& args) auto checkProgramArguments(std::vector<std::string> const& args)
-> std::string; -> std::string;
@@ -43,8 +61,8 @@ class Director {
SceneManager::SceneContext& context) SceneManager::SceneContext& context)
-> std::unique_ptr<Scene>; -> std::unique_ptr<Scene>;
// Ejecuta el bucle de frames de UNA escena hasta que scene.isFinished() // Pivota a la següent escena: destrueix l'actual, llegeix context_->nextScene()
// sea true. Maneja delta_time, eventos (globales + escena), update y draw. // i construeix la nova. Retorna SDL_APP_SUCCESS si la nova és EXIT o no es pot
// El debug_overlay es global a todas las escenas; el Director lo posee. // construir; SDL_APP_CONTINUE si tot OK.
static void runFrameLoop(Scene& scene, SDLManager& sdl, SceneManager::SceneContext& context, System::DebugOverlay& debug_overlay); auto advanceScene() -> SDL_AppResult;
}; };
+17 -17
View File
@@ -4,52 +4,52 @@
namespace GameConfig { namespace GameConfig {
// Mode de juego // Mode de juego
enum class Mode : std::uint8_t { enum class Mode : std::uint8_t {
NORMAL, // Partida normal NORMAL, // Partida normal
DEMO // Mode demostració (futur) DEMO // Mode demostració (futur)
}; };
// Configuración de una match // Configuración de una match
struct MatchConfig { struct MatchConfig {
bool jugador1_actiu{false}; // Es active el player 1? bool player1_active{false}; // Es active el player 1?
bool jugador2_actiu{false}; // Es active el player 2? bool player2_active{false}; // Es active el player 2?
Mode mode{Mode::NORMAL}; // Mode de juego Mode mode{Mode::NORMAL}; // Mode de juego
// Métodos auxiliars // Métodos auxiliars
// Retorna true si solo hay un player active // Retorna true si solo hay un player active
[[nodiscard]] auto isSinglePlayer() const -> bool { [[nodiscard]] auto isSinglePlayer() const -> bool {
return (jugador1_actiu && !jugador2_actiu) || return (player1_active && !player2_active) ||
(!jugador1_actiu && jugador2_actiu); (!player1_active && player2_active);
} }
// Retorna true si hay dos jugadors active // Retorna true si hay dos jugadors active
[[nodiscard]] auto isCoop() const -> bool { [[nodiscard]] auto isCoop() const -> bool {
return jugador1_actiu && jugador2_actiu; return player1_active && player2_active;
} }
// Retorna true si no hay sin player active // Retorna true si no hay sin player active
[[nodiscard]] auto hasNoPlayers() const -> bool { [[nodiscard]] auto hasNoPlayers() const -> bool {
return !jugador1_actiu && !jugador2_actiu; return !player1_active && !player2_active;
} }
// Compte de jugadors active (0, 1 o 2) // Compte de jugadors active (0, 1 o 2)
[[nodiscard]] auto getPlayerCount() const -> uint8_t { [[nodiscard]] auto getPlayerCount() const -> uint8_t {
return (jugador1_actiu ? 1 : 0) + (jugador2_actiu ? 1 : 0); return (player1_active ? 1 : 0) + (player2_active ? 1 : 0);
} }
// Retorna l'ID de l'únic player active (0 o 1) // Retorna l'ID de l'únic player active (0 o 1)
// Solo vàlid si es_un_jugador() retorna true // Solo vàlid si es_un_jugador() retorna true
[[nodiscard]] auto getSinglePlayerId() const -> uint8_t { [[nodiscard]] auto getSinglePlayerId() const -> uint8_t {
if (jugador1_actiu && !jugador2_actiu) { if (player1_active && !player2_active) {
return 0; return 0;
} }
if (!jugador1_actiu && jugador2_actiu) { if (!player1_active && player2_active) {
return 1; return 1;
} }
return 0; // Fallback (necesario comprovar es_un_jugador() primer) return 0; // Fallback (necesario comprovar es_un_jugador() primer)
} }
}; };
} // namespace GameConfig } // namespace GameConfig
+4 -7
View File
@@ -15,13 +15,10 @@ namespace SceneManager {
public: public:
// Tipo de escena del juego // Tipo de escena del juego
enum class SceneType : std::uint8_t { enum class SceneType : std::uint8_t {
LOGO, // Pantalla de start (logo JAILGAMES) LOGO, // Pantalla de start (logo JAILGAMES)
TITLE, // Pantalla de título (versió 2D actual). Si l'env var TITLE, // Pantalla de título (3D)
// ORNI_TITLE_3D=1 està activa, Director::buildScene GAME, // Juego principal (Asteroids)
// redirigeix aquest valor a TitleScene3D. EXIT // Salir del programa
TITLE_3D, // Pantalla de títol 3D real (variant en proves)
GAME, // Juego principal (Asteroids)
EXIT // Salir del programa
}; };
// Opciones específiques para cada escena // Opciones específiques para cada escena
+3 -3
View File
@@ -3,9 +3,9 @@
// //
// La configuració runtime viu en Config::EngineConfig (core/config/). // La configuració runtime viu en Config::EngineConfig (core/config/).
// Aquest fitxer afegeix una capa de persistència YAML que llegeix i // Aquest fitxer afegeix una capa de persistència YAML que llegeix i
// escriu aquesta struct a disc. La connexió amb el Director es fa via // escriu aquesta struct a disc. El Director crida ConfigYaml::* directament
// Config::ConfigPersistence (lambdes a `main.cpp`), mantenint `core/` // (init / setConfigFile / loadFromFile / saveToFile): la separació
// agnòstic respecte d'aquesta capa. // core/game queda relaxada al Director, que és EL programa, no part del motor.
#pragma once #pragma once
+19 -19
View File
@@ -7,40 +7,40 @@
namespace Constants { namespace Constants {
// Límits de objectes // Límits de objectes
constexpr int MAX_ORNIS = Defaults::Entities::MAX_ORNIS; constexpr int MAX_ORNIS = Defaults::Entities::MAX_ORNIS;
constexpr int MAX_BALES = Defaults::Entities::MAX_BALES; constexpr int MAX_BULLETS = Defaults::Entities::MAX_BULLETS;
// Matemàtiques // Matemàtiques
constexpr float PI = Defaults::Math::PI; constexpr float PI = Defaults::Math::PI;
// Helpers per comprovar límits de zona // Helpers per comprovar límits de zone
inline auto isInPlayArea(float x, float y) -> bool { inline auto isInPlayArea(float x, float y) -> bool {
const SDL_FPoint POINT = {x, y}; const SDL_FPoint POINT = {x, y};
return SDL_PointInRectFloat(&POINT, &Defaults::Zones::PLAYAREA); return SDL_PointInRectFloat(&POINT, &Defaults::Zones::PLAYAREA);
} }
inline void getPlayAreaBounds(float& min_x, float& max_x, float& min_y, float& max_y) { inline void getPlayAreaBounds(float& min_x, float& max_x, float& min_y, float& max_y) {
const auto& zona = Defaults::Zones::PLAYAREA; const auto& zone = Defaults::Zones::PLAYAREA;
min_x = zona.x; min_x = zone.x;
max_x = zona.x + zona.w; max_x = zone.x + zone.w;
min_y = zona.y; min_y = zone.y;
max_y = zona.y + zona.h; max_y = zone.y + zone.h;
} }
// Obtenir límits segurs (compensant radi de l'entidad) // Obtenir límits segurs (compensant radius de l'entidad)
inline void getSafePlayAreaBounds(float radi, float& min_x, float& max_x, float& min_y, float& max_y) { inline void getSafePlayAreaBounds(float radius, float& min_x, float& max_x, float& min_y, float& max_y) {
const auto& zona = Defaults::Zones::PLAYAREA; const auto& zone = Defaults::Zones::PLAYAREA;
constexpr float MARGE_SEGURETAT = 10.0F; // Safety margin constexpr float SAFETY_MARGIN = 10.0F; // Safety margin
min_x = zona.x + radi + MARGE_SEGURETAT; min_x = zone.x + radius + SAFETY_MARGIN;
max_x = zona.x + zona.w - radi - MARGE_SEGURETAT; max_x = zone.x + zone.w - radius - SAFETY_MARGIN;
min_y = zona.y + radi + MARGE_SEGURETAT; min_y = zone.y + radius + SAFETY_MARGIN;
max_y = zona.y + zona.h - radi - MARGE_SEGURETAT; max_y = zone.y + zone.h - radius - SAFETY_MARGIN;
} }
// Obtenir centro de l'àrea de juego // Obtenir centro de l'àrea de juego
inline void getPlayAreaCenter(float& centre_x, float& centre_y) { inline void getPlayAreaCenter(float& center_x, float& center_y) {
const auto& zona = Defaults::Zones::PLAYAREA; const auto& zone = Defaults::Zones::PLAYAREA;
centre_x = zona.x + (zona.w / 2.0F); center_x = zone.x + (zone.w / 2.0F);
centre_y = zona.y + (zona.h / 2.0F); center_y = zone.y + (zone.h / 2.0F);
} }
} // namespace Constants } // namespace Constants
+1 -1
View File
@@ -39,7 +39,7 @@ namespace Effects {
// Política: viu sempre durant min_lifetime, després mor quan // Política: viu sempre durant min_lifetime, després mor quan
// |velocity| < MIN_SPEED_TO_DIE (definit en Defaults). Així els // |velocity| < MIN_SPEED_TO_DIE (definit en Defaults). Així els
// fragments ràpids no "popen" en moviment. // fragments ràpids no "popen" en moviment.
float temps_vida; // Temps transcorregut (segons) float elapsed_time; // Temps transcorregut (segons)
float min_lifetime; // Temps mínim garantit (segons) float min_lifetime; // Temps mínim garantit (segons)
bool active; // Està actiu? bool active; // Està actiu?
+12 -12
View File
@@ -135,7 +135,7 @@ namespace Effects {
float speed = float speed =
velocitat_base + velocitat_base +
(((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) * (((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) *
Defaults::Physics::Debris::VARIACIO_VELOCITAT); Defaults::Physics::Debris::VARIACIO_SPEED);
debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x; debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x;
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y; debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y;
debris->acceleration = friction; debris->acceleration = friction;
@@ -150,7 +150,7 @@ namespace Effects {
// Vida i shrinking — min_lifetime és el temps mínim garantit; després // Vida i shrinking — min_lifetime és el temps mínim garantit; després
// el fragment mor quan |velocity| < MIN_SPEED_TO_DIE. // el fragment mor quan |velocity| < MIN_SPEED_TO_DIE.
debris->temps_vida = 0.0F; debris->elapsed_time = 0.0F;
debris->min_lifetime = lifetime; debris->min_lifetime = lifetime;
debris->factor_shrink = Defaults::Physics::Debris::SHRINK_RATE; debris->factor_shrink = Defaults::Physics::Debris::SHRINK_RATE;
@@ -170,16 +170,16 @@ namespace Effects {
// FASE 1: Aplicar herència i variació // FASE 1: Aplicar herència i variació
float factor_herencia = float factor_herencia =
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN + Defaults::Physics::Debris::INHERITANCE_FACTOR_MIN +
((std::rand() / static_cast<float>(RAND_MAX)) * ((std::rand() / static_cast<float>(RAND_MAX)) *
(Defaults::Physics::Debris::FACTOR_HERENCIA_MAX - (Defaults::Physics::Debris::INHERITANCE_FACTOR_MAX -
Defaults::Physics::Debris::FACTOR_HERENCIA_MIN)); Defaults::Physics::Debris::INHERITANCE_FACTOR_MIN));
float velocitat_ang_heretada = velocitat_angular * factor_herencia; float velocitat_ang_heretada = velocitat_angular * factor_herencia;
float variacio = ((std::rand() / static_cast<float>(RAND_MAX)) * 0.2F) - 0.1F; float variacio = ((std::rand() / static_cast<float>(RAND_MAX)) * 0.2F) - 0.1F;
velocitat_ang_heretada *= (1.0F + variacio); velocitat_ang_heretada *= (1.0F + variacio);
// FASE 2: Cap a la velocity màxima; l'excés es converteix en tangencial // FASE 2: Cap a la velocity màxima; l'excés es converteix en tangencial
constexpr float CAP = Defaults::Physics::Debris::VELOCITAT_ROT_MAX; constexpr float CAP = Defaults::Physics::Debris::SPEED_ROT_MAX;
float abs_ang = std::abs(velocitat_ang_heretada); float abs_ang = std::abs(velocitat_ang_heretada);
float sign_ang = (velocitat_ang_heretada >= 0.0F) ? 1.0F : -1.0F; float sign_ang = (velocitat_ang_heretada >= 0.0F) ? 1.0F : -1.0F;
@@ -213,10 +213,10 @@ namespace Effects {
// Rotación visual aleatòria (factor = 0.0 o sin velocidad angular) // Rotación visual aleatòria (factor = 0.0 o sin velocidad angular)
debris.velocitat_rot_visual = debris.velocitat_rot_visual =
Defaults::Physics::Debris::ROTACIO_MIN + Defaults::Physics::Debris::ROTATION_MIN +
((std::rand() / static_cast<float>(RAND_MAX)) * ((std::rand() / static_cast<float>(RAND_MAX)) *
(Defaults::Physics::Debris::ROTACIO_MAX - (Defaults::Physics::Debris::ROTATION_MAX -
Defaults::Physics::Debris::ROTACIO_MIN)); Defaults::Physics::Debris::ROTATION_MIN));
// 50% probabilitat de rotación en sentit contrari // 50% probabilitat de rotación en sentit contrari
if (std::rand() % 2 == 0) { if (std::rand() % 2 == 0) {
@@ -266,12 +266,12 @@ namespace Effects {
} }
// 1. Actualitzar time de vida // 1. Actualitzar time de vida
debris.temps_vida += delta_time; debris.elapsed_time += delta_time;
// Política de mort: viu sí o sí durant min_lifetime; després mor // Política de mort: viu sí o sí durant min_lifetime; després mor
// quan la velocity cau per sota d'un llindar. Així els fragments // quan la velocity cau per sota d'un llindar. Així els fragments
// ràpids no desapareixen en moviment. // ràpids no desapareixen en moviment.
if (debris.temps_vida >= debris.min_lifetime) { if (debris.elapsed_time >= debris.min_lifetime) {
const float SPEED_SQ = (debris.velocity.x * debris.velocity.x) + const float SPEED_SQ = (debris.velocity.x * debris.velocity.x) +
(debris.velocity.y * debris.velocity.y); (debris.velocity.y * debris.velocity.y);
if (SPEED_SQ < Defaults::Physics::Debris::MIN_SPEED_TO_DIE_SQ) { if (SPEED_SQ < Defaults::Physics::Debris::MIN_SPEED_TO_DIE_SQ) {
@@ -344,7 +344,7 @@ namespace Effects {
// 6. Shrink lineal sobre la longitud ORIGINAL (no iteratiu). // 6. Shrink lineal sobre la longitud ORIGINAL (no iteratiu).
// SHRINK_T va de 0 a 1 al llarg de min_lifetime; després queda // SHRINK_T va de 0 a 1 al llarg de min_lifetime; després queda
// a 1 i el shrink_factor manté el valor mínim (1 - factor_shrink). // a 1 i el shrink_factor manté el valor mínim (1 - factor_shrink).
const float SHRINK_T = std::min(debris.temps_vida / debris.min_lifetime, 1.0F); const float SHRINK_T = std::min(debris.elapsed_time / debris.min_lifetime, 1.0F);
const float SHRINK_FACTOR = std::max(0.0F, 1.0F - (debris.factor_shrink * SHRINK_T)); const float SHRINK_FACTOR = std::max(0.0F, 1.0F - (debris.factor_shrink * SHRINK_T));
// 7. Reconstruir p1/p2 des de la geometria autoritaritzada: // 7. Reconstruir p1/p2 des de la geometria autoritaritzada:
+7 -2
View File
@@ -16,7 +16,7 @@ namespace Effects {
// tail = head velocity_normalitzada × current_length. // tail = head velocity_normalitzada × current_length.
// //
// Cicle de vida: // Cicle de vida:
// Fase 1 (temps_vida < grow_duration): current_length creix linealment // Fase 1 (elapsed_time < grow_duration): current_length creix linealment
// de 0 a max_length. Brillor al màxim. // de 0 a max_length. Brillor al màxim.
// Fase 2: current_length = max_length × (speed/initial_speed) i brillor // Fase 2: current_length = max_length × (speed/initial_speed) i brillor
// amb la mateixa proporció. Mor quan length o brightness cauen sota // amb la mateixa proporció. Mor quan length o brightness cauen sota
@@ -30,11 +30,16 @@ namespace Effects {
float max_length; // Longitud màxima (final de la fase de creixement) float max_length; // Longitud màxima (final de la fase de creixement)
float grow_duration; // Temps de creixement de 0 a max_length (s) float grow_duration; // Temps de creixement de 0 a max_length (s)
float temps_vida; // Acumulador (s) float elapsed_time; // Acumulador (s)
float initial_speed; // Speed inicial per a la proporció de fase 2 float initial_speed; // Speed inicial per a la proporció de fase 2
float brightness; // 0..1 float brightness; // 0..1
SDL_Color color{}; // alpha==0 → oscilador global SDL_Color color{}; // alpha==0 → oscilador global
// Halo neon (off per defecte). Si glow_color.a > 0, el halo usa
// glow_color (línia blanca + halo daurat, p.ex.); si alpha==0, el
// halo agafa el color de la línia.
bool glow{false};
SDL_Color glow_color{};
bool active; bool active;
}; };
+32 -16
View File
@@ -61,18 +61,20 @@ namespace Effects {
} }
} }
void FireworkManager::spawn(const Vec2& origen, void FireworkManager::spawn(const Vec2& origin,
SDL_Color color, SDL_Color color,
float initial_speed, float initial_speed,
int n_points, int n_points,
float initial_brightness) { float initial_brightness,
bool glow,
SDL_Color glow_color) {
if (n_points <= 0) { if (n_points <= 0) {
return; return;
} }
// Notificar als subscriptors (playfield pulses, etc.). // Notificar als subscriptors (playfield pulses, etc.).
if (spawn_callback_) { if (spawn_callback_) {
spawn_callback_(origen); spawn_callback_(origin);
} }
const float ANGLE_STEP = 2.0F * Defaults::Math::PI / static_cast<float>(n_points); const float ANGLE_STEP = 2.0F * Defaults::Math::PI / static_cast<float>(n_points);
@@ -92,7 +94,7 @@ namespace Effects {
const float SPEED = const float SPEED =
initial_speed + (randSigned() * Defaults::FX::Firework::SPEED_VARIATION); initial_speed + (randSigned() * Defaults::FX::Firework::SPEED_VARIATION);
fw->head = origen; fw->head = origin;
fw->velocity = {.x = std::cos(ANGLE) * SPEED, .y = std::sin(ANGLE) * SPEED}; fw->velocity = {.x = std::cos(ANGLE) * SPEED, .y = std::sin(ANGLE) * SPEED};
fw->acceleration = Defaults::FX::Firework::FRICTION; fw->acceleration = Defaults::FX::Firework::FRICTION;
@@ -100,11 +102,13 @@ namespace Effects {
fw->max_length = Defaults::FX::Firework::MAX_LENGTH; fw->max_length = Defaults::FX::Firework::MAX_LENGTH;
fw->grow_duration = Defaults::FX::Firework::GROW_DURATION; fw->grow_duration = Defaults::FX::Firework::GROW_DURATION;
fw->temps_vida = 0.0F; fw->elapsed_time = 0.0F;
fw->initial_speed = SPEED; fw->initial_speed = SPEED;
fw->brightness = initial_brightness; fw->brightness = initial_brightness;
fw->color = color; fw->color = color;
fw->glow = glow;
fw->glow_color = glow_color;
fw->active = true; fw->active = true;
} }
} }
@@ -115,7 +119,7 @@ namespace Effects {
continue; continue;
} }
fw.temps_vida += delta_time; fw.elapsed_time += delta_time;
// 1. Fricció lineal (aplicar en la direcció del movement). // 1. Fricció lineal (aplicar en la direcció del movement).
const float SPEED = std::sqrt( const float SPEED = std::sqrt(
@@ -140,9 +144,9 @@ namespace Effects {
bounceOffPlayArea(fw.head, fw.velocity); bounceOffPlayArea(fw.head, fw.velocity);
// 4. Calcular longitud i brillor segons fase. // 4. Calcular longitud i brillor segons fase.
if (fw.temps_vida < fw.grow_duration) { if (fw.elapsed_time < fw.grow_duration) {
// Fase 1: creixement lineal de 0 a max_length. // Fase 1: creixement lineal de 0 a max_length.
const float T = fw.temps_vida / fw.grow_duration; const float T = fw.elapsed_time / fw.grow_duration;
fw.current_length = fw.max_length * T; fw.current_length = fw.max_length * T;
fw.brightness = Defaults::FX::Firework::INITIAL_BRIGHTNESS; fw.brightness = Defaults::FX::Firework::INITIAL_BRIGHTNESS;
} else { } else {
@@ -185,14 +189,26 @@ namespace Effects {
.y = fw.head.y - (DIR_Y * fw.current_length), .y = fw.head.y - (DIR_Y * fw.current_length),
}; };
Rendering::linea(renderer_, if (fw.glow) {
static_cast<int>(fw.head.x), Rendering::lineaGlow(renderer_,
static_cast<int>(fw.head.y), static_cast<int>(fw.head.x),
static_cast<int>(TAIL.x), static_cast<int>(fw.head.y),
static_cast<int>(TAIL.y), static_cast<int>(TAIL.x),
fw.brightness, static_cast<int>(TAIL.y),
0.0F, fw.brightness,
fw.color); 0.0F,
fw.color,
fw.glow_color);
} else {
Rendering::linea(renderer_,
static_cast<int>(fw.head.x),
static_cast<int>(fw.head.y),
static_cast<int>(TAIL.x),
static_cast<int>(TAIL.y),
fw.brightness,
0.0F,
fw.color);
}
} }
} }
+7 -3
View File
@@ -21,7 +21,7 @@ namespace Effects {
class FireworkManager { class FireworkManager {
public: public:
// Notificació opcional cada vegada que es genera un burst. // Notificació opcional cada vegada que es genera un burst.
using SpawnCallback = std::function<void(Vec2 origen)>; using SpawnCallback = std::function<void(Vec2 origin)>;
explicit FireworkManager(Rendering::Renderer* renderer); explicit FireworkManager(Rendering::Renderer* renderer);
@@ -35,11 +35,15 @@ namespace Effects {
// initial_speed: velocitat radial inicial (px/s). // initial_speed: velocitat radial inicial (px/s).
// n_points: nombre de línies. Default Defaults::FX::Firework::N_POINTS. // n_points: nombre de línies. Default Defaults::FX::Firework::N_POINTS.
// initial_brightness: 0..1. // initial_brightness: 0..1.
void spawn(const Vec2& origen, // glow: si true, cada partícula es renderitza amb halo neon.
// glow_color: color del halo. Si alpha==0, agafa el color de la línia.
void spawn(const Vec2& origin,
SDL_Color color = Defaults::FX::Firework::DEFAULT_COLOR, SDL_Color color = Defaults::FX::Firework::DEFAULT_COLOR,
float initial_speed = Defaults::FX::Firework::SPEED, float initial_speed = Defaults::FX::Firework::SPEED,
int n_points = Defaults::FX::Firework::N_POINTS, int n_points = Defaults::FX::Firework::N_POINTS,
float initial_brightness = Defaults::FX::Firework::INITIAL_BRIGHTNESS); float initial_brightness = Defaults::FX::Firework::INITIAL_BRIGHTNESS,
bool glow = false,
SDL_Color glow_color = {0, 0, 0, 0});
void update(float delta_time); void update(float delta_time);
void draw() const; void draw() const;
+7 -7
View File
@@ -9,9 +9,9 @@
namespace Effects { namespace Effects {
// FloatingScore: text animat que muestra points guanyats // FloatingScore: text animat que muestra points guanyats
// S'activa cuando es destrueix un enemy i s'esvaeix después de un time // S'activa cuando es destrueix un enemy i s'esvaeix después de un time
struct FloatingScore { struct FloatingScore {
// Text a mostrar (e.g., "100", "150", "200") // Text a mostrar (e.g., "100", "150", "200")
std::string text; std::string text;
@@ -22,12 +22,12 @@ struct FloatingScore {
Vec2 velocity; // px/s (normalment sin amunt: {0.0f, -30.0f}) Vec2 velocity; // px/s (normalment sin amunt: {0.0f, -30.0f})
// Animación de fade // Animación de fade
float temps_vida; // Temps transcorregut (segons) float elapsed_time; // Temps transcorregut (segons)
float temps_max; // Temps de vida màxim (segons) float max_lifetime; // Temps de vida màxim (segons)
float brightness; // Brillantor calculada (0.0-1.0) float brightness; // Brillantor calculada (0.0-1.0)
// Estat // Estat
bool active; bool active;
}; };
} // namespace Effects } // namespace Effects
+74 -74
View File
@@ -7,93 +7,93 @@
namespace Effects { namespace Effects {
FloatingScoreManager::FloatingScoreManager(Rendering::Renderer* renderer) FloatingScoreManager::FloatingScoreManager(Rendering::Renderer* renderer)
: text_(renderer) { : text_(renderer) {
// Inicialitzar todos los slots como inactius // Inicialitzar todos los slots como inactius
for (auto& pf : pool_) { for (auto& pf : pool_) {
pf.active = false;
}
}
void FloatingScoreManager::crear(int points, const Vec2& position) {
// 1. Trobar slot lliure
FloatingScore* pf = findFreeSlot();
if (pf == nullptr) {
return; // Pool ple (improbable)
}
// 2. Inicialitzar puntuación flotante
pf->text = std::to_string(points);
pf->position = position;
pf->velocity = {.x = Defaults::FloatingScore::VELOCITY_X,
.y = Defaults::FloatingScore::VELOCITY_Y};
pf->temps_vida = 0.0F;
pf->temps_max = Defaults::FloatingScore::LIFETIME;
pf->brightness = 1.0F;
pf->active = true;
}
void FloatingScoreManager::update(float delta_time) {
for (auto& pf : pool_) {
if (!pf.active) {
continue;
}
// 1. Actualitzar posición (deriva sin amunt)
pf.position.x += pf.velocity.x * delta_time;
pf.position.y += pf.velocity.y * delta_time;
// 2. Actualitzar time de vida
pf.temps_vida += delta_time;
// 3. Calcular brightness (fade lineal)
float progress = pf.temps_vida / pf.temps_max; // 0.0 → 1.0
pf.brightness = 1.0F - progress; // 1.0 → 0.0
// 4. Desactivar cuando acaba el time
if (pf.temps_vida >= pf.temps_max) {
pf.active = false; pf.active = false;
} }
} }
}
void FloatingScoreManager::draw() { void FloatingScoreManager::crear(int points, const Vec2& position) {
for (const auto& pf : pool_) { // 1. Trobar slot lliure
if (!pf.active) { FloatingScore* pf = findFreeSlot();
continue; if (pf == nullptr) {
return; // Pool ple (improbable)
} }
// Renderizar centrat con brightness (fade) // 2. Inicialitzar puntuación flotante
constexpr float SCALE = Defaults::FloatingScore::SCALE; pf->text = std::to_string(points);
constexpr float SPACING = Defaults::FloatingScore::SPACING; pf->position = position;
pf->velocity = {.x = Defaults::FloatingScore::VELOCITY_X,
text_.renderCentered(pf.text, pf.position, SCALE, SPACING, pf.brightness); .y = Defaults::FloatingScore::VELOCITY_Y};
pf->elapsed_time = 0.0F;
pf->max_lifetime = Defaults::FloatingScore::LIFETIME;
pf->brightness = 1.0F;
pf->active = true;
} }
}
void FloatingScoreManager::reset() { void FloatingScoreManager::update(float delta_time) {
for (auto& pf : pool_) { for (auto& pf : pool_) {
pf.active = false; if (!pf.active) {
} continue;
} }
auto FloatingScoreManager::getActiveCount() const -> int { // 1. Actualitzar posición (deriva sin amunt)
int count = 0; pf.position.x += pf.velocity.x * delta_time;
for (const auto& pf : pool_) { pf.position.y += pf.velocity.y * delta_time;
if (pf.active) {
count++; // 2. Actualitzar time de vida
pf.elapsed_time += delta_time;
// 3. Calcular brightness (fade lineal)
float progress = pf.elapsed_time / pf.max_lifetime; // 0.0 → 1.0
pf.brightness = 1.0F - progress; // 1.0 → 0.0
// 4. Desactivar cuando acaba el time
if (pf.elapsed_time >= pf.max_lifetime) {
pf.active = false;
}
} }
} }
return count;
}
auto FloatingScoreManager::findFreeSlot() -> FloatingScore* { void FloatingScoreManager::draw() {
for (auto& pf : pool_) { for (const auto& pf : pool_) {
if (!pf.active) { if (!pf.active) {
return &pf; continue;
}
// Renderizar centrat con brightness (fade)
constexpr float SCALE = Defaults::FloatingScore::SCALE;
constexpr float SPACING = Defaults::FloatingScore::SPACING;
text_.renderCentered(pf.text, pf.position, SCALE, SPACING, pf.brightness);
} }
} }
return nullptr; // Pool ple
} void FloatingScoreManager::reset() {
for (auto& pf : pool_) {
pf.active = false;
}
}
auto FloatingScoreManager::getActiveCount() const -> int {
int count = 0;
for (const auto& pf : pool_) {
if (pf.active) {
count++;
}
}
return count;
}
auto FloatingScoreManager::findFreeSlot() -> FloatingScore* {
for (auto& pf : pool_) {
if (!pf.active) {
return &pf;
}
}
return nullptr; // Pool ple
}
} // namespace Effects } // namespace Effects
+12 -34
View File
@@ -3,7 +3,6 @@
#include "game/entities/bullet.hpp" #include "game/entities/bullet.hpp"
#include <algorithm>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <iostream> #include <iostream>
@@ -43,8 +42,8 @@ void Bullet::init() {
// Inicialment inactiva // Inicialment inactiva
is_active_ = false; is_active_ = false;
center_ = {.x = 0.0F, .y = 0.0F}; center_ = {.x = 0.0F, .y = 0.0F};
prev_position_ = {.x = 0.0F, .y = 0.0F};
angle_ = 0.0F; angle_ = 0.0F;
grace_timer_ = 0.0F;
// Reset del cuerpo físico // Reset del cuerpo físico
body_.position = Vec2{}; body_.position = Vec2{};
@@ -54,18 +53,16 @@ void Bullet::init() {
body_.clearAccumulators(); body_.clearAccumulators();
} }
void Bullet::disparar(const Vec2& position, float angle, uint8_t owner_id) { void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id) {
// Activar bullet // Activar bullet
is_active_ = true; is_active_ = true;
// Almacenar propietario (0=P1, 1=P2) // Almacenar propietario (0=P1, 1=P2)
owner_id_ = owner_id; owner_id_ = owner_id;
// Activar grace period (prevents instant self-collision)
grace_timer_ = Defaults::Game::BULLET_GRACE_PERIOD;
// Posición y orientación iniciales = ship // Posición y orientación iniciales = ship
center_ = position; center_ = position;
prev_position_ = position; // Al spawn no hi ha moviment encara: swept degenera a punt-cercle
angle_ = angle; angle_ = angle;
// Sincronizar el body físico: posición + velocidad cartesiana // Sincronizar el body físico: posición + velocidad cartesiana
@@ -82,37 +79,18 @@ void Bullet::disparar(const Vec2& position, float angle, uint8_t owner_id) {
Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME); Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME);
} }
void Bullet::update(float delta_time) { void Bullet::update(float /*delta_time*/) {
if (!is_active_) { // No-op: la desactivació per fora-de-zone viu a
return; // Systems::Collision::desactivateOutOfBoundsBullets() perquè així té accés
} // al DebrisManager i pot generar el "trencament" visual de la bala alhora.
// El moviment l'integra PhysicsWorld; postUpdate sincronitza center_ i prev_position_.
// Decrementar grace timer
if (grace_timer_ > 0.0F) {
grace_timer_ -= delta_time;
grace_timer_ = std::max(grace_timer_, 0.0F);
}
// El movimiento real lo hace PhysicsWorld::update() (integración).
// Aquí solo lógica de estado: detectar salida del PLAYAREA y desactivar.
float min_x;
float max_x;
float min_y;
float max_y;
Constants::getSafePlayAreaBounds(Defaults::Entities::BULLET_RADIUS,
min_x,
max_x,
min_y,
max_y);
if (body_.position.x < min_x || body_.position.x > max_x ||
body_.position.y < min_y || body_.position.y > max_y) {
desactivar();
}
} }
void Bullet::postUpdate(float /*delta_time*/) { void Bullet::postUpdate(float /*delta_time*/) {
// Sincronizar mirror desde body_ tras la integración del world. // Captura la posició al final del frame anterior abans de sobreescriure center_;
// així el sistema de col·lisions pot fer swept (segment-vs-cercle) entre prev_position_
// i la nova center_, evitant tunneling a velocitats altes.
prev_position_ = center_;
center_ = body_.position; center_ = body_.position;
// angle_ no cambia (las balas no rotan visualmente). // angle_ no cambia (las balas no rotan visualmente).
} }
+30 -29
View File
@@ -11,38 +11,39 @@
#include "core/types.hpp" #include "core/types.hpp"
class Bullet : public Entities::Entity { class Bullet : public Entities::Entity {
public: public:
Bullet() Bullet()
: Entity(nullptr) {} : Entity(nullptr) {}
explicit Bullet(Rendering::Renderer* renderer); explicit Bullet(Rendering::Renderer* renderer);
void init() override; void init() override;
void disparar(const Vec2& position, float angle, uint8_t owner_id); void fire(const Vec2& position, float angle, uint8_t owner_id);
void update(float delta_time) override; void update(float delta_time) override;
void postUpdate(float delta_time) override; void postUpdate(float delta_time) override;
void draw() const override; void draw() const override;
// Override: Interfaz de Entity // Override: Interfaz de Entity
[[nodiscard]] auto isActive() const -> bool override { return is_active_; } [[nodiscard]] auto isActive() const -> bool override { return is_active_; }
// Override: Interfaz de colisión (gameplay-level: PLAYAREA bounds-check) // Override: Interfaz de colisión (gameplay-level: PLAYAREA bounds-check)
[[nodiscard]] auto getCollisionRadius() const -> float override { [[nodiscard]] auto getCollisionRadius() const -> float override {
return Defaults::Entities::BULLET_RADIUS; return Defaults::Entities::BULLET_RADIUS;
} }
[[nodiscard]] auto isCollidable() const -> bool override { [[nodiscard]] auto isCollidable() const -> bool override {
return is_active_ && grace_timer_ <= 0.0F; return is_active_;
} }
// Getters (API pública sin cambios) // Getters (API pública sin cambios)
[[nodiscard]] auto getOwnerId() const -> uint8_t { return owner_id_; } [[nodiscard]] auto getOwnerId() const -> uint8_t { return owner_id_; }
[[nodiscard]] auto getGraceTimer() const -> float { return grace_timer_; } // Posició al final del frame anterior, per a CCD segment-vs-cercle.
void desactivar(); [[nodiscard]] auto getPrevPosition() const -> const Vec2& { return prev_position_; }
void desactivar();
private: private:
// Miembros específicos de Bullet (heredados: renderer_, shape_, center_, angle_, brightness_, body_). // Miembros específicos de Bullet (heredados: renderer_, shape_, center_, angle_, brightness_, body_).
// Inicializados en la declaración para que tanto el ctor por defecto como el que toma renderer // Inicializados en la declaración para que tanto el ctor por defecto como el que toma renderer
// dejen el objeto en estado coherente (proyectil inactivo, sin owner, sin grace timer). // dejen el objeto en estado coherente (proyectil inactivo, sin owner).
bool is_active_{false}; bool is_active_{false};
uint8_t owner_id_{0}; // 0=P1, 1=P2 uint8_t owner_id_{0}; // 0=P1, 1=P2
float grace_timer_{0.0F}; // Grace period timer (0.0 = vulnerable) Vec2 prev_position_{}; // Posició al final del frame anterior (per a swept collision)
}; };
+112 -112
View File
@@ -8,7 +8,6 @@
#include <cstdlib> #include <cstdlib>
#include <iostream> #include <iostream>
#include "core/audio/audio.hpp"
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/graphics/shape_loader.hpp" #include "core/graphics/shape_loader.hpp"
@@ -42,7 +41,7 @@ namespace {
Enemy::Enemy(Rendering::Renderer* renderer) Enemy::Enemy(Rendering::Renderer* renderer)
: Entity(renderer), : Entity(renderer),
tracking_strength_(Defaults::Enemies::Cuadrado::TRACKING_STRENGTH) { tracking_strength_(Defaults::Enemies::Square::TRACKING_STRENGTH) {
brightness_ = Defaults::Brightness::ENEMIC; brightness_ = Defaults::Brightness::ENEMIC;
// Configuración del cuerpo físico — defaults para enemy genérico. // Configuración del cuerpo físico — defaults para enemy genérico.
@@ -59,43 +58,43 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
const char* shape_file = nullptr; const char* shape_file = nullptr;
float base_speed = 0.0F; float base_speed = 0.0F;
float drotacio_min = 0.0F; float rotation_delta_min = 0.0F;
float drotacio_max = 0.0F; float rotation_delta_max = 0.0F;
float type_mass = Defaults::Enemies::Body::DEFAULT_MASS; float type_mass = Defaults::Enemies::Body::DEFAULT_MASS;
switch (type_) { switch (type_) {
case EnemyType::PENTAGON: case EnemyType::PENTAGON:
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE; shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
base_speed = Defaults::Enemies::Pentagon::VELOCITAT; base_speed = Defaults::Enemies::Pentagon::SPEED;
drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN; rotation_delta_min = Defaults::Enemies::Pentagon::ROTATION_DELTA_MIN;
drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX; rotation_delta_max = Defaults::Enemies::Pentagon::ROTATION_DELTA_MAX;
type_mass = Defaults::Enemies::Pentagon::MASS; type_mass = Defaults::Enemies::Pentagon::MASS;
break; break;
case EnemyType::QUADRAT: case EnemyType::SQUARE:
shape_file = Defaults::Enemies::Cuadrado::SHAPE_FILE; shape_file = Defaults::Enemies::Square::SHAPE_FILE;
base_speed = Defaults::Enemies::Cuadrado::VELOCITAT; base_speed = Defaults::Enemies::Square::SPEED;
drotacio_min = Defaults::Enemies::Cuadrado::DROTACIO_MIN; rotation_delta_min = Defaults::Enemies::Square::ROTATION_DELTA_MIN;
drotacio_max = Defaults::Enemies::Cuadrado::DROTACIO_MAX; rotation_delta_max = Defaults::Enemies::Square::ROTATION_DELTA_MAX;
type_mass = Defaults::Enemies::Cuadrado::MASS; type_mass = Defaults::Enemies::Square::MASS;
tracking_timer_ = 0.0F; tracking_timer_ = 0.0F;
break; break;
case EnemyType::MOLINILLO: case EnemyType::PINWHEEL:
shape_file = Defaults::Enemies::Molinillo::SHAPE_FILE; shape_file = Defaults::Enemies::Pinwheel::SHAPE_FILE;
base_speed = Defaults::Enemies::Molinillo::VELOCITAT; base_speed = Defaults::Enemies::Pinwheel::SPEED;
drotacio_min = Defaults::Enemies::Molinillo::DROTACIO_MIN; rotation_delta_min = Defaults::Enemies::Pinwheel::ROTATION_DELTA_MIN;
drotacio_max = Defaults::Enemies::Molinillo::DROTACIO_MAX; rotation_delta_max = Defaults::Enemies::Pinwheel::ROTATION_DELTA_MAX;
type_mass = Defaults::Enemies::Molinillo::MASS; type_mass = Defaults::Enemies::Pinwheel::MASS;
break; break;
default: default:
std::cerr << "[Enemy] Error: tipo desconocido (" std::cerr << "[Enemy] Error: tipo desconocido ("
<< static_cast<int>(type_) << "), usando PENTAGON\n"; << static_cast<int>(type_) << "), usando PENTAGON\n";
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE; shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
base_speed = Defaults::Enemies::Pentagon::VELOCITAT; base_speed = Defaults::Enemies::Pentagon::SPEED;
drotacio_min = Defaults::Enemies::Pentagon::DROTACIO_MIN; rotation_delta_min = Defaults::Enemies::Pentagon::ROTATION_DELTA_MIN;
drotacio_max = Defaults::Enemies::Pentagon::DROTACIO_MAX; rotation_delta_max = Defaults::Enemies::Pentagon::ROTATION_DELTA_MAX;
break; break;
} }
@@ -132,7 +131,7 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
const int RANGE_Y = static_cast<int>(max_y - min_y); const int RANGE_Y = static_cast<int>(max_y - min_y);
center_.x = static_cast<float>((std::rand() % RANGE_X) + static_cast<int>(min_x)); center_.x = static_cast<float>((std::rand() % RANGE_X) + static_cast<int>(min_x));
center_.y = static_cast<float>((std::rand() % RANGE_Y) + static_cast<int>(min_y)); center_.y = static_cast<float>((std::rand() % RANGE_Y) + static_cast<int>(min_y));
std::cout << "[Enemy] Advertencia: spawn sin zona segura tras " std::cout << "[Enemy] Advertencia: spawn sin zone segura tras "
<< Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS << " intentos\n"; << Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS << " intentos\n";
} }
} else { } else {
@@ -153,28 +152,28 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
body_.clearAccumulators(); body_.clearAccumulators();
// Rotación visual aleatoria (independiente del body) // Rotación visual aleatoria (independiente del body)
const float DROTACIO_RANGE = drotacio_max - drotacio_min; const float ROTATION_DELTA_RANGE = rotation_delta_max - rotation_delta_min;
drotacio_ = drotacio_min + ((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DROTACIO_RANGE); rotation_delta_ = rotation_delta_min + ((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * ROTATION_DELTA_RANGE);
rotacio_ = 0.0F; rotation_ = 0.0F;
// Estado de animación // Estado de animación
animacio_ = EnemyAnimation(); animation_ = EnemyAnimation();
animacio_.drotacio_base = drotacio_; animation_.rotation_delta_base = rotation_delta_;
animacio_.drotacio_objetivo = drotacio_; animation_.rotation_delta_target = rotation_delta_;
animacio_.drotacio_t = 1.0F; animation_.rotation_delta_t = 1.0F;
// Invulnerabilidad post-spawn // Invulnerabilidad post-spawn
timer_invulnerabilitat_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; invulnerability_timer_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;
// Timer para próximo cambio de dirección (Pentagon) // Timer para próximo cambio de dirección (Pentagon)
direction_change_timer_ = 0.0F; direction_change_timer_ = 0.0F;
esta_ = true; is_active_ = true;
} }
void Enemy::update(float delta_time) { void Enemy::update(float delta_time) {
if (!esta_) { if (!is_active_) {
return; return;
} }
@@ -190,11 +189,11 @@ void Enemy::update(float delta_time) {
} }
// Decremento de invulnerabilidad + LERP de brightness // Decremento de invulnerabilidad + LERP de brightness
if (timer_invulnerabilitat_ > 0.0F) { if (invulnerability_timer_ > 0.0F) {
timer_invulnerabilitat_ -= delta_time; invulnerability_timer_ -= delta_time;
timer_invulnerabilitat_ = std::max(timer_invulnerabilitat_, 0.0F); invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F);
const float T_INV = timer_invulnerabilitat_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; const float T_INV = invulnerability_timer_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
const float T = 1.0F - T_INV; const float T = 1.0F - T_INV;
const float SMOOTH_T = T * T * (3.0F - (2.0F * T)); const float SMOOTH_T = T * T * (3.0F - (2.0F * T));
constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;
@@ -210,11 +209,11 @@ void Enemy::update(float delta_time) {
case EnemyType::PENTAGON: case EnemyType::PENTAGON:
behaviorPentagon(delta_time); behaviorPentagon(delta_time);
break; break;
case EnemyType::QUADRAT: case EnemyType::SQUARE:
behaviorQuadrat(delta_time); behaviorSquare(delta_time);
break; break;
case EnemyType::MOLINILLO: case EnemyType::PINWHEEL:
behaviorMolinillo(delta_time); behaviorPinwheel(delta_time);
break; break;
} }
} }
@@ -223,18 +222,18 @@ void Enemy::update(float delta_time) {
updateAnimation(delta_time); updateAnimation(delta_time);
// Rotación visual (decoración, no afecta movimiento) // Rotación visual (decoración, no afecta movimiento)
rotacio_ += drotacio_ * delta_time; rotation_ += rotation_delta_ * delta_time;
} }
void Enemy::postUpdate(float /*delta_time*/) { void Enemy::postUpdate(float /*delta_time*/) {
// Sincronizar mirror tras la integración del world. // Sincronizar mirror tras la integración del world.
if (esta_) { if (is_active_) {
center_ = body_.position; center_ = body_.position;
} }
} }
void Enemy::draw() const { void Enemy::draw() const {
if (!esta_ || !shape_) { if (!is_active_ || !shape_) {
return; return;
} }
const float SCALE = computeCurrentScale(); const float SCALE = computeCurrentScale();
@@ -243,11 +242,11 @@ void Enemy::draw() const {
case EnemyType::PENTAGON: case EnemyType::PENTAGON:
color = Defaults::Palette::PENTAGON; color = Defaults::Palette::PENTAGON;
break; break;
case EnemyType::QUADRAT: case EnemyType::SQUARE:
color = Defaults::Palette::QUADRAT; color = Defaults::Palette::SQUARE;
break; break;
case EnemyType::MOLINILLO: case EnemyType::PINWHEEL:
color = Defaults::Palette::MOLINILLO; color = Defaults::Palette::PINWHEEL;
break; break;
} }
@@ -261,11 +260,11 @@ void Enemy::draw() const {
} }
} }
Rendering::renderShape(renderer_, shape_, center_, rotacio_, SCALE, 1.0F, brightness_, color); Rendering::renderShape(renderer_, shape_, center_, rotation_, SCALE, 1.0F, brightness_, color);
} }
void Enemy::destruir() { void Enemy::destroy() {
esta_ = false; is_active_ = false;
body_.velocity = Vec2{}; body_.velocity = Vec2{};
body_.angular_velocity = 0.0F; body_.angular_velocity = 0.0F;
body_.radius = 0.0F; // No colisiona mientras está inactivo body_.radius = 0.0F; // No colisiona mientras está inactivo
@@ -274,10 +273,11 @@ void Enemy::destruir() {
last_hit_by_ = 0xFF; last_hit_by_ = 0xFF;
} }
void Enemy::herir(uint8_t shooter_id) { void Enemy::hurt(uint8_t shooter_id) {
wounded_timer_ = Defaults::Enemies::Wounded::DURATION; wounded_timer_ = Defaults::Enemies::Wounded::DURATION;
last_hit_by_ = shooter_id; last_hit_by_ = shooter_id;
Audio::get()->playSound(Defaults::Sound::HIT, Audio::Group::GAME); // El so HIT ara el reprodueix la bala quan es trenca en debris
// (Systems::Collision::breakBullet), no l'enemic en entrar a HURT.
} }
void Enemy::applyImpulse(const Vec2& impulse) { void Enemy::applyImpulse(const Vec2& impulse) {
@@ -312,7 +312,7 @@ void Enemy::behaviorPentagon(float delta_time) {
if (RAND_VAL < Defaults::Enemies::Pentagon::ZIGZAG_PROB_PER_SECOND * delta_time) { if (RAND_VAL < Defaults::Enemies::Pentagon::ZIGZAG_PROB_PER_SECOND * delta_time) {
const float CURRENT_ANGLE = velocityToAngle(body_.velocity); const float CURRENT_ANGLE = velocityToAngle(body_.velocity);
const float DELTA = (static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * const float DELTA = (static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) *
Defaults::Enemies::Pentagon::CANVI_ANGLE_MAX; Defaults::Enemies::Pentagon::ANGLE_CHANGE_MAX;
const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA); const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA);
const float SPEED = body_.velocity.length(); const float SPEED = body_.velocity.length();
setVelocityFromAngle(NEW_ANGLE, SPEED); setVelocityFromAngle(NEW_ANGLE, SPEED);
@@ -320,12 +320,12 @@ void Enemy::behaviorPentagon(float delta_time) {
} }
} }
// QUADRAT: tracking discreto cada TRACKING_INTERVAL. Ajusta dirección // SQUARE: tracking discreto cada TRACKING_INTERVAL. Ajusta dirección
// hacia el ship mezclando con tracking_strength_. // hacia el ship mezclando con tracking_strength_.
void Enemy::behaviorQuadrat(float delta_time) { void Enemy::behaviorSquare(float delta_time) {
tracking_timer_ += delta_time; tracking_timer_ += delta_time;
if (tracking_timer_ >= Defaults::Enemies::Cuadrado::TRACKING_INTERVAL && ship_position_ != nullptr) { if (tracking_timer_ >= Defaults::Enemies::Square::TRACKING_INTERVAL && ship_position_ != nullptr) {
tracking_timer_ = 0.0F; tracking_timer_ = 0.0F;
const Vec2 TO_SHIP = *ship_position_ - center_; const Vec2 TO_SHIP = *ship_position_ - center_;
@@ -348,89 +348,89 @@ void Enemy::behaviorQuadrat(float delta_time) {
} }
} }
// MOLINILLO: movimiento recto + boost de rotación visual cerca del ship. // PINWHEEL: movimiento recto + boost de rotación visual cerca del ship.
// Sin tracking — solo cambios de dirección raros (igual que Pentagon pero // Sin tracking — solo cambios de dirección raros (igual que Pentagon pero
// con probabilidad mucho menor). // con probabilidad mucho menor).
void Enemy::behaviorMolinillo(float /*delta_time*/) { void Enemy::behaviorPinwheel(float /*delta_time*/) {
// Boost de rotación visual por proximidad al ship // Boost de rotación visual por proximidad al ship
if (ship_position_ != nullptr) { if (ship_position_ != nullptr) {
const Vec2 TO_SHIP = *ship_position_ - center_; const Vec2 TO_SHIP = *ship_position_ - center_;
const float DIST = TO_SHIP.length(); const float DIST = TO_SHIP.length();
if (DIST < Defaults::Enemies::Molinillo::PROXIMITY_DISTANCE) { if (DIST < Defaults::Enemies::Pinwheel::PROXIMITY_DISTANCE) {
drotacio_ = animacio_.drotacio_base * Defaults::Enemies::Molinillo::DROTACIO_PROXIMITY_MULTIPLIER; rotation_delta_ = animation_.rotation_delta_base * Defaults::Enemies::Pinwheel::ROTATION_DELTA_PROXIMITY_MULTIPLIER;
} else { } else {
drotacio_ = animacio_.drotacio_base; rotation_delta_ = animation_.rotation_delta_base;
} }
} }
// Movimiento lineal puro: el world se encarga de integrar y rebotar. // Movimiento lineal puro: el world se encarga de integrar y rebotar.
} }
void Enemy::updateAnimation(float delta_time) { void Enemy::updateAnimation(float delta_time) {
updatePalpitation(delta_time); updatePulse(delta_time);
updateRotationAcceleration(delta_time); updateRotationAcceleration(delta_time);
} }
void Enemy::updatePalpitation(float delta_time) { void Enemy::updatePulse(float delta_time) {
if (animacio_.palpitacio_activa) { if (animation_.pulse_active) {
animacio_.palpitacio_fase += 2.0F * Constants::PI * animacio_.palpitacio_frequencia * delta_time; animation_.pulse_phase += 2.0F * Constants::PI * animation_.pulse_frequency * delta_time;
animacio_.palpitacio_temps_restant -= delta_time; animation_.pulse_time_remaining -= delta_time;
if (animacio_.palpitacio_temps_restant <= 0.0F) { if (animation_.pulse_time_remaining <= 0.0F) {
animacio_.palpitacio_activa = false; animation_.pulse_active = false;
} }
} else { } else {
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX); const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
const float TRIGGER_PROB = Defaults::Enemies::Animation::PALPITACIO_TRIGGER_PROB * delta_time; const float TRIGGER_PROB = Defaults::Enemies::Animation::PULSE_TRIGGER_PROB * delta_time;
if (RAND_VAL < TRIGGER_PROB) { if (RAND_VAL < TRIGGER_PROB) {
animacio_.palpitacio_activa = true; animation_.pulse_active = true;
animacio_.palpitacio_fase = 0.0F; animation_.pulse_phase = 0.0F;
const float FREQ_RANGE = Defaults::Enemies::Animation::PALPITACIO_FREQ_MAX - const float FREQ_RANGE = Defaults::Enemies::Animation::PULSE_FREQ_MAX -
Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN; Defaults::Enemies::Animation::PULSE_FREQ_MIN;
animacio_.palpitacio_frequencia = Defaults::Enemies::Animation::PALPITACIO_FREQ_MIN + animation_.pulse_frequency = Defaults::Enemies::Animation::PULSE_FREQ_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * FREQ_RANGE); ((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * FREQ_RANGE);
const float AMP_RANGE = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MAX - const float AMP_RANGE = Defaults::Enemies::Animation::PULSE_AMPLITUD_MAX -
Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN; Defaults::Enemies::Animation::PULSE_AMPLITUD_MIN;
animacio_.palpitacio_amplitud = Defaults::Enemies::Animation::PALPITACIO_AMPLITUD_MIN + animation_.pulse_amplitude = Defaults::Enemies::Animation::PULSE_AMPLITUD_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * AMP_RANGE); ((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * AMP_RANGE);
const float DUR_RANGE = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MAX - const float DUR_RANGE = Defaults::Enemies::Animation::PULSE_DURATION_MAX -
Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN; Defaults::Enemies::Animation::PULSE_DURATION_MIN;
animacio_.palpitacio_temps_restant = Defaults::Enemies::Animation::PALPITACIO_DURACIO_MIN + animation_.pulse_time_remaining = Defaults::Enemies::Animation::PULSE_DURATION_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE); ((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE);
} }
} }
} }
void Enemy::updateRotationAcceleration(float delta_time) { void Enemy::updateRotationAcceleration(float delta_time) {
if (animacio_.drotacio_t < 1.0F) { if (animation_.rotation_delta_t < 1.0F) {
animacio_.drotacio_t += delta_time / animacio_.drotacio_duracio; animation_.rotation_delta_t += delta_time / animation_.rotation_delta_duration;
if (animacio_.drotacio_t >= 1.0F) { if (animation_.rotation_delta_t >= 1.0F) {
animacio_.drotacio_t = 1.0F; animation_.rotation_delta_t = 1.0F;
animacio_.drotacio_base = animacio_.drotacio_objetivo; animation_.rotation_delta_base = animation_.rotation_delta_target;
drotacio_ = animacio_.drotacio_base; rotation_delta_ = animation_.rotation_delta_base;
} else { } else {
const float T = animacio_.drotacio_t; const float T = animation_.rotation_delta_t;
const float SMOOTH_T = T * T * (3.0F - (2.0F * T)); const float SMOOTH_T = T * T * (3.0F - (2.0F * T));
const float INITIAL = animacio_.drotacio_base; const float INITIAL = animation_.rotation_delta_base;
const float TARGET = animacio_.drotacio_objetivo; const float TARGET = animation_.rotation_delta_target;
drotacio_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T); rotation_delta_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T);
} }
} else { } else {
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX); const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
const float TRIGGER_PROB = Defaults::Enemies::Animation::ROTACIO_ACCEL_TRIGGER_PROB * delta_time; const float TRIGGER_PROB = Defaults::Enemies::Animation::ROTATION_ACCEL_TRIGGER_PROB * delta_time;
if (RAND_VAL < TRIGGER_PROB) { if (RAND_VAL < TRIGGER_PROB) {
animacio_.drotacio_t = 0.0F; animation_.rotation_delta_t = 0.0F;
const float MULT_RANGE = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MAX - const float MULT_RANGE = Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MAX -
Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN; Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MIN;
const float MULTIPLIER = Defaults::Enemies::Animation::ROTACIO_ACCEL_MULTIPLIER_MIN + const float MULTIPLIER = Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * MULT_RANGE); ((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * MULT_RANGE);
animacio_.drotacio_objetivo = animacio_.drotacio_base * MULTIPLIER; animation_.rotation_delta_target = animation_.rotation_delta_base * MULTIPLIER;
const float DUR_RANGE = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MAX - const float DUR_RANGE = Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MAX -
Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN; Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MIN;
animacio_.drotacio_duracio = Defaults::Enemies::Animation::ROTACIO_ACCEL_DURACIO_MIN + animation_.rotation_delta_duration = Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE); ((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE);
} }
} }
@@ -438,15 +438,15 @@ void Enemy::updateRotationAcceleration(float delta_time) {
auto Enemy::computeCurrentScale() const -> float { auto Enemy::computeCurrentScale() const -> float {
float scale = 1.0F; float scale = 1.0F;
if (timer_invulnerabilitat_ > 0.0F) { if (invulnerability_timer_ > 0.0F) {
const float T_INV = timer_invulnerabilitat_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; const float T_INV = invulnerability_timer_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION;
const float T = 1.0F - T_INV; const float T = 1.0F - T_INV;
const float SMOOTH_T = T * T * (3.0F - (2.0F * T)); const float SMOOTH_T = T * T * (3.0F - (2.0F * T));
constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_START; constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_START;
constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_END; constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_END;
scale = START + ((END - START) * SMOOTH_T); scale = START + ((END - START) * SMOOTH_T);
} else if (animacio_.palpitacio_activa) { } else if (animation_.pulse_active) {
scale += animacio_.palpitacio_amplitud * std::sin(animacio_.palpitacio_fase); scale += animation_.pulse_amplitude * std::sin(animation_.pulse_phase);
} }
return scale; return scale;
} }
@@ -454,22 +454,22 @@ auto Enemy::computeCurrentScale() const -> float {
auto Enemy::getBaseVelocity() const -> float { auto Enemy::getBaseVelocity() const -> float {
switch (type_) { switch (type_) {
case EnemyType::PENTAGON: case EnemyType::PENTAGON:
return Defaults::Enemies::Pentagon::VELOCITAT; return Defaults::Enemies::Pentagon::SPEED;
case EnemyType::QUADRAT: case EnemyType::SQUARE:
return Defaults::Enemies::Cuadrado::VELOCITAT; return Defaults::Enemies::Square::SPEED;
case EnemyType::MOLINILLO: case EnemyType::PINWHEEL:
return Defaults::Enemies::Molinillo::VELOCITAT; return Defaults::Enemies::Pinwheel::SPEED;
default: default:
return Defaults::Enemies::Pentagon::VELOCITAT; return Defaults::Enemies::Pentagon::SPEED;
} }
} }
auto Enemy::getBaseRotation() const -> float { auto Enemy::getBaseRotation() const -> float {
return animacio_.drotacio_base != 0.0F ? animacio_.drotacio_base : drotacio_; return animation_.rotation_delta_base != 0.0F ? animation_.rotation_delta_base : rotation_delta_;
} }
void Enemy::setTrackingStrength(float strength) { void Enemy::setTrackingStrength(float strength) {
if (type_ == EnemyType::QUADRAT) { if (type_ == EnemyType::SQUARE) {
tracking_strength_ = strength; tracking_strength_ = strength;
} }
} }
+31 -28
View File
@@ -13,24 +13,24 @@
// Tipo de enemy // Tipo de enemy
enum class EnemyType : uint8_t { enum class EnemyType : uint8_t {
PENTAGON = 0, // Pentágono esquivador (zigzag) PENTAGON = 0, // Pentágono esquivador (zigzag)
QUADRAT = 1, // Cuadrado perseguidor (tracks ship) SQUARE = 1, // Square perseguidor (tracks ship)
MOLINILLO = 2 // Molinillo agresivo (rápido, girando) PINWHEEL = 2 // Molinillo agresivo (rápido, girando)
}; };
// Estado de animación (palpitación + rotación acelerada) // Estado de animación (palpitación + rotación acelerada)
struct EnemyAnimation { struct EnemyAnimation {
// Palpitación (efecto respiración) // Palpitación (efecto respiración)
bool palpitacio_activa = false; bool pulse_active = false;
float palpitacio_fase = 0.0F; float pulse_phase = 0.0F;
float palpitacio_frequencia = 2.0F; float pulse_frequency = 2.0F;
float palpitacio_amplitud = 0.15F; float pulse_amplitude = 0.15F;
float palpitacio_temps_restant = 0.0F; float pulse_time_remaining = 0.0F;
// Aceleración de rotación visual (modulación a largo plazo) // Aceleración de rotación visual (modulación a largo plazo)
float drotacio_base = 0.0F; float rotation_delta_base = 0.0F;
float drotacio_objetivo = 0.0F; float rotation_delta_target = 0.0F;
float drotacio_t = 0.0F; float rotation_delta_t = 0.0F;
float drotacio_duracio = 0.0F; float rotation_delta_duration = 0.0F;
}; };
class Enemy : public Entities::Entity { class Enemy : public Entities::Entity {
@@ -46,21 +46,24 @@ class Enemy : public Entities::Entity {
void draw() const override; void draw() const override;
// Override: Interfaz de Entity // Override: Interfaz de Entity
[[nodiscard]] auto isActive() const -> bool override { return esta_; } [[nodiscard]] auto isActive() const -> bool override { return is_active_; }
// Override: Interfaz de colisión // Override: Interfaz de colisión
[[nodiscard]] auto getCollisionRadius() const -> float override { [[nodiscard]] auto getCollisionRadius() const -> float override {
return Defaults::Entities::ENEMY_RADIUS; return Defaults::Entities::ENEMY_RADIUS;
} }
// Mentre fa spawn (invulnerable) segueix col·lisionant: les bales el
// poden abatre i el cos físic rebota amb la nau. El damage a la nau
// segueix filtrat per `isInvulnerable()` al detectShipEnemy.
[[nodiscard]] auto isCollidable() const -> bool override { [[nodiscard]] auto isCollidable() const -> bool override {
return esta_ && timer_invulnerabilitat_ <= 0.0F; return is_active_;
} }
// Marcar destruido (desactiva el cuerpo físicamente: radius=0) // Marcar destruido (desactiva el cuerpo físicamente: radius=0)
void destruir(); void destroy();
// Getters // Getters
[[nodiscard]] auto getRotationDelta() const -> float { return drotacio_; } [[nodiscard]] auto getRotationDelta() const -> float { return rotation_delta_; }
[[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; } [[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; }
// Set ship position reference for tracking behavior // Set ship position reference for tracking behavior
@@ -76,18 +79,18 @@ class Enemy : public Entities::Entity {
// actual del body_.velocity. // actual del body_.velocity.
void setVelocity(float speed); void setVelocity(float speed);
void setRotation(float rot) { void setRotation(float rot) {
drotacio_ = rot; rotation_delta_ = rot;
animacio_.drotacio_base = rot; animation_.rotation_delta_base = rot;
} }
void setTrackingStrength(float strength); void setTrackingStrength(float strength);
// Invulnerabilidad // Invulnerabilidad
[[nodiscard]] auto isInvulnerable() const -> bool { return timer_invulnerabilitat_ > 0.0F; } [[nodiscard]] auto isInvulnerable() const -> bool { return invulnerability_timer_ > 0.0F; }
[[nodiscard]] auto getInvulnerabilityTime() const -> float { return timer_invulnerabilitat_; } [[nodiscard]] auto getInvulnerabilityTime() const -> float { return invulnerability_timer_; }
// Estado "herido": entre primer impacto de bala y explosión diferida. // Estado "herido": entre primer impacto de bala y explosión diferida.
// shooter_id: id del jugador que herí; 0xFF = sin atribución (cadena, etc.). // shooter_id: id del jugador que herí; 0xFF = sin atribución (cadena, etc.).
void herir(uint8_t shooter_id = 0xFF); void hurt(uint8_t shooter_id = 0xFF);
[[nodiscard]] auto isWounded() const -> bool { return wounded_timer_ > 0.0F; } [[nodiscard]] auto isWounded() const -> bool { return wounded_timer_ > 0.0F; }
[[nodiscard]] auto getWoundedTimer() const -> float { return wounded_timer_; } [[nodiscard]] auto getWoundedTimer() const -> float { return wounded_timer_; }
[[nodiscard]] auto woundExpiredThisFrame() const -> bool { return wound_expired_this_frame_; } [[nodiscard]] auto woundExpiredThisFrame() const -> bool { return wound_expired_this_frame_; }
@@ -101,12 +104,12 @@ class Enemy : public Entities::Entity {
// Miembros específicos (heredados: renderer_, shape_, center_, angle_, brightness_, body_). // Miembros específicos (heredados: renderer_, shape_, center_, angle_, brightness_, body_).
// Inicializados en la declaración: el ctor por defecto deja al enemy en estado "inactivo // Inicializados en la declaración: el ctor por defecto deja al enemy en estado "inactivo
// como pentágono", coherente con lo que harán init() o el ctor con renderer al activarlo. // como pentágono", coherente con lo que harán init() o el ctor con renderer al activarlo.
float drotacio_{0.0F}; // Velocidad angular visual (rad/s) — solo decoración, separada de body_.angular_velocity float rotation_delta_{0.0F}; // Velocidad angular visual (rad/s) — solo decoración, separada de body_.angular_velocity
float rotacio_{0.0F}; // Rotación visual acumulada (no afecta movimiento) float rotation_{0.0F}; // Rotación visual acumulada (no afecta movimiento)
bool esta_{false}; bool is_active_{false};
EnemyType type_{EnemyType::PENTAGON}; EnemyType type_{EnemyType::PENTAGON};
EnemyAnimation animacio_; EnemyAnimation animation_;
// Comportamiento type-specific // Comportamiento type-specific
float tracking_timer_{0.0F}; // Quadrat: tiempo desde último update de dirección float tracking_timer_{0.0F}; // Quadrat: tiempo desde último update de dirección
@@ -115,7 +118,7 @@ class Enemy : public Entities::Entity {
float direction_change_timer_{0.0F}; // Pentagon: tiempo para próximo cambio de dirección float direction_change_timer_{0.0F}; // Pentagon: tiempo para próximo cambio de dirección
// Invulnerabilidad post-spawn // Invulnerabilidad post-spawn
float timer_invulnerabilitat_{0.0F}; float invulnerability_timer_{0.0F};
// Estado "herido": timer cuenta atrás; al cruzar 0 se marca expiración. // Estado "herido": timer cuenta atrás; al cruzar 0 se marca expiración.
float wounded_timer_{0.0F}; float wounded_timer_{0.0F};
@@ -124,11 +127,11 @@ class Enemy : public Entities::Entity {
// Métodos privados // Métodos privados
void updateAnimation(float delta_time); void updateAnimation(float delta_time);
void updatePalpitation(float delta_time); void updatePulse(float delta_time);
void updateRotationAcceleration(float delta_time); void updateRotationAcceleration(float delta_time);
void behaviorPentagon(float delta_time); void behaviorPentagon(float delta_time);
void behaviorQuadrat(float delta_time); void behaviorSquare(float delta_time);
void behaviorMolinillo(float delta_time); void behaviorPinwheel(float delta_time);
[[nodiscard]] auto computeCurrentScale() const -> float; [[nodiscard]] auto computeCurrentScale() const -> float;
// Estático: solo opera sobre ship_pos pasado; no consulta estado del enemy. // Estático: solo opera sobre ship_pos pasado; no consulta estado del enemy.
static auto attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool; static auto attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool;
+30 -5
View File
@@ -10,6 +10,7 @@
#include <cstdint> #include <cstdint>
#include <iostream> #include <iostream>
#include "core/audio/audio.hpp"
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/graphics/shape_loader.hpp" #include "core/graphics/shape_loader.hpp"
@@ -43,10 +44,10 @@ void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) {
if (spawn_point != nullptr) { if (spawn_point != nullptr) {
center_ = *spawn_point; center_ = *spawn_point;
} else { } else {
float centre_x; float center_x;
float centre_y; float center_y;
Constants::getPlayAreaCenter(centre_x, centre_y); Constants::getPlayAreaCenter(center_x, center_y);
center_ = {.x = centre_x, .y = centre_y}; center_ = {.x = center_x, .y = center_y};
} }
// Reset orientación // Reset orientación
@@ -62,6 +63,8 @@ void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) {
// Activar invulnerabilidad solo si es respawn // Activar invulnerabilidad solo si es respawn
invulnerable_timer_ = activar_invulnerabilitat ? Defaults::Ship::INVULNERABILITY_DURATION : 0.0F; invulnerable_timer_ = activar_invulnerabilitat ? Defaults::Ship::INVULNERABILITY_DURATION : 0.0F;
is_hit_ = false; is_hit_ = false;
hurt_timer_ = 0.0F;
touching_enemy_prev_frame_ = false;
} }
void Ship::processInput(float delta_time, uint8_t player_id) { void Ship::processInput(float delta_time, uint8_t player_id) {
@@ -115,6 +118,12 @@ void Ship::update(float delta_time) {
invulnerable_timer_ = std::max(invulnerable_timer_, 0.0F); invulnerable_timer_ = std::max(invulnerable_timer_, 0.0F);
} }
// Decrementar timer d'estat HURT (a 0 → torna a normal sense efecte extern)
if (hurt_timer_ > 0.0F) {
hurt_timer_ -= delta_time;
hurt_timer_ = std::max(hurt_timer_, 0.0F);
}
// El movimiento real lo hace PhysicsWorld::update(). // El movimiento real lo hace PhysicsWorld::update().
// Aquí solo lógica de estado. // Aquí solo lógica de estado.
@@ -157,5 +166,21 @@ void Ship::draw() const {
const float VISUAL_PUSH = SPEED / Defaults::Ship::VISUAL_PUSH_DIVISOR; const float VISUAL_PUSH = SPEED / Defaults::Ship::VISUAL_PUSH_DIVISOR;
const float SCALE = 1.0F + (VISUAL_PUSH / Defaults::Ship::VISUAL_SCALE_DIVISOR); const float SCALE = 1.0F + (VISUAL_PUSH / Defaults::Ship::VISUAL_SCALE_DIVISOR);
Rendering::renderShape(renderer_, shape_, center_, angle_, SCALE, 1.0F, brightness_, Defaults::Palette::SHIP); // Parpelleig daurat mentre està ferida: alterna color normal ↔ color hurt
// a Hurt::BLINK_HZ (mateixa estètica que el wounded dels enemics).
SDL_Color color = color_normal_;
if (hurt_timer_ > 0.0F) {
const float CYCLE = 1.0F / Defaults::Ship::Hurt::BLINK_HZ;
const float T = std::fmod(hurt_timer_, CYCLE);
if (T < (CYCLE / 2.0F)) {
color = color_hurt_;
}
}
Rendering::renderShape(renderer_, shape_, center_, angle_, SCALE, 1.0F, brightness_, color);
}
void Ship::hurt() {
hurt_timer_ = Defaults::Ship::Hurt::DURATION;
Audio::get()->playSound(Defaults::Sound::HURT, Audio::Group::GAME);
} }
+21
View File
@@ -53,10 +53,31 @@ class Ship : public Entities::Entity {
body_.velocity = Vec2{}; // Detener al morir body_.velocity = Vec2{}; // Detener al morir
} }
// Estat "ferit": primera col·lisió amb enemic dispara HURT; segona durant HURT mata.
void hurt();
[[nodiscard]] auto isHurt() const -> bool { return hurt_timer_ > 0.0F; }
[[nodiscard]] auto getHurtTimer() const -> float { return hurt_timer_; }
// Edge-trigger del contacte amb enemics: un impacte només compta a la transició
// no-tocant → tocant. Sense açò, el contacte continu durant el rebot frame-a-frame
// dispararia HURT i mort en frames consecutius.
[[nodiscard]] auto wasTouchingEnemyPrevFrame() const -> bool { return touching_enemy_prev_frame_; }
void setTouchingEnemyPrevFrame(bool touching) { touching_enemy_prev_frame_ = touching; }
private: private:
// Miembros específicos de Ship (heredados: renderer_, shape_, center_, angle_, brightness_, body_). // Miembros específicos de Ship (heredados: renderer_, shape_, center_, angle_, brightness_, body_).
// Inicializados en la declaración: el ctor por defecto deja la nave "viva y sin invulnerabilidad", // Inicializados en la declaración: el ctor por defecto deja la nave "viva y sin invulnerabilidad",
// que es el estado coherente al que llevan tanto init() como el ctor con renderer. // que es el estado coherente al que llevan tanto init() como el ctor con renderer.
bool is_hit_{false}; bool is_hit_{false};
float invulnerable_timer_{0.0F}; // 0.0f = vulnerable, >0.0f = invulnerable float invulnerable_timer_{0.0F}; // 0.0f = vulnerable, >0.0f = invulnerable
// Colors de la nau (propietats, prep per migració a YAML).
SDL_Color color_normal_{Defaults::Palette::SHIP};
SDL_Color color_hurt_{Defaults::Palette::WOUNDED};
// >0 → estat HURT (parpelleig color_normal_ ↔ color_hurt_).
float hurt_timer_{0.0F};
// Edge-trigger: true si el frame anterior la nau ja estava en contacte amb un enemic.
bool touching_enemy_prev_frame_{false};
}; };
+123 -79
View File
@@ -30,6 +30,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
floating_score_manager_(sdl.getRenderer()), floating_score_manager_(sdl.getRenderer()),
trail_manager_(sdl.getRenderer()), trail_manager_(sdl.getRenderer()),
text_(sdl.getRenderer()), text_(sdl.getRenderer()),
starfield_parallax_(sdl.getRenderer()),
playfield_(sdl.getRenderer()), playfield_(sdl.getRenderer()),
border_(sdl.getRenderer()) { border_(sdl.getRenderer()) {
// Recuperar configuración de match des del context // Recuperar configuración de match des del context
@@ -37,9 +38,9 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
// Debug output de la configuración // Debug output de la configuración
std::cout << "[GameScene] Configuración de match - P1: " std::cout << "[GameScene] Configuración de match - P1: "
<< (match_config_.jugador1_actiu ? "ACTIU" : "INACTIU") << (match_config_.player1_active ? "ACTIU" : "INACTIU")
<< ", P2: " << ", P2: "
<< (match_config_.jugador2_actiu ? "ACTIU" : "INACTIU") << (match_config_.player2_active ? "ACTIU" : "INACTIU")
<< '\n'; << '\n';
// Consumir opciones (preparació per MODE_DEMO futur) // Consumir opciones (preparació per MODE_DEMO futur)
@@ -60,7 +61,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
// Basat en el codi Pascal original: line 376 // Basat en el codi Pascal original: line 376
std::srand(static_cast<unsigned>(std::time(nullptr))); std::srand(static_cast<unsigned>(std::time(nullptr)));
// Configurar el mundo físico con los límites de la zona de juego. // Configurar el mundo físico con los límites de la zone de juego.
physics_world_.clear(); physics_world_.clear();
physics_world_.setBounds(Defaults::Zones::PLAYAREA); physics_world_.setBounds(Defaults::Zones::PLAYAREA);
@@ -75,18 +76,18 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
border_.bumpAt(hit.contact_point, STRENGTH); border_.bumpAt(hit.contact_point, STRENGTH);
}); });
// Fireworks generen un pulse a les línies V i H més properes del playfield. // Fireworks generen una ripple gran al playfield (ona d'aigua centrada al burst).
firework_manager_.setSpawnCallback([this](Vec2 origen) { firework_manager_.setSpawnCallback([this](Vec2 origin) {
playfield_.notifyFireworkSpawn(origen); playfield_.notifyExplosion(origin);
}); });
// Explosions properes a una paret també generen bump (falloff lineal amb la distància). // Explosions properes a una paret també generen bump (falloff lineal amb la distància).
debris_manager_.setExplosionCallback([this](Vec2 center) { debris_manager_.setExplosionCallback([this](Vec2 center) {
const SDL_FRect& zona = Defaults::Zones::PLAYAREA; const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
const float DIST_LEFT = std::abs(center.x - zona.x); const float DIST_LEFT = std::abs(center.x - zone.x);
const float DIST_RIGHT = std::abs((zona.x + zona.w) - center.x); const float DIST_RIGHT = std::abs((zone.x + zone.w) - center.x);
const float DIST_TOP = std::abs(center.y - zona.y); const float DIST_TOP = std::abs(center.y - zone.y);
const float DIST_BOTTOM = std::abs((zona.y + zona.h) - center.y); const float DIST_BOTTOM = std::abs((zone.y + zone.h) - center.y);
const float MIN_DIST = std::min({DIST_LEFT, DIST_RIGHT, DIST_TOP, DIST_BOTTOM}); const float MIN_DIST = std::min({DIST_LEFT, DIST_RIGHT, DIST_TOP, DIST_BOTTOM});
if (MIN_DIST > Defaults::Border::EXPLOSION_FALLOFF_PX) { if (MIN_DIST > Defaults::Border::EXPLOSION_FALLOFF_PX) {
return; return;
@@ -129,9 +130,9 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
// Inicialitzar naves segons configuración (solo jugadors active) // Inicialitzar naves segons configuración (solo jugadors active)
for (uint8_t i = 0; i < 2; i++) { for (uint8_t i = 0; i < 2; i++) {
bool jugador_actiu = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; bool player_active = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
if (jugador_actiu) { if (player_active) {
// Jugador active: init normalment // Jugador active: init normalment
Vec2 spawn_pos = getSpawnPoint(i); Vec2 spawn_pos = getSpawnPoint(i);
ships_[i].init(&spawn_pos, false); // No invulnerability at start ships_[i].init(&spawn_pos, false); // No invulnerability at start
@@ -213,24 +214,45 @@ void GameScene::stepPhysics(float delta_time) {
bullet.postUpdate(delta_time); bullet.postUpdate(delta_time);
} }
trail_manager_.update(delta_time, ships_); trail_manager_.update(delta_time, ships_);
// Starfield: world_velocity = -mitjana_de_naus_actives. Si dues naus van en
// sentits oposats, es cancel·len → estrelles quietes (cap jugador "guanya").
// Si només n'hi ha una activa, segueix la seva velocitat.
Vec2 ship_vel_avg{.x = 0.0F, .y = 0.0F};
int n_active = 0;
for (const auto& ship : ships_) {
if (ship.isActive()) {
const Vec2 V = ship.getVelocityVector();
ship_vel_avg.x += V.x;
ship_vel_avg.y += V.y;
n_active++;
}
}
if (n_active > 0) {
ship_vel_avg.x /= static_cast<float>(n_active);
ship_vel_avg.y /= static_cast<float>(n_active);
}
starfield_parallax_.update(delta_time, Vec2{.x = -ship_vel_avg.x, .y = -ship_vel_avg.y});
playfield_.update(delta_time); playfield_.update(delta_time);
border_.update(delta_time); border_.update(delta_time);
// Notificar al playfield que la nau ha passat (per excitar línies properes). // Notificar al playfield que la nau es mou (genera ripples petites a cadència).
for (const auto& ship : ships_) { for (std::size_t id = 0; id < ships_.size(); id++) {
if (ship.isActive()) { if (ships_[id].isActive()) {
playfield_.notifyShipPass(ship.getCenter(), ship.getSpeed()); playfield_.notifyShipMoving(static_cast<std::uint8_t>(id),
ships_[id].getCenter(),
ships_[id].getSpeed(),
delta_time);
} }
} }
} }
void GameScene::stepShootingInput() { void GameScene::stepShootingInput() {
auto* input = Input::get(); auto* input = Input::get();
if (match_config_.jugador1_actiu && if (match_config_.player1_active &&
input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) { input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) {
fireBullet(0); fireBullet(0);
} }
if (match_config_.jugador2_actiu && if (match_config_.player2_active &&
input->checkActionPlayer2(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) { input->checkActionPlayer2(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) {
fireBullet(1); fireBullet(1);
} }
@@ -245,16 +267,16 @@ void GameScene::stepMidGameJoin() {
// Solo se permite join si hay al menos un jugador vivo (no se puede // Solo se permite join si hay al menos un jugador vivo (no se puede
// hacer join en pantalla vacía). // hacer join en pantalla vacía).
const bool ALGU_VIU = const bool ALGU_VIU =
(match_config_.jugador1_actiu && hit_timer_per_player_[0] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER) || (match_config_.player1_active && hit_timer_per_player_[0] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER) ||
(match_config_.jugador2_actiu && hit_timer_per_player_[1] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER); (match_config_.player2_active && hit_timer_per_player_[1] != Defaults::Game::HIT_TIMER_INACTIVE_PLAYER);
if (!ALGU_VIU) { if (!ALGU_VIU) {
return; return;
} }
auto* input = Input::get(); auto* input = Input::get();
for (uint8_t pid = 0; pid < 2; pid++) { for (uint8_t pid = 0; pid < 2; pid++) {
const bool ACTIU = (pid == 0) ? match_config_.jugador1_actiu const bool ACTIU = (pid == 0) ? match_config_.player1_active
: match_config_.jugador2_actiu; : match_config_.player2_active;
const bool MUERTO_SIN_VIDAS = hit_timer_per_player_[pid] == Defaults::Game::HIT_TIMER_INACTIVE_PLAYER; const bool MUERTO_SIN_VIDAS = hit_timer_per_player_[pid] == Defaults::Game::HIT_TIMER_INACTIVE_PLAYER;
if (ACTIU && !MUERTO_SIN_VIDAS) { if (ACTIU && !MUERTO_SIN_VIDAS) {
continue; // jugador ya está jugando continue; // jugador ya está jugando
@@ -296,6 +318,7 @@ auto GameScene::stepContinueScreen(float delta_time) -> bool {
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
bullet.update(delta_time); bullet.update(delta_time);
} }
Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_);
debris_manager_.update(delta_time); debris_manager_.update(delta_time);
firework_manager_.update(delta_time); firework_manager_.update(delta_time);
floating_score_manager_.update(delta_time); floating_score_manager_.update(delta_time);
@@ -321,6 +344,7 @@ auto GameScene::stepGameOver(float delta_time) -> bool {
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
bullet.update(delta_time); bullet.update(delta_time);
} }
Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_);
debris_manager_.update(delta_time); debris_manager_.update(delta_time);
firework_manager_.update(delta_time); firework_manager_.update(delta_time);
floating_score_manager_.update(delta_time); floating_score_manager_.update(delta_time);
@@ -351,8 +375,8 @@ void GameScene::stepDeathSequence(float delta_time) {
// Sin vidas: marcar definitivamente muerto y comprobar transición a CONTINUE. // Sin vidas: marcar definitivamente muerto y comprobar transición a CONTINUE.
hit_timer_per_player_[i] = Defaults::Game::HIT_TIMER_INACTIVE_PLAYER; hit_timer_per_player_[i] = Defaults::Game::HIT_TIMER_INACTIVE_PLAYER;
const bool P1_DEAD = !match_config_.jugador1_actiu || lives_per_player_[0] <= 0; const bool P1_DEAD = !match_config_.player1_active || lives_per_player_[0] <= 0;
const bool P2_DEAD = !match_config_.jugador2_actiu || lives_per_player_[1] <= 0; const bool P2_DEAD = !match_config_.player2_active || lives_per_player_[1] <= 0;
if (P1_DEAD && P2_DEAD) { if (P1_DEAD && P2_DEAD) {
game_over_state_ = GameOverState::CONTINUE; game_over_state_ = GameOverState::CONTINUE;
continue_counter_ = Defaults::Game::CONTINUE_COUNT_START; continue_counter_ = Defaults::Game::CONTINUE_COUNT_START;
@@ -416,10 +440,10 @@ void GameScene::runStageInitHud(float delta_time) {
Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT, Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT,
Defaults::Game::INIT_HUD_SHIP2_RATIO_END); Defaults::Game::INIT_HUD_SHIP2_RATIO_END);
if (match_config_.jugador1_actiu && SHIP1_P < 1.0F) { if (match_config_.player1_active && SHIP1_P < 1.0F) {
ships_[0].setCenter(Systems::InitHud::computeShipPosition(SHIP1_P, getSpawnPoint(0))); ships_[0].setCenter(Systems::InitHud::computeShipPosition(SHIP1_P, getSpawnPoint(0)));
} }
if (match_config_.jugador2_actiu && SHIP2_P < 1.0F) { if (match_config_.player2_active && SHIP2_P < 1.0F) {
ships_[1].setCenter(Systems::InitHud::computeShipPosition(SHIP2_P, getSpawnPoint(1))); ships_[1].setCenter(Systems::InitHud::computeShipPosition(SHIP2_P, getSpawnPoint(1)));
} }
} }
@@ -429,7 +453,7 @@ void GameScene::runStageLevelStart(float delta_time) {
// Ambas naves pueden moverse y disparar durante el intro. // Ambas naves pueden moverse y disparar durante el intro.
for (uint8_t i = 0; i < 2; i++) { for (uint8_t i = 0; i < 2; i++) {
const bool ACTIU = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; const bool ACTIU = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
if (ACTIU && hit_timer_per_player_[i] == 0.0F) { if (ACTIU && hit_timer_per_player_[i] == 0.0F) {
ships_[i].processInput(delta_time, i); ships_[i].processInput(delta_time, i);
ships_[i].update(delta_time); ships_[i].update(delta_time);
@@ -438,6 +462,7 @@ void GameScene::runStageLevelStart(float delta_time) {
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
bullet.update(delta_time); bullet.update(delta_time);
} }
Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_);
debris_manager_.update(delta_time); debris_manager_.update(delta_time);
firework_manager_.update(delta_time); firework_manager_.update(delta_time);
} }
@@ -456,7 +481,7 @@ void GameScene::runStagePlaying(float delta_time) {
// Gameplay normal: ships activos + entidades + colisiones + efectos. // Gameplay normal: ships activos + entidades + colisiones + efectos.
for (uint8_t i = 0; i < 2; i++) { for (uint8_t i = 0; i < 2; i++) {
const bool ACTIU = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; const bool ACTIU = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
if (ACTIU && hit_timer_per_player_[i] == 0.0F) { if (ACTIU && hit_timer_per_player_[i] == 0.0F) {
ships_[i].processInput(delta_time, i); ships_[i].processInput(delta_time, i);
ships_[i].update(delta_time); ships_[i].update(delta_time);
@@ -465,11 +490,15 @@ void GameScene::runStagePlaying(float delta_time) {
for (auto& enemy : enemies_) { for (auto& enemy : enemies_) {
enemy.update(delta_time); enemy.update(delta_time);
} }
// Col·lisions primer, després desactivació per fora-de-zone: així una bala que
// el mateix frame xoca amb un enemic i alhora surt del PLAYAREA es compta com a
// impacte abans no se la trenqui per sortir.
runCollisionDetections();
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
bullet.update(delta_time); bullet.update(delta_time);
} }
Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_);
runCollisionDetections();
debris_manager_.update(delta_time); debris_manager_.update(delta_time);
firework_manager_.update(delta_time); firework_manager_.update(delta_time);
floating_score_manager_.update(delta_time); floating_score_manager_.update(delta_time);
@@ -478,7 +507,7 @@ void GameScene::runStagePlaying(float delta_time) {
void GameScene::runStageLevelCompleted(float delta_time) { void GameScene::runStageLevelCompleted(float delta_time) {
stage_manager_->update(delta_time); stage_manager_->update(delta_time);
for (uint8_t i = 0; i < 2; i++) { for (uint8_t i = 0; i < 2; i++) {
const bool ACTIU = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; const bool ACTIU = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
if (ACTIU && hit_timer_per_player_[i] == 0.0F) { if (ACTIU && hit_timer_per_player_[i] == 0.0F) {
ships_[i].processInput(delta_time, i); ships_[i].processInput(delta_time, i);
ships_[i].update(delta_time); ships_[i].update(delta_time);
@@ -487,6 +516,7 @@ void GameScene::runStageLevelCompleted(float delta_time) {
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
bullet.update(delta_time); bullet.update(delta_time);
} }
Systems::Collision::desactivateOutOfBoundsBullets(bullets_, debris_manager_);
debris_manager_.update(delta_time); debris_manager_.update(delta_time);
firework_manager_.update(delta_time); firework_manager_.update(delta_time);
floating_score_manager_.update(delta_time); floating_score_manager_.update(delta_time);
@@ -550,14 +580,15 @@ void GameScene::drawBullets() const {
void GameScene::drawActiveShipsAlive() const { void GameScene::drawActiveShipsAlive() const {
for (uint8_t i = 0; i < 2; i++) { for (uint8_t i = 0; i < 2; i++) {
bool jugador_actiu = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; bool player_active = (i == 0) ? match_config_.player1_active : match_config_.player2_active;
if (jugador_actiu && hit_timer_per_player_[i] == 0.0F) { if (player_active && hit_timer_per_player_[i] == 0.0F) {
ships_[i].draw(); ships_[i].draw();
} }
} }
} }
void GameScene::drawContinueState() { void GameScene::drawContinueState() {
starfield_parallax_.draw();
border_.draw(); border_.draw();
drawEnemies(); drawEnemies();
drawBullets(); drawBullets();
@@ -569,6 +600,7 @@ void GameScene::drawContinueState() {
} }
void GameScene::drawGameOverState() { void GameScene::drawGameOverState() {
starfield_parallax_.draw();
border_.draw(); border_.draw();
drawEnemies(); drawEnemies();
drawBullets(); drawBullets();
@@ -581,10 +613,10 @@ void GameScene::drawGameOverState() {
constexpr float SPACING = Defaults::Game::GameOverScreen::TEXT_SPACING; constexpr float SPACING = Defaults::Game::GameOverScreen::TEXT_SPACING;
const SDL_FRect& play_area = Defaults::Zones::PLAYAREA; const SDL_FRect& play_area = Defaults::Zones::PLAYAREA;
float centre_x = play_area.x + (play_area.w / 2.0F); float center_x = play_area.x + (play_area.w / 2.0F);
float centre_y = play_area.y + (play_area.h / 2.0F); float center_y = play_area.y + (play_area.h / 2.0F);
text_.renderCentered(GAME_OVER_TEXT, {.x = centre_x, .y = centre_y}, SCALE, SPACING); text_.renderCentered(GAME_OVER_TEXT, {.x = center_x, .y = center_y}, SCALE, SPACING);
drawScoreboard(); drawScoreboard();
} }
@@ -614,6 +646,8 @@ void GameScene::drawInitHudState() {
Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT, Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT,
Defaults::Game::INIT_HUD_SHIP2_RATIO_END); Defaults::Game::INIT_HUD_SHIP2_RATIO_END);
// Capa de fons més profunda: estrelles 2D (apareixen senceres des del frame 0).
starfield_parallax_.draw();
// Graella de fons al darrere (timer intern propi, cobreix tot l'INIT_HUD). // Graella de fons al darrere (timer intern propi, cobreix tot l'INIT_HUD).
playfield_.draw(); playfield_.draw();
@@ -629,16 +663,17 @@ void GameScene::drawInitHudState() {
Systems::InitHud::drawScoreboardAnimated(text_, buildScoreboard(), score_progress); Systems::InitHud::drawScoreboardAnimated(text_, buildScoreboard(), score_progress);
} }
if (ship1_progress > 0.0F && match_config_.jugador1_actiu && ships_[0].isActive()) { if (ship1_progress > 0.0F && match_config_.player1_active && ships_[0].isActive()) {
ships_[0].draw(); ships_[0].draw();
} }
if (ship2_progress > 0.0F && match_config_.jugador2_actiu && ships_[1].isActive()) { if (ship2_progress > 0.0F && match_config_.player2_active && ships_[1].isActive()) {
ships_[1].draw(); ships_[1].draw();
} }
} }
void GameScene::drawLevelStartState() { void GameScene::drawLevelStartState() {
starfield_parallax_.draw();
playfield_.draw(); playfield_.draw();
border_.draw(); border_.draw();
trail_manager_.draw(); trail_manager_.draw();
@@ -652,6 +687,7 @@ void GameScene::drawLevelStartState() {
} }
void GameScene::drawPlayingState() { void GameScene::drawPlayingState() {
starfield_parallax_.draw();
playfield_.draw(); playfield_.draw();
border_.draw(); border_.draw();
trail_manager_.draw(); trail_manager_.draw();
@@ -665,6 +701,7 @@ void GameScene::drawPlayingState() {
} }
void GameScene::drawLevelCompletedState() { void GameScene::drawLevelCompletedState() {
starfield_parallax_.draw();
playfield_.draw(); playfield_.draw();
border_.draw(); border_.draw();
trail_manager_.draw(); trail_manager_.draw();
@@ -686,30 +723,36 @@ void GameScene::tocado(uint8_t player_id) {
if (hit_timer_per_player_[player_id] == 0.0F) { if (hit_timer_per_player_[player_id] == 0.0F) {
// *** PHASE 1: TRIGGER DEATH *** // *** PHASE 1: TRIGGER DEATH ***
// Capturar velocitat ABANS del markHit (que la reseteja a zero).
// Sense això, els debris no hereten cap inèrcia de la nau.
const Vec2 SHIP_VEL_PRE_DEATH = ships_[player_id].getVelocityVector();
const Vec2 SHIP_POS = ships_[player_id].getCenter();
const float SHIP_ANGLE = ships_[player_id].getAngle();
const float SHIP_BRIGHT = ships_[player_id].getBrightness();
// Mark ship as dead (stops rendering and input) // Mark ship as dead (stops rendering and input)
ships_[player_id].markHit(); ships_[player_id].markHit();
// Create ship explosion const Vec2 INHERITED_VEL = SHIP_VEL_PRE_DEATH *
const Vec2& ship_pos = ships_[player_id].getCenter(); Defaults::Physics::Debris::SHIP_VELOCITY_INHERITANCE;
float ship_angle = ships_[player_id].getAngle();
Vec2 vel_nau = ships_[player_id].getVelocityVector();
// Reduir la velocity heretada per la ship segons defaults (més realista)
constexpr float INHERIT = Defaults::Physics::Debris::SHIP_VELOCITY_INHERITANCE;
Vec2 vel_nau_80 = {.x = vel_nau.x * INHERIT, .y = vel_nau.y * INHERIT};
// Mateixa dispersió i efecte que els debris d'enemic (lifetime,
// friction, segment_multiplier alineats); només canvien sound i color.
debris_manager_.explode( debris_manager_.explode(
ships_[player_id].getShape(), // Ship shape (3 lines) ships_[player_id].getShape(),
ship_pos, // Center position SHIP_POS,
ship_angle, // Ship orientation SHIP_ANGLE,
1.0F, // Normal scale 1.0F,
Defaults::Physics::Debris::VELOCITAT_BASE, // 80 px/s Defaults::Physics::Debris::SPEED_BASE,
ships_[player_id].getBrightness(), // Heredar brightness SHIP_BRIGHT,
vel_nau_80, // Heredar 80% velocity INHERITED_VEL,
0.0F, // Nave: trayectorias rectas (sin drotacio) 0.0F, // sense herència angular
0.0F, // Sin herencia visual (rotación aleatoria) 0.0F, // sin herencia visual
Defaults::Sound::EXPLOSION2, // Sonido alternativo para la explosión Defaults::Sound::EXPLOSION2,
Defaults::Palette::SHIP // Debris hereda color de la nave Defaults::Palette::SHIP,
); Defaults::Physics::Debris::ENEMY_LIFETIME,
Defaults::Physics::Debris::ENEMY_FRICTION,
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER);
// Start death timer (non-zero to avoid re-triggering) // Start death timer (non-zero to avoid re-triggering)
hit_timer_per_player_[player_id] = Defaults::Game::HIT_TIMER_TRIGGER_DEATH; hit_timer_per_player_[player_id] = Defaults::Game::HIT_TIMER_TRIGGER_DEATH;
@@ -726,20 +769,20 @@ void GameScene::drawScoreboard() {
const float SCALE = Defaults::Hud::SCOREBOARD_TEXT_SCALE; const float SCALE = Defaults::Hud::SCOREBOARD_TEXT_SCALE;
const float SPACING = Defaults::Hud::SCOREBOARD_TEXT_SPACING; const float SPACING = Defaults::Hud::SCOREBOARD_TEXT_SPACING;
// Calcular centro de la zona del marcador // Calcular centro de la zone del marcador
const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD; const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD;
float centre_x = scoreboard_zone.w / 2.0F; float center_x = scoreboard_zone.w / 2.0F;
float centre_y = scoreboard_zone.y + (scoreboard_zone.h / 2.0F); float center_y = scoreboard_zone.y + (scoreboard_zone.h / 2.0F);
// Renderizar centrat // Renderizar centrat
text_.renderCentered(text, {.x = centre_x, .y = centre_y}, SCALE, SPACING); text_.renderCentered(text, {.x = center_x, .y = center_y}, SCALE, SPACING);
} }
auto GameScene::buildScoreboard() const -> std::string { auto GameScene::buildScoreboard() const -> std::string {
// Puntuación P1 (6 dígits) - mostrar zeros si inactiu // Puntuación P1 (6 dígits) - mostrar zeros si inactiu
std::string score_p1; std::string score_p1;
std::string vides_p1; std::string vides_p1;
if (match_config_.jugador1_actiu) { if (match_config_.player1_active) {
score_p1 = std::to_string(score_per_player_[0]); score_p1 = std::to_string(score_per_player_[0]);
score_p1 = std::string(6 - std::min(6, static_cast<int>(score_p1.length())), '0') + score_p1; score_p1 = std::string(6 - std::min(6, static_cast<int>(score_p1.length())), '0') + score_p1;
vides_p1 = (lives_per_player_[0] < 10) vides_p1 = (lives_per_player_[0] < 10)
@@ -758,7 +801,7 @@ auto GameScene::buildScoreboard() const -> std::string {
// Puntuación P2 (6 dígits) - mostrar zeros si inactiu // Puntuación P2 (6 dígits) - mostrar zeros si inactiu
std::string score_p2; std::string score_p2;
std::string vides_p2; std::string vides_p2;
if (match_config_.jugador2_actiu) { if (match_config_.player2_active) {
score_p2 = std::to_string(score_per_player_[1]); score_p2 = std::to_string(score_per_player_[1]);
score_p2 = std::string(6 - std::min(6, static_cast<int>(score_p2.length())), '0') + score_p2; score_p2 = std::string(6 - std::min(6, static_cast<int>(score_p2.length())), '0') + score_p2;
vides_p2 = (lives_per_player_[1] < 10) vides_p2 = (lives_per_player_[1] < 10)
@@ -836,9 +879,10 @@ void GameScene::drawStageMessage(const std::string& message) {
float x = play_area.x + ((play_area.w - full_text_width) / 2.0F); float x = play_area.x + ((play_area.w - full_text_width) / 2.0F);
float y = play_area.y + (play_area.h * Defaults::Game::STAGE_MESSAGE_Y_RATIO) - (text_height / 2.0F); float y = play_area.y + (play_area.h * Defaults::Game::STAGE_MESSAGE_Y_RATIO) - (text_height / 2.0F);
// Render only the partial message (typewriter effect) // Render only the partial message (typewriter effect) amb el color
// ambre neon del "PRESS START" del títol — unifica el feel dels missatges.
Vec2 pos = {.x = x, .y = y}; Vec2 pos = {.x = x, .y = y};
text_.render(partial_message, pos, scale, SPACING); text_.render(partial_message, pos, scale, SPACING, 1.0F, Defaults::Title::Colors::PRESS_START);
} }
// ======================================== // ========================================
@@ -846,7 +890,7 @@ void GameScene::drawStageMessage(const std::string& message) {
// ======================================== // ========================================
auto GameScene::getSpawnPoint(uint8_t player_id) const -> Vec2 { auto GameScene::getSpawnPoint(uint8_t player_id) const -> Vec2 {
const SDL_FRect& zona = Defaults::Zones::PLAYAREA; const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
float x_ratio; float x_ratio;
if (match_config_.isSinglePlayer()) { if (match_config_.isSinglePlayer()) {
@@ -860,8 +904,8 @@ auto GameScene::getSpawnPoint(uint8_t player_id) const -> Vec2 {
} }
return { return {
.x = zona.x + (zona.w * x_ratio), .x = zone.x + (zone.w * x_ratio),
.y = zona.y + (zona.h * Defaults::Game::SPAWN_Y_RATIO)}; .y = zone.y + (zone.h * Defaults::Game::SPAWN_Y_RATIO)};
} }
void GameScene::fireBullet(uint8_t player_id) { void GameScene::fireBullet(uint8_t player_id) {
@@ -883,15 +927,15 @@ void GameScene::fireBullet(uint8_t player_id) {
float sin_a = std::sin(ship_angle); float sin_a = std::sin(ship_angle);
float tip_x = (LOCAL_TIP_X * cos_a) - (LOCAL_TIP_Y * sin_a) + ship_centre.x; float tip_x = (LOCAL_TIP_X * cos_a) - (LOCAL_TIP_Y * sin_a) + ship_centre.x;
float tip_y = (LOCAL_TIP_X * sin_a) + (LOCAL_TIP_Y * cos_a) + ship_centre.y; float tip_y = (LOCAL_TIP_X * sin_a) + (LOCAL_TIP_Y * cos_a) + ship_centre.y;
Vec2 posicio_dispar = {.x = tip_x, .y = tip_y}; Vec2 fire_position = {.x = tip_x, .y = tip_y};
// Buscar primera bullet inactiva en el pool del player. // Buscar primera bullet inactiva en el pool del player.
// El pool global té MAX_BALES slots per jugador (P1=[0..MAX-1], P2=[MAX..2*MAX-1]). // El pool global té MAX_BULLETS slots per jugador (P1=[0..MAX-1], P2=[MAX..2*MAX-1]).
constexpr int SLOTS_PER_PLAYER = Defaults::Entities::MAX_BALES; constexpr int SLOTS_PER_PLAYER = Defaults::Entities::MAX_BULLETS;
const int START_IDX = player_id * SLOTS_PER_PLAYER; const int START_IDX = player_id * SLOTS_PER_PLAYER;
for (int i = START_IDX; i < START_IDX + SLOTS_PER_PLAYER; i++) { for (int i = START_IDX; i < START_IDX + SLOTS_PER_PLAYER; i++) {
if (!bullets_[i].isActive()) { if (!bullets_[i].isActive()) {
bullets_[i].disparar(posicio_dispar, ship_angle, player_id); bullets_[i].fire(fire_position, ship_angle, player_id);
break; break;
} }
} }
@@ -906,10 +950,10 @@ void GameScene::drawContinue() {
float escala_continue = Defaults::Game::ContinueScreen::CONTINUE_TEXT_SCALE; float escala_continue = Defaults::Game::ContinueScreen::CONTINUE_TEXT_SCALE;
float y_ratio_continue = Defaults::Game::ContinueScreen::CONTINUE_TEXT_Y_RATIO; float y_ratio_continue = Defaults::Game::ContinueScreen::CONTINUE_TEXT_Y_RATIO;
float centre_x = play_area.x + (play_area.w / 2.0F); float center_x = play_area.x + (play_area.w / 2.0F);
float centre_y_continue = play_area.y + (play_area.h * y_ratio_continue); float centre_y_continue = play_area.y + (play_area.h * y_ratio_continue);
text_.renderCentered(CONTINUE_TEXT, {.x = centre_x, .y = centre_y_continue}, escala_continue, SPACING); text_.renderCentered(CONTINUE_TEXT, {.x = center_x, .y = centre_y_continue}, escala_continue, SPACING);
// Countdown number (using constants) // Countdown number (using constants)
const std::string COUNTER_STR = std::to_string(continue_counter_); const std::string COUNTER_STR = std::to_string(continue_counter_);
@@ -918,7 +962,7 @@ void GameScene::drawContinue() {
float centre_y_counter = play_area.y + (play_area.h * y_ratio_counter); float centre_y_counter = play_area.y + (play_area.h * y_ratio_counter);
text_.renderCentered(COUNTER_STR, {.x = centre_x, .y = centre_y_counter}, escala_counter, SPACING); text_.renderCentered(COUNTER_STR, {.x = center_x, .y = centre_y_counter}, escala_counter, SPACING);
// "CONTINUES LEFT" (conditional + using constants) // "CONTINUES LEFT" (conditional + using constants)
if (!Defaults::Game::INFINITE_CONTINUES) { if (!Defaults::Game::INFINITE_CONTINUES) {
@@ -928,16 +972,16 @@ void GameScene::drawContinue() {
float centre_y_info = play_area.y + (play_area.h * y_ratio_info); float centre_y_info = play_area.y + (play_area.h * y_ratio_info);
text_.renderCentered(CONTINUES_TEXT, {.x = centre_x, .y = centre_y_info}, escala_info, SPACING); text_.renderCentered(CONTINUES_TEXT, {.x = center_x, .y = centre_y_info}, escala_info, SPACING);
} }
} }
void GameScene::joinPlayer(uint8_t player_id) { void GameScene::joinPlayer(uint8_t player_id) {
// Activate player // Activate player
if (player_id == 0) { if (player_id == 0) {
match_config_.jugador1_actiu = true; match_config_.player1_active = true;
} else { } else {
match_config_.jugador2_actiu = true; match_config_.player2_active = true;
} }
// Reset stats // Reset stats
+5 -1
View File
@@ -10,6 +10,7 @@
#include "core/graphics/border.hpp" #include "core/graphics/border.hpp"
#include "core/graphics/playfield.hpp" #include "core/graphics/playfield.hpp"
#include "core/graphics/starfield_parallax.hpp"
#include "core/graphics/vector_text.hpp" #include "core/graphics/vector_text.hpp"
#include "core/physics/physics_world.hpp" #include "core/physics/physics_world.hpp"
#include "core/rendering/sdl_manager.hpp" #include "core/rendering/sdl_manager.hpp"
@@ -66,7 +67,7 @@ class GameScene final : public Scene {
std::array<Enemy, Constants::MAX_ORNIS> enemies_; std::array<Enemy, Constants::MAX_ORNIS> enemies_;
// 6 balas: P1=[0,1,2], P2=[3,4,5]. El cast a size_t evita la // 6 balas: P1=[0,1,2], P2=[3,4,5]. El cast a size_t evita la
// widening conversion implícita que detecta clang-tidy. // widening conversion implícita que detecta clang-tidy.
std::array<Bullet, static_cast<std::size_t>(Constants::MAX_BALES) * 2> bullets_; std::array<Bullet, static_cast<std::size_t>(Constants::MAX_BULLETS) * 2> bullets_;
std::array<float, 2> hit_timer_per_player_; // Death timers per player (seconds) std::array<float, 2> hit_timer_per_player_; // Death timers per player (seconds)
// Lives and game over system // Lives and game over system
@@ -82,6 +83,9 @@ class GameScene final : public Scene {
// Text vectorial // Text vectorial
Graphics::VectorText text_; Graphics::VectorText text_;
// Capa més profunda del fons: estrelles 2D amb parallax (estàtiques de moment).
Graphics::StarfieldParallax starfield_parallax_;
// Fons del playfield (graella + futures capes) // Fons del playfield (graella + futures capes)
Graphics::Playfield playfield_; Graphics::Playfield playfield_;
+96 -97
View File
@@ -20,18 +20,18 @@ using SceneManager::SceneContext;
using SceneType = SceneContext::SceneType; using SceneType = SceneContext::SceneType;
using Option = SceneContext::Option; using Option = SceneContext::Option;
// Helper: calcular el progrés individual de una lletra // Helper: calcular el progrés individual de una letter
// en función del progrés global (efecte seqüencial) // en función del progrés global (efecte seqüencial)
static auto computeLetterProgress(size_t letra_index, size_t num_letras, float global_progress, float threshold) -> float { static auto computeLetterProgress(size_t letter_index, size_t num_letters, float global_progress, float threshold) -> float {
if (num_letras == 0) { if (num_letters == 0) {
return 1.0F; return 1.0F;
} }
// Calcular time per lletra // Calcular time per letter
float duration_per_letra = 1.0F / static_cast<float>(num_letras); float duration_per_letter = 1.0F / static_cast<float>(num_letters);
float step = threshold * duration_per_letra; float step = threshold * duration_per_letter;
float start = static_cast<float>(letra_index) * step; float start = static_cast<float>(letter_index) * step;
float end = start + duration_per_letra; float end = start + duration_per_letter;
// Interpolar progrés // Interpolar progrés
if (global_progress < start) { if (global_progress < start) {
@@ -47,15 +47,14 @@ LogoScene::LogoScene(SDLManager& sdl, SceneContext& context)
: sdl_(sdl), : sdl_(sdl),
context_(context), context_(context),
debris_manager_(std::make_unique<Effects::DebrisManager>(sdl.getRenderer())) debris_manager_(std::make_unique<Effects::DebrisManager>(sdl.getRenderer())) {
{
std::cout << "SceneType Logo: Inicialitzant...\n"; std::cout << "SceneType Logo: Inicialitzant...\n";
// Consumir opciones (LOGO no processa opciones actualment) // Consumir opciones (LOGO no processa opciones actualment)
auto option = context_.consumeOption(); auto option = context_.consumeOption();
(void)option; // Suprimir warning (void)option; // Suprimir warning
so_reproduit_.fill(false); // Inicialitzar seguiment de sons sound_played_.fill(false); // Inicialitzar seguiment de sons
initLetters(); initLetters();
} }
@@ -91,7 +90,7 @@ void LogoScene::initLetters() {
"logo/letra_s.shp"}; "logo/letra_s.shp"};
// Pas 1: Carregar todas las formes i calcular amplades // Pas 1: Carregar todas las formes i calcular amplades
float ancho_total = 0.0F; float total_width = 0.0F;
for (const auto& file : archivos) { for (const auto& file : archivos) {
auto shape = ShapeLoader::load(file); auto shape = ShapeLoader::load(file);
@@ -111,66 +110,66 @@ void LogoScene::initLetters() {
} }
} }
float ancho_sin_escalar = max_x - min_x; float width_unscaled = max_x - min_x;
// IMPORTANT: Escalar ancho i offset con ESCALA_FINAL // IMPORTANT: Escalar ancho i offset con FINAL_SCALE
// per que las posicions finals coincideixin con la mida real de las lletres // per que las posicions finals coincideixin con la mida real de las lletres
float ancho = ancho_sin_escalar * ESCALA_FINAL; float width = width_unscaled * FINAL_SCALE;
float offset_centre = (shape->getCenter().x - min_x) * ESCALA_FINAL; float center_offset = (shape->getCenter().x - min_x) * FINAL_SCALE;
lletres_.push_back({shape, letters_.push_back({shape,
{.x = 0.0F, .y = 0.0F}, // Posición es calcularà después {.x = 0.0F, .y = 0.0F}, // Posición es calcularà después
ancho, width,
offset_centre}); center_offset});
ancho_total += ancho; total_width += width;
} }
// Pas 2: Añadir espaiat entre lletres // Pas 2: Añadir espaiat entre lletres
ancho_total += ESPAI_ENTRE_LLETRES * (lletres_.size() - 1); total_width += LETTER_SPACING * (letters_.size() - 1);
// Pas 3: Calcular posición inicial (centrat horitzontal) // Pas 3: Calcular posición inicial (centrat horitzontal)
constexpr auto PANTALLA_ANCHO = static_cast<float>(Defaults::Game::WIDTH); constexpr auto SCREEN_WIDTH = static_cast<float>(Defaults::Game::WIDTH);
constexpr auto PANTALLA_ALTO = static_cast<float>(Defaults::Game::HEIGHT); constexpr auto PANTALLA_ALTO = static_cast<float>(Defaults::Game::HEIGHT);
float x_inicial = (PANTALLA_ANCHO - ancho_total) / 2.0F; float x_inicial = (SCREEN_WIDTH - total_width) / 2.0F;
float y_centre = PANTALLA_ALTO / 2.0F; float y_centre = PANTALLA_ALTO / 2.0F;
// Pas 4: Assignar posicions a cada lletra // Pas 4: Assignar posicions a cada letter
float x_actual = x_inicial; float x_actual = x_inicial;
for (auto& lletra : lletres_) { for (auto& letter : letters_) {
// Posicionar el centro de la shape (shape_centre) en pantalla // Posicionar el centro de la shape (shape_centre) en pantalla
// Usar offset_centre en lloc de ancho/2 perquè shape_centre // Usar center_offset en lloc de ancho/2 perquè shape_centre
// pot no estar exactament al mig del bounding box // pot no estar exactament al mig del bounding box
lletra.position.x = x_actual + lletra.offset_centre; letter.position.x = x_actual + letter.center_offset;
lletra.position.y = y_centre; letter.position.y = y_centre;
// Avançar para següent lletra // Avançar para següent letter
x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES; x_actual += letter.width + LETTER_SPACING;
} }
std::cout << "[LogoScene] " << lletres_.size() std::cout << "[LogoScene] " << letters_.size()
<< " lletres carregades, ancho total: " << ancho_total << " px\n"; << " lletres carregades, ancho total: " << total_width << " px\n";
} }
void LogoScene::changeState(AnimationState nou_estat) { void LogoScene::changeState(AnimationState nou_estat) {
estat_actual_ = nou_estat; current_state_ = nou_estat;
temps_estat_actual_ = 0.0F; // Reset time temps_current_state_ = 0.0F; // Reset time
// Inicialitzar state de explosión // Inicialitzar state de explosión
if (nou_estat == AnimationState::EXPLOSION) { if (nou_estat == AnimationState::EXPLOSION) {
lletra_explosio_index_ = 0; letter_explosion_index_ = 0;
temps_des_ultima_explosio_ = 0.0F; time_since_last_explosion_ = 0.0F;
// Generar ordre aleatori de explosions // Generar ordre aleatori de explosions
ordre_explosio_.clear(); explosion_order_.clear();
for (size_t i = 0; i < lletres_.size(); i++) { for (size_t i = 0; i < letters_.size(); i++) {
ordre_explosio_.push_back(i); explosion_order_.push_back(i);
} }
std::random_device rd; std::random_device rd;
std::mt19937 g(rd()); std::mt19937 g(rd());
std::shuffle(ordre_explosio_.begin(), ordre_explosio_.end(), g); std::shuffle(explosion_order_.begin(), explosion_order_.end(), g);
} else if (nou_estat == AnimationState::POST_EXPLOSION) { } else if (nou_estat == AnimationState::POST_EXPLOSION) {
Audio::get()->playMusic("title.ogg"); Audio::get()->playMusic("title.ogg");
} }
@@ -180,35 +179,35 @@ void LogoScene::changeState(AnimationState nou_estat) {
} }
auto LogoScene::allLettersComplete() const -> bool { auto LogoScene::allLettersComplete() const -> bool {
// Cuando global_progress = 1.0, todas las lletres tenen letra_progress = 1.0 // Cuando global_progress = 1.0, todas las lletres tenen letter_progress = 1.0
return temps_estat_actual_ >= DURACIO_ZOOM; return temps_current_state_ >= DURATION_ZOOM;
} }
void LogoScene::updateExplosions(float delta_time) { void LogoScene::updateExplosions(float delta_time) {
temps_des_ultima_explosio_ += delta_time; time_since_last_explosion_ += delta_time;
// Comprovar si es el moment de explode la següent lletra // Comprovar si es el moment de explode la següent letter
if (temps_des_ultima_explosio_ >= DELAY_ENTRE_EXPLOSIONS) { if (time_since_last_explosion_ >= DELAY_ENTRE_EXPLOSIONS) {
if (lletra_explosio_index_ < lletres_.size()) { if (letter_explosion_index_ < letters_.size()) {
// Explotar lletra actual (en ordre aleatori) // Explotar letter actual (en ordre aleatori)
size_t index_actual = ordre_explosio_[lletra_explosio_index_]; size_t index_actual = explosion_order_[letter_explosion_index_];
const auto& lletra = lletres_[index_actual]; const auto& letter = letters_[index_actual];
debris_manager_->explode( debris_manager_->explode(
lletra.shape, // Forma a explode letter.shape, // Forma a explode
lletra.position, // Posición letter.position, // Posición
0.0F, // Angle (sin rotación) 0.0F, // Angle (sin rotación)
ESCALA_FINAL, // Escala (lletres a scale final) FINAL_SCALE, // Escala (lletres a scale final)
VELOCITAT_EXPLOSIO, // Velocidad base SPEED_EXPLOSIO, // Velocidad base
1.0F, // Brightness màxim (per defecte) 1.0F, // Brightness màxim (per defecte)
{.x = 0.0F, .y = 0.0F} // Sin velocity (per defecte) {.x = 0.0F, .y = 0.0F} // Sin velocity (per defecte)
); );
std::cout << "[LogoScene] Explota lletra " << lletra_explosio_index_ << "\n"; std::cout << "[LogoScene] Explota letter " << letter_explosion_index_ << "\n";
// Passar a la següent lletra // Passar a la següent letter
lletra_explosio_index_++; letter_explosion_index_++;
temps_des_ultima_explosio_ = 0.0F; time_since_last_explosion_ = 0.0F;
} else { } else {
// Todas las lletres han explotat, transición a POST_EXPLOSION // Todas las lletres han explotat, transición a POST_EXPLOSION
changeState(AnimationState::POST_EXPLOSION); changeState(AnimationState::POST_EXPLOSION);
@@ -217,31 +216,31 @@ void LogoScene::updateExplosions(float delta_time) {
} }
void LogoScene::update(float delta_time) { void LogoScene::update(float delta_time) {
temps_estat_actual_ += delta_time; temps_current_state_ += delta_time;
switch (estat_actual_) { switch (current_state_) {
case AnimationState::PRE_ANIMATION: case AnimationState::PRE_ANIMATION:
if (temps_estat_actual_ >= DURACIO_PRE) { if (temps_current_state_ >= DURATION_PRE) {
changeState(AnimationState::ANIMATION); changeState(AnimationState::ANIMATION);
} }
break; break;
case AnimationState::ANIMATION: { case AnimationState::ANIMATION: {
// Reproduir so per cada lletra cuando comença a aparèixer // Reproduir so per cada letter cuando comença a aparèixer
float global_progress = std::min(temps_estat_actual_ / DURACIO_ZOOM, 1.0F); float global_progress = std::min(temps_current_state_ / DURATION_ZOOM, 1.0F);
for (size_t i = 0; i < lletres_.size() && i < so_reproduit_.size(); i++) { for (size_t i = 0; i < letters_.size() && i < sound_played_.size(); i++) {
if (!so_reproduit_[i]) { if (!sound_played_[i]) {
float letra_progress = computeLetterProgress( float letter_progress = computeLetterProgress(
i, i,
lletres_.size(), letters_.size(),
global_progress, global_progress,
THRESHOLD_LETRA); LETTER_THRESHOLD);
// Reproduir so cuando la lletra comença a aparèixer (progress > 0) // Reproduir so cuando la letter comença a aparèixer (progress > 0)
if (letra_progress > 0.0F) { if (letter_progress > 0.0F) {
Audio::get()->playSound(Defaults::Sound::LOGO, Audio::Group::GAME); Audio::get()->playSound(Defaults::Sound::LOGO, Audio::Group::GAME);
so_reproduit_[i] = true; sound_played_[i] = true;
} }
} }
} }
@@ -253,7 +252,7 @@ void LogoScene::update(float delta_time) {
} }
case AnimationState::POST_ANIMATION: case AnimationState::POST_ANIMATION:
if (temps_estat_actual_ >= DURACIO_POST_ANIMATION) { if (temps_current_state_ >= DURATION_POST_ANIMATION) {
changeState(AnimationState::EXPLOSION); changeState(AnimationState::EXPLOSION);
} }
break; break;
@@ -263,7 +262,7 @@ void LogoScene::update(float delta_time) {
break; break;
case AnimationState::POST_EXPLOSION: case AnimationState::POST_EXPLOSION:
if (temps_estat_actual_ >= DURACIO_POST_EXPLOSION) { if (temps_current_state_ >= DURATION_POST_EXPLOSION) {
// Transición a pantalla de título // Transición a pantalla de título
context_.setNextScene(SceneType::TITLE); context_.setNextScene(SceneType::TITLE);
} }
@@ -283,47 +282,47 @@ void LogoScene::draw() {
// Director ha hecho el clear; aquí solo pintamos lo de la escena. // Director ha hecho el clear; aquí solo pintamos lo de la escena.
// PRE_ANIMATION: Solo pantalla negra (no se pinta nada). // PRE_ANIMATION: Solo pantalla negra (no se pinta nada).
if (estat_actual_ == AnimationState::PRE_ANIMATION) { if (current_state_ == AnimationState::PRE_ANIMATION) {
return; return;
} }
// ANIMATION o POST_ANIMATION: Dibuixar lletres con animación // ANIMATION o POST_ANIMATION: Dibuixar lletres con animación
if (estat_actual_ == AnimationState::ANIMATION || if (current_state_ == AnimationState::ANIMATION ||
estat_actual_ == AnimationState::POST_ANIMATION) { current_state_ == AnimationState::POST_ANIMATION) {
float global_progress = float global_progress =
(estat_actual_ == AnimationState::ANIMATION) (current_state_ == AnimationState::ANIMATION)
? std::min(temps_estat_actual_ / DURACIO_ZOOM, 1.0F) ? std::min(temps_current_state_ / DURATION_ZOOM, 1.0F)
: 1.0F; // POST: mantenir al 100% : 1.0F; // POST: mantenir al 100%
const Vec2 ORIGEN_ZOOM = {.x = ORIGEN_ZOOM_X, .y = ORIGEN_ZOOM_Y}; const Vec2 ZOOM_ORIGIN = {.x = ZOOM_ORIGIN_X, .y = ZOOM_ORIGIN_Y};
for (size_t i = 0; i < lletres_.size(); i++) { for (size_t i = 0; i < letters_.size(); i++) {
const auto& lletra = lletres_[i]; const auto& letter = letters_[i];
float letra_progress = computeLetterProgress( float letter_progress = computeLetterProgress(
i, i,
lletres_.size(), letters_.size(),
global_progress, global_progress,
THRESHOLD_LETRA); LETTER_THRESHOLD);
if (letra_progress <= 0.0F) { if (letter_progress <= 0.0F) {
continue; continue;
} }
Vec2 pos_actual; Vec2 pos_actual;
pos_actual.x = pos_actual.x =
ORIGEN_ZOOM.x + ((lletra.position.x - ORIGEN_ZOOM.x) * letra_progress); ZOOM_ORIGIN.x + ((letter.position.x - ZOOM_ORIGIN.x) * letter_progress);
pos_actual.y = pos_actual.y =
ORIGEN_ZOOM.y + ((lletra.position.y - ORIGEN_ZOOM.y) * letra_progress); ZOOM_ORIGIN.y + ((letter.position.y - ZOOM_ORIGIN.y) * letter_progress);
float t = letra_progress; float t = letter_progress;
float ease_factor = 1.0F - ((1.0F - t) * (1.0F - t)); float ease_factor = 1.0F - ((1.0F - t) * (1.0F - t));
float current_scale = float current_scale =
ESCALA_INICIAL + ((ESCALA_FINAL - ESCALA_INICIAL) * ease_factor); INITIAL_SCALE + ((FINAL_SCALE - INITIAL_SCALE) * ease_factor);
Rendering::renderShape( Rendering::renderShape(
sdl_.getRenderer(), sdl_.getRenderer(),
lletra.shape, letter.shape,
pos_actual, pos_actual,
0.0F, 0.0F,
current_scale, current_scale,
@@ -332,24 +331,24 @@ void LogoScene::draw() {
} }
// EXPLOSION: Dibuixar solo lletres que aún no han explotat // EXPLOSION: Dibuixar solo lletres que aún no han explotat
if (estat_actual_ == AnimationState::EXPLOSION) { if (current_state_ == AnimationState::EXPLOSION) {
// Crear conjunt de lletres ya explotades // Crear conjunt de lletres ya explotades
std::set<size_t> explotades; std::set<size_t> explotades;
for (size_t i = 0; i < lletra_explosio_index_; i++) { for (size_t i = 0; i < letter_explosion_index_; i++) {
explotades.insert(ordre_explosio_[i]); explotades.insert(explosion_order_[i]);
} }
// Dibuixar solo lletres que NO han explotat // Dibuixar solo lletres que NO han explotat
for (size_t i = 0; i < lletres_.size(); i++) { for (size_t i = 0; i < letters_.size(); i++) {
if (!explotades.contains(i)) { if (!explotades.contains(i)) {
const auto& lletra = lletres_[i]; const auto& letter = letters_[i];
Rendering::renderShape( Rendering::renderShape(
sdl_.getRenderer(), sdl_.getRenderer(),
lletra.shape, letter.shape,
lletra.position, letter.position,
0.0F, 0.0F,
ESCALA_FINAL, FINAL_SCALE,
1.0F); 1.0F);
} }
} }
+60 -60
View File
@@ -20,76 +20,76 @@
#include "game/effects/debris_manager.hpp" #include "game/effects/debris_manager.hpp"
class LogoScene final : public Scene { class LogoScene final : public Scene {
public: public:
explicit LogoScene(SDLManager& sdl, SceneManager::SceneContext& context); explicit LogoScene(SDLManager& sdl, SceneManager::SceneContext& context);
~LogoScene() override; // Destructor per aturar sons ~LogoScene() override; // Destructor per aturar sons
// Scene interface // Scene interface
void handleEvent(const SDL_Event& event) override; void handleEvent(const SDL_Event& event) override;
void update(float delta_time) override; void update(float delta_time) override;
void draw() override; void draw() override;
[[nodiscard]] auto isFinished() const -> bool override; [[nodiscard]] auto isFinished() const -> bool override;
private: private:
// Màquina de estats per l'animación // Màquina de estats per l'animación
enum class AnimationState : std::uint8_t { enum class AnimationState : std::uint8_t {
PRE_ANIMATION, // Pantalla negra inicial PRE_ANIMATION, // Pantalla negra inicial
ANIMATION, // Animación de zoom de lletres ANIMATION, // Animación de zoom de lletres
POST_ANIMATION, // Logo complet visible POST_ANIMATION, // Logo complet visible
EXPLOSION, // Explosión seqüencial de lletres EXPLOSION, // Explosión seqüencial de lletres
POST_EXPLOSION // Espera después de l'última explosión POST_EXPLOSION // Espera después de l'última explosión
}; };
SDLManager& sdl_; SDLManager& sdl_;
SceneManager::SceneContext& context_; SceneManager::SceneContext& context_;
AnimationState estat_actual_{AnimationState::PRE_ANIMATION}; // Estat actual de la màquina AnimationState current_state_{AnimationState::PRE_ANIMATION}; // Estat actual de la màquina
float float
temps_estat_actual_{0.0F}; // Temps en l'state actual (reset en cada transición) temps_current_state_{0.0F}; // Temps en l'state actual (reset en cada transición)
// Gestor de fragments de explosions // Gestor de fragments de explosions
std::unique_ptr<Effects::DebrisManager> debris_manager_; std::unique_ptr<Effects::DebrisManager> debris_manager_;
// Seguiment de explosions seqüencials // Seguiment de explosions seqüencials
size_t lletra_explosio_index_{0}; // Índex de la següent lletra a explode size_t letter_explosion_index_{0}; // Índex de la següent letter a explode
float temps_des_ultima_explosio_{0.0F}; // Temps desde l'última explosión float time_since_last_explosion_{0.0F}; // Temps desde l'última explosión
std::vector<size_t> ordre_explosio_; // Ordre aleatori de índexs de lletres std::vector<size_t> explosion_order_; // Ordre aleatori de índexs de lletres
// Estructura para cada lletra del logo // Estructura para cada letter del logo
struct LetraLogo { struct LogoLetter {
std::shared_ptr<Graphics::Shape> shape; std::shared_ptr<Graphics::Shape> shape;
Vec2 position; // Posición final en pantalla Vec2 position; // Posición final en pantalla
float ancho; // Ancho del bounding box float width; // Ancho del bounding box
float offset_centre; // Distancia de min_x a shape_centre.x float center_offset; // Distancia de min_x a shape_centre.x
}; };
std::vector<LetraLogo> lletres_; // 9 lletres: J-A-I-L-G-A-M-E-S std::vector<LogoLetter> letters_; // 9 lletres: J-A-I-L-G-A-M-E-S
// Seguiment de sons de lletres (evitar reproduccions repetides) // Seguiment de sons de lletres (evitar reproduccions repetides)
std::array<bool, 9> so_reproduit_; // Track si cada lletra ya ha reproduit el so std::array<bool, 9> sound_played_; // Track si cada letter ya ha reproduit el so
// Constants de animación // Constants de animación
static constexpr float DURACIO_PRE = 1.5F; // Duració PRE_ANIMATION (pantalla negra) static constexpr float DURATION_PRE = 1.5F; // Duració PRE_ANIMATION (pantalla negra)
static constexpr float DURACIO_ZOOM = 4.0F; // Duració del zoom (segons) static constexpr float DURATION_ZOOM = 4.0F; // Duració del zoom (segons)
static constexpr float DURACIO_POST_ANIMATION = 3.0F; // Duració POST_ANIMATION (logo complet) static constexpr float DURATION_POST_ANIMATION = 3.0F; // Duració POST_ANIMATION (logo complet)
static constexpr float DURACIO_POST_EXPLOSION = 3.0F; // Duració POST_EXPLOSION (espera final) static constexpr float DURATION_POST_EXPLOSION = 3.0F; // Duració POST_EXPLOSION (espera final)
static constexpr float DELAY_ENTRE_EXPLOSIONS = 0.1F; // Temps entre explosions de lletres static constexpr float DELAY_ENTRE_EXPLOSIONS = 0.1F; // Temps entre explosions de lletres
static constexpr float VELOCITAT_EXPLOSIO = 240.0F; // Velocidad base fragments (px/s) static constexpr float SPEED_EXPLOSIO = 240.0F; // Velocidad base fragments (px/s)
static constexpr float ESCALA_INICIAL = 0.1F; // Escala inicial (10%) static constexpr float INITIAL_SCALE = 0.1F; // Escala inicial (10%)
static constexpr float ESCALA_FINAL = 0.8F; // Escala final (80%) static constexpr float FINAL_SCALE = 0.8F; // Escala final (80%)
static constexpr float ESPAI_ENTRE_LLETRES = 10.0F; // Espaiat entre lletres static constexpr float LETTER_SPACING = 10.0F; // Espaiat entre lletres
// Constants de animación seqüencial // Constants de animación seqüencial
static constexpr float THRESHOLD_LETRA = 0.6F; // Umbral per activar següent lletra (0.0-1.0) static constexpr float LETTER_THRESHOLD = 0.6F; // Umbral per activar següent letter (0.0-1.0)
static constexpr float ORIGEN_ZOOM_X = Defaults::Game::WIDTH * 0.5F; // Vec2 inicial X del zoom static constexpr float ZOOM_ORIGIN_X = Defaults::Game::WIDTH * 0.5F; // Vec2 inicial X del zoom
static constexpr float ORIGEN_ZOOM_Y = Defaults::Game::HEIGHT * 0.4F; // Vec2 inicial Y del zoom static constexpr float ZOOM_ORIGIN_Y = Defaults::Game::HEIGHT * 0.4F; // Vec2 inicial Y del zoom
// Métodos privats // Métodos privats
void initLetters(); void initLetters();
void updateExplosions(float delta_time); void updateExplosions(float delta_time);
// Estático: solo consulta Input (singleton), no estado de la escena. // Estático: solo consulta Input (singleton), no estado de la escena.
static auto checkSkipButtonPressed() -> bool; static auto checkSkipButtonPressed() -> bool;
// Métodos de gestió de estats // Métodos de gestió de estats
void changeState(AnimationState nou_estat); void changeState(AnimationState nou_estat);
[[nodiscard]] auto allLettersComplete() const -> bool; [[nodiscard]] auto allLettersComplete() const -> bool;
}; };
File diff suppressed because it is too large Load Diff
+107 -98
View File
@@ -1,6 +1,12 @@
// title_scene.hpp - Pantalla de título del juego // title_scene.hpp - Escena de títol en 3D real
// Muestra message "PRESS BUTTON TO PLAY" y copyright
// © 2026 JailDesigner // © 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 #pragma once
@@ -11,125 +17,128 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#include "core/graphics/camera3d.hpp"
#include "core/graphics/shape.hpp" #include "core/graphics/shape.hpp"
#include "core/graphics/starfield.hpp" #include "core/graphics/starfield.hpp"
#include "core/graphics/vector_text.hpp" #include "core/graphics/vector_text.hpp"
#include "core/input/input_types.hpp" #include "core/input/input_types.hpp"
#include "core/rendering/sdl_manager.hpp" #include "core/rendering/sdl_manager.hpp"
#include "core/system/game_config.hpp"
#include "core/system/scene.hpp" #include "core/system/scene.hpp"
#include "core/system/scene_context.hpp" #include "core/system/scene_context.hpp"
#include "core/system/game_config.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/title/ship_animator.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 { class TitleScene final : public Scene {
public: public:
explicit TitleScene(SDLManager& sdl, SceneManager::SceneContext& context); explicit TitleScene(SDLManager& sdl, SceneManager::SceneContext& context);
~TitleScene() override; // Destructor per aturar música ~TitleScene() override;
// Scene interface void handleEvent(const SDL_Event& event) override;
void handleEvent(const SDL_Event& event) override; void update(float delta_time) override;
void update(float delta_time) override; void draw() override;
void draw() override; [[nodiscard]] auto isFinished() const -> bool override;
[[nodiscard]] auto isFinished() const -> bool override;
private: private:
// Màquina de estats per la pantalla de título enum class TitleState : std::uint8_t {
enum class TitleState : std::uint8_t { STARFIELD_FADE_IN,
STARFIELD_FADE_IN, // Fade-in del starfield (3.0s) STARFIELD,
STARFIELD, // Pantalla con camp de estrelles (4.0s) MAIN,
MAIN, // Pantalla de título con text (indefinit, hasta START) PLAYER_JOIN_PHASE,
PLAYER_JOIN_PHASE, // Fase de unió de jugadors: fade-out música + text parpellejant (2.5s) BLACK_SCREEN,
BLACK_SCREEN // Pantalla negra de transición (2.0s) };
};
// Estructura per emmagatzemar informació de cada lletra del título struct LogoLetter {
struct LetraLogo { std::shared_ptr<Graphics::Shape> shape;
std::shared_ptr<Graphics::Shape> shape; // Forma vectorial de la lletra Vec2 position;
Vec2 position; // Posición en pantalla float width;
float ancho; // Amplada scaled float height;
float altura; // Altura scaled float center_offset;
float offset_centre; // Offset del centro per posicionament };
};
SDLManager& sdl_; SDLManager& sdl_;
SceneManager::SceneContext& context_; SceneManager::SceneContext& context_;
GameConfig::MatchConfig match_config_; // Configuración de jugadors active GameConfig::MatchConfig match_config_;
Graphics::VectorText text_; // Sistema de text vectorial Graphics::VectorText text_;
std::unique_ptr<Graphics::Starfield> starfield_; // Camp de estrelles de fons std::unique_ptr<Graphics::Camera3D> camera_;
std::unique_ptr<Title::ShipAnimator> ship_animator_; // Naves 3D flotantes std::unique_ptr<Graphics::Starfield> starfield_;
TitleState estat_actual_{TitleState::STARFIELD_FADE_IN}; // Estat actual de la màquina std::unique_ptr<Title::ShipAnimator> ship_animator_;
float temps_acumulat_{0.0F}; // Temps acumulat per l'state INIT
// Lletres del título "ORNI ATTACK!" // Destell que tapa el "pop" final de cada nau quan arriba al VP.
std::vector<LetraLogo> lletres_orni_; // Lletres de "ORNI" (línia 1) // Pool fix de 2 (una per nau). Anima escala 0→max→0.
std::vector<LetraLogo> lletres_attack_; // Lletres de "ATTACK!" (línia 2) struct Flash {
float y_attack_dinamica_; // Posición Y calculada dinàmicament per "ATTACK!" Vec2 position{};
float timer{0.0F};
bool active{false};
};
std::array<Flash, 2> flashes_{};
std::shared_ptr<Graphics::Shape> flash_shape_;
// Logo "JAILGAMES" pequeño sobre el copyright (esquinas inferiores del título). void triggerFlash(Vec2 pos);
std::vector<LetraLogo> lletres_jailgames_; void updateFlashes(float delta_time);
void drawFlashes();
TitleState current_state_{TitleState::STARFIELD_FADE_IN};
float temps_acumulat_{0.0F};
// Estat de animación del logo std::vector<LogoLetter> letters_orni_;
float temps_animacio_{0.0F}; // Temps acumulat per animación orbital std::vector<LogoLetter> letters_attack_;
std::vector<Vec2> posicions_originals_orni_; // Posicions originals de "ORNI" float dynamic_attack_y_{0.0F};
std::vector<Vec2> posicions_originals_attack_; // Posicions originals de "ATTACK!"
// Estat de arrencada de l'animación std::vector<LogoLetter> letters_jailgames_;
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)
// Constants float animation_time_{0.0F};
static constexpr float BRIGHTNESS_STARFIELD = 1.2F; // Brightness del starfield (>1.0 = més brillant) std::vector<Vec2> original_positions_orni_;
static constexpr float DURACIO_FADE_IN = 3.0F; // Duració del fade-in del starfield (1.5 segons) std::vector<Vec2> original_positions_attack_;
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
// Constants de animación del logo float state_time_main_{0.0F};
static constexpr float ORBIT_AMPLITUDE_X = 4.0F; // Amplitud oscil·lació horitzontal (píxels) bool animation_active_{false};
static constexpr float ORBIT_AMPLITUDE_Y = 3.0F; // Amplitud oscil·lació vertical (píxels) float lerp_factor_{0.0F};
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)
// Constants de ombra del logo // Progresos de la intro coreografiada al state MAIN.
static constexpr float SHADOW_DELAY = 0.5F; // Retard temporal de l'ombra (segons) float intro_logo_progress_{0.0F};
static constexpr float SHADOW_BRIGHTNESS = 0.4F; // Multiplicador de brightness de l'ombra (0.0-1.0) float intro_jailgames_progress_{0.0F};
static constexpr float SHADOW_OFFSET_X = 2.0F; // Offset espacial X fix (píxels) float intro_copyright_progress_{0.0F};
static constexpr float SHADOW_OFFSET_Y = 2.0F; // Offset espacial Y fix (píxels) bool press_start_visible_{false};
bool ships_intro_launched_{false};
// Temporització de l'arrencada de l'animación static constexpr float BRIGHTNESS_STARFIELD = 1.2F;
static constexpr float DELAY_INICI_ANIMACIO = 10.0F; // 10s estàtic antes de animar static constexpr float DURATION_FADE_IN = 3.0F;
static constexpr float DURACIO_LERP = 2.0F; // 2s per arribar a amplitud completa static constexpr float DURATION_INIT = 4.0F;
static constexpr float DURATION_TRANSITION = 2.5F;
static constexpr float LETTER_SPACING = 10.0F;
static constexpr float BLINK_FREQUENCY = 3.0F;
static constexpr float DURATION_BLACK_SCREEN = 2.0F;
static constexpr int MUSIC_FADE = 1500;
// Métodos privats static constexpr float ORBIT_AMPLITUDE_X = 4.0F;
void updateLogoAnimation(float delta_time); // Actualitza l'animación orbital del logo static constexpr float ORBIT_AMPLITUDE_Y = 3.0F;
// Estático: solo consulta Input (singleton), no estado de la escena. static constexpr float ORBIT_FREQUENCY_X = 0.8F;
static auto checkSkipButtonPressed() -> bool; static constexpr float ORBIT_FREQUENCY_Y = 1.2F;
auto checkStartGameButtonPressed() -> bool; static constexpr float ORBIT_PHASE_OFFSET = 1.57F;
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
// Sub-pasos de update() (extreure cada state per reduir complexitat). static constexpr float SHADOW_DELAY = 0.5F;
void updateStarfieldFadeInState(float delta_time); static constexpr float SHADOW_BRIGHTNESS = 0.4F;
void updateStarfieldState(float delta_time); static constexpr float SHADOW_OFFSET_X = 2.0F;
void updateMainState(float delta_time); static constexpr float SHADOW_OFFSET_Y = 2.0F;
void updatePlayerJoinPhaseState(float delta_time);
void updateBlackScreenState(float delta_time); static constexpr float DURATION_LERP = 2.0F;
// Handlers de input globals (independents de l'state actual).
void handleSkipInput(); // Càmera 3D: FOV vertical en radians.
void handleStartInput(); static constexpr float CAMERA_FOV_Y_RAD = 1.0472F; // 60°
// 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 updateLogoAnimation(float delta_time);
void triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, static auto checkSkipButtonPressed() -> bool;
const char* log_prefix); 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);
}; };
-547
View File
@@ -1,547 +0,0 @@
// title_scene_3d.cpp - Implementació de l'escena de títol 3D real
// © 2026 JailDesigner
#include "title_scene_3d.hpp"
#include <algorithm>
#include <cfloat>
#include <cmath>
#include <iostream>
#include <numbers>
#include <string>
#include "core/audio/audio.hpp"
#include "core/defaults.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/input/input.hpp"
#include "core/rendering/shape_renderer.hpp"
#include "core/system/scene_context.hpp"
#include "project.h"
using SceneManager::SceneContext;
using SceneType = SceneContext::SceneType;
using Option = SceneContext::Option;
namespace {
// Botons per iniciar partida des de MAIN (només START). Duplicat del que viu
// al `title_scene.hpp` perquè no volem un acoblament entre la versió 2D i la
// 3D mentre conviuen.
constexpr std::array<InputAction, 1> START_GAME_BUTTONS_3D = {InputAction::START};
} // namespace
TitleScene3D::TitleScene3D(SDLManager& sdl, SceneContext& context)
: sdl_(sdl),
context_(context),
text_(sdl.getRenderer()) {
std::cout << "SceneType Titol3D: Inicialitzant...\n";
match_config_.jugador1_actiu = false;
match_config_.jugador2_actiu = false;
match_config_.mode = GameConfig::Mode::NORMAL;
auto option = context_.consumeOption();
if (option == Option::JUMP_TO_TITLE_MAIN) {
std::cout << "SceneType Titol3D: Opció JUMP_TO_TITLE_MAIN activada\n";
estat_actual_ = TitleState::MAIN;
temps_estat_main_ = 0.0F;
}
// Càmera 3D: posicionada a l'origen, mirant cap a +Z, amb Y cap amunt.
camera_ = std::make_unique<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));
starfield_ = std::make_unique<Graphics::Starfield3D>(
sdl_.getRenderer(),
camera_.get(),
200);
if (estat_actual_ == TitleState::MAIN) {
starfield_->setBrightness(BRIGHTNESS_STARFIELD);
} else {
starfield_->setBrightness(0.0F);
}
ship_animator_ = std::make_unique<Title::ShipAnimator3D>(sdl_.getRenderer(), camera_.get());
ship_animator_->init();
if (estat_actual_ == TitleState::MAIN) {
ship_animator_->setVisible(true);
ship_animator_->startEntryAnimation();
} else {
ship_animator_->setVisible(false);
}
initTitle();
inicialitzarJailgames();
if (Audio::getMusicState() != Audio::MusicState::PLAYING) {
Audio::get()->playMusic("title.ogg");
}
}
TitleScene3D::~TitleScene3D() {
Audio::get()->stopMusic();
}
void TitleScene3D::initTitle() {
using namespace Graphics;
const std::vector<std::string> FITXERS_ORNI = {
"title/letra_o.shp",
"title/letra_r.shp",
"title/letra_n.shp",
"title/letra_i.shp"};
float ancho_total_orni = 0.0F;
for (const auto& file : FITXERS_ORNI) {
auto shape = ShapeLoader::load(file);
if (!shape || !shape->isValid()) {
std::cerr << "[TitleScene3D] Error carregant " << file << '\n';
continue;
}
float min_x = FLT_MAX;
float max_x = -FLT_MAX;
float min_y = FLT_MAX;
float max_y = -FLT_MAX;
for (const auto& prim : shape->getPrimitives()) {
for (const auto& point : prim.points) {
min_x = std::min(min_x, point.x);
max_x = std::max(max_x, point.x);
min_y = std::min(min_y, point.y);
max_y = std::max(max_y, point.y);
}
}
const float ANCHO = (max_x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
const float ALTURA = (max_y - min_y) * Defaults::Title::Layout::LOGO_SCALE;
const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
lletres_orni_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ANCHO, ALTURA, OFFSET_CENTRE});
ancho_total_orni += ANCHO;
}
ancho_total_orni += ESPAI_ENTRE_LLETRES * static_cast<float>(lletres_orni_.size() - 1);
float x_actual = (Defaults::Game::WIDTH - ancho_total_orni) / 2.0F;
for (auto& lletra : lletres_orni_) {
lletra.position.x = x_actual + lletra.offset_centre;
lletra.position.y = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS;
x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES;
}
const float ALTURA_ORNI = lletres_orni_.empty() ? 50.0F : lletres_orni_[0].altura;
const float Y_ORNI = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_POS;
const float SEPARACION = Defaults::Game::HEIGHT * Defaults::Title::Layout::LOGO_LINE_SPACING;
y_attack_dinamica_ = Y_ORNI + ALTURA_ORNI + SEPARACION;
const std::vector<std::string> FITXERS_ATTACK = {
"title/letra_a.shp",
"title/letra_t.shp",
"title/letra_t.shp",
"title/letra_a.shp",
"title/letra_c.shp",
"title/letra_k.shp",
"title/letra_exclamacion.shp"};
float ancho_total_attack = 0.0F;
for (const auto& file : FITXERS_ATTACK) {
auto shape = ShapeLoader::load(file);
if (!shape || !shape->isValid()) {
std::cerr << "[TitleScene3D] Error carregant " << file << '\n';
continue;
}
float min_x = FLT_MAX;
float max_x = -FLT_MAX;
float min_y = FLT_MAX;
float max_y = -FLT_MAX;
for (const auto& prim : shape->getPrimitives()) {
for (const auto& point : prim.points) {
min_x = std::min(min_x, point.x);
max_x = std::max(max_x, point.x);
min_y = std::min(min_y, point.y);
max_y = std::max(max_y, point.y);
}
}
const float ANCHO = (max_x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
const float ALTURA = (max_y - min_y) * Defaults::Title::Layout::LOGO_SCALE;
const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * Defaults::Title::Layout::LOGO_SCALE;
lletres_attack_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ANCHO, ALTURA, OFFSET_CENTRE});
ancho_total_attack += ANCHO;
}
ancho_total_attack += ESPAI_ENTRE_LLETRES * static_cast<float>(lletres_attack_.size() - 1);
x_actual = (Defaults::Game::WIDTH - ancho_total_attack) / 2.0F;
for (auto& lletra : lletres_attack_) {
lletra.position.x = x_actual + lletra.offset_centre;
lletra.position.y = y_attack_dinamica_;
x_actual += lletra.ancho + ESPAI_ENTRE_LLETRES;
}
posicions_originals_orni_.clear();
for (const auto& lletra : lletres_orni_) {
posicions_originals_orni_.push_back(lletra.position);
}
posicions_originals_attack_.clear();
for (const auto& lletra : lletres_attack_) {
posicions_originals_attack_.push_back(lletra.position);
}
}
void TitleScene3D::inicialitzarJailgames() {
using namespace Graphics;
const std::vector<std::string> FITXERS = {
"logo/letra_j.shp",
"logo/letra_a.shp",
"logo/letra_i.shp",
"logo/letra_l.shp",
"logo/letra_g.shp",
"logo/letra_a.shp",
"logo/letra_m.shp",
"logo/letra_e.shp",
"logo/letra_s.shp"};
constexpr float SCALE = Defaults::Title::Layout::JAILGAMES_SCALE;
float ancho_total = 0.0F;
float altura_max = 0.0F;
for (const auto& file : FITXERS) {
auto shape = ShapeLoader::load(file);
if (!shape || !shape->isValid()) {
std::cerr << "[TitleScene3D] Error carregant " << file << '\n';
continue;
}
float min_x = FLT_MAX;
float max_x = -FLT_MAX;
float min_y = FLT_MAX;
float max_y = -FLT_MAX;
for (const auto& prim : shape->getPrimitives()) {
for (const auto& point : prim.points) {
min_x = std::min(min_x, point.x);
max_x = std::max(max_x, point.x);
min_y = std::min(min_y, point.y);
max_y = std::max(max_y, point.y);
}
}
const float ANCHO = (max_x - min_x) * SCALE;
const float ALTURA = (max_y - min_y) * SCALE;
const float OFFSET_CENTRE = (shape->getCenter().x - min_x) * SCALE;
lletres_jailgames_.push_back({shape, {.x = 0.0F, .y = 0.0F}, ANCHO, ALTURA, OFFSET_CENTRE});
ancho_total += ANCHO;
altura_max = std::max(altura_max, ALTURA);
}
constexpr float ESPAI_JAILGAMES = ESPAI_ENTRE_LLETRES * SCALE;
if (!lletres_jailgames_.empty()) {
ancho_total += ESPAI_JAILGAMES * static_cast<float>(lletres_jailgames_.size() - 1);
}
const float Y_COPY = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS;
const float GAP = Defaults::Game::HEIGHT * Defaults::Title::Layout::JAILGAMES_COPYRIGHT_GAP;
const float Y_CENTRE = Y_COPY - GAP - (altura_max / 2.0F);
const float X_INICIAL = (Defaults::Game::WIDTH - ancho_total) / 2.0F;
float x_actual = X_INICIAL;
for (auto& lletra : lletres_jailgames_) {
lletra.position.x = x_actual + lletra.offset_centre;
lletra.position.y = Y_CENTRE;
x_actual += lletra.ancho + ESPAI_JAILGAMES;
}
}
void TitleScene3D::dibuixarPeuTitol(float spacing) const {
for (const auto& lletra : lletres_jailgames_) {
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::JAILGAMES_SCALE, 1.0F);
}
std::string copyright = Project::COPYRIGHT;
for (char& c : copyright) {
if (c >= 'a' && c <= 'z') {
c = static_cast<char>(c - 32);
}
}
const float Y_COPY = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS;
const float CENTRE_X = Defaults::Game::WIDTH / 2.0F;
text_.renderCentered(copyright, {.x = CENTRE_X, .y = Y_COPY}, Defaults::Title::Layout::COPYRIGHT_SCALE, spacing);
}
auto TitleScene3D::isFinished() const -> bool {
// Aquesta escena és la destinació d'un setNextScene(TITLE) quan ORNI_TITLE_3D
// està activat; mentre el context continue marcant TITLE com a destí actual,
// l'escena resta viva. També accepta TITLE_3D explícit.
const SceneType NEXT = context_.nextScene();
return NEXT != SceneType::TITLE && NEXT != SceneType::TITLE_3D;
}
void TitleScene3D::update(float delta_time) {
if (starfield_) {
starfield_->update(delta_time);
}
if (ship_animator_ &&
(estat_actual_ == TitleState::STARFIELD_FADE_IN ||
estat_actual_ == TitleState::STARFIELD ||
estat_actual_ == TitleState::MAIN ||
estat_actual_ == TitleState::PLAYER_JOIN_PHASE)) {
ship_animator_->update(delta_time);
}
switch (estat_actual_) {
case TitleState::STARFIELD_FADE_IN:
updateStarfieldFadeInState(delta_time);
break;
case TitleState::STARFIELD:
updateStarfieldState(delta_time);
break;
case TitleState::MAIN:
updateMainState(delta_time);
break;
case TitleState::PLAYER_JOIN_PHASE:
updatePlayerJoinPhaseState(delta_time);
break;
case TitleState::BLACK_SCREEN:
updateBlackScreenState(delta_time);
break;
}
handleSkipInput();
handleStartInput();
}
void TitleScene3D::updateStarfieldFadeInState(float delta_time) {
temps_acumulat_ += delta_time;
const float PROGRESS = std::min(1.0F, temps_acumulat_ / DURACIO_FADE_IN);
starfield_->setBrightness(PROGRESS * BRIGHTNESS_STARFIELD);
if (temps_acumulat_ >= DURACIO_FADE_IN) {
estat_actual_ = TitleState::STARFIELD;
temps_acumulat_ = 0.0F;
starfield_->setBrightness(BRIGHTNESS_STARFIELD);
}
}
void TitleScene3D::updateStarfieldState(float delta_time) {
temps_acumulat_ += delta_time;
if (temps_acumulat_ >= DURACIO_INIT) {
estat_actual_ = TitleState::MAIN;
temps_estat_main_ = 0.0F;
animacio_activa_ = false;
factor_lerp_ = 0.0F;
}
}
void TitleScene3D::updateMainState(float delta_time) {
temps_estat_main_ += delta_time;
if (temps_estat_main_ >= Defaults::Title::Ships::ENTRANCE_DELAY &&
ship_animator_ && !ship_animator_->isVisible()) {
ship_animator_->setVisible(true);
ship_animator_->startEntryAnimation();
}
if (temps_estat_main_ < DELAY_INICI_ANIMACIO) {
factor_lerp_ = 0.0F;
animacio_activa_ = false;
} else if (temps_estat_main_ < DELAY_INICI_ANIMACIO + DURACIO_LERP) {
const float TEMPS_LERP = temps_estat_main_ - DELAY_INICI_ANIMACIO;
factor_lerp_ = TEMPS_LERP / DURACIO_LERP;
animacio_activa_ = true;
} else {
factor_lerp_ = 1.0F;
animacio_activa_ = true;
}
updateLogoAnimation(delta_time);
}
void TitleScene3D::updatePlayerJoinPhaseState(float delta_time) {
temps_acumulat_ += delta_time;
updateLogoAnimation(delta_time);
const bool P1_ABANS = match_config_.jugador1_actiu;
const bool P2_ABANS = match_config_.jugador2_actiu;
if (checkStartGameButtonPressed()) {
context_.setMatchConfig(match_config_);
triggerExitForJoinedPlayers(P1_ABANS, P2_ABANS, "late join - ");
Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME);
temps_acumulat_ = 0.0F;
}
if (temps_acumulat_ >= DURACIO_TRANSITION) {
estat_actual_ = TitleState::BLACK_SCREEN;
temps_acumulat_ = 0.0F;
}
}
void TitleScene3D::updateBlackScreenState(float delta_time) {
temps_acumulat_ += delta_time;
if (temps_acumulat_ >= DURACIO_BLACK_SCREEN) {
context_.setNextScene(SceneType::GAME);
}
}
void TitleScene3D::handleSkipInput() {
if (estat_actual_ != TitleState::STARFIELD_FADE_IN && estat_actual_ != TitleState::STARFIELD) {
return;
}
if (!checkSkipButtonPressed()) {
return;
}
estat_actual_ = TitleState::MAIN;
starfield_->setBrightness(BRIGHTNESS_STARFIELD);
temps_estat_main_ = 0.0F;
}
void TitleScene3D::handleStartInput() {
if (estat_actual_ != TitleState::MAIN) {
return;
}
const bool P1_ABANS = match_config_.jugador1_actiu;
const bool P2_ABANS = match_config_.jugador2_actiu;
if (!checkStartGameButtonPressed()) {
return;
}
if (ship_animator_ && !ship_animator_->isVisible()) {
ship_animator_->setVisible(true);
ship_animator_->skipToFloatingState();
}
context_.setMatchConfig(match_config_);
estat_actual_ = TitleState::PLAYER_JOIN_PHASE;
temps_acumulat_ = 0.0F;
triggerExitForJoinedPlayers(P1_ABANS, P2_ABANS, "");
Audio::get()->fadeOutMusic(MUSIC_FADE);
Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME);
}
void TitleScene3D::triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix) {
if (ship_animator_ == nullptr) {
return;
}
if (match_config_.jugador1_actiu && !p1_was_active) {
ship_animator_->triggerExitAnimationForPlayer(1);
std::cout << "[TitleScene3D] P1 " << log_prefix << "ship exiting\n";
}
if (match_config_.jugador2_actiu && !p2_was_active) {
ship_animator_->triggerExitAnimationForPlayer(2);
std::cout << "[TitleScene3D] P2 " << log_prefix << "ship exiting\n";
}
}
void TitleScene3D::updateLogoAnimation(float delta_time) {
if (!animacio_activa_) {
return;
}
temps_animacio_ += delta_time * factor_lerp_;
const float TWO_PI = 2.0F * Defaults::Math::PI;
const float OFFSET_X = ORBIT_AMPLITUDE_X * std::sin(TWO_PI * ORBIT_FREQUENCY_X * temps_animacio_);
const float OFFSET_Y = ORBIT_AMPLITUDE_Y * std::sin((TWO_PI * ORBIT_FREQUENCY_Y * temps_animacio_) + ORBIT_PHASE_OFFSET);
for (std::size_t i = 0; i < lletres_orni_.size(); ++i) {
lletres_orni_[i].position.x = posicions_originals_orni_[i].x + std::round(OFFSET_X);
lletres_orni_[i].position.y = posicions_originals_orni_[i].y + std::round(OFFSET_Y);
}
for (std::size_t i = 0; i < lletres_attack_.size(); ++i) {
lletres_attack_[i].position.x = posicions_originals_attack_[i].x + std::round(OFFSET_X);
lletres_attack_[i].position.y = posicions_originals_attack_[i].y + std::round(OFFSET_Y);
}
}
void TitleScene3D::draw() {
if (starfield_ && estat_actual_ != TitleState::BLACK_SCREEN) {
starfield_->draw();
}
if (ship_animator_ &&
(estat_actual_ == TitleState::STARFIELD_FADE_IN ||
estat_actual_ == TitleState::STARFIELD ||
estat_actual_ == TitleState::MAIN ||
estat_actual_ == TitleState::PLAYER_JOIN_PHASE)) {
ship_animator_->draw();
}
if (estat_actual_ == TitleState::STARFIELD_FADE_IN || estat_actual_ == TitleState::STARFIELD) {
return;
}
if (estat_actual_ != TitleState::MAIN && estat_actual_ != TitleState::PLAYER_JOIN_PHASE) {
return;
}
if (animacio_activa_) {
float temps_shadow = std::max(0.0F, temps_animacio_ - SHADOW_DELAY);
const float TWO_PI = 2.0F * Defaults::Math::PI;
const float SHADOW_OX = (ORBIT_AMPLITUDE_X * std::sin(TWO_PI * ORBIT_FREQUENCY_X * temps_shadow)) + SHADOW_OFFSET_X;
const float SHADOW_OY = (ORBIT_AMPLITUDE_Y * std::sin((TWO_PI * ORBIT_FREQUENCY_Y * temps_shadow) + ORBIT_PHASE_OFFSET)) + SHADOW_OFFSET_Y;
for (std::size_t i = 0; i < lletres_orni_.size(); ++i) {
const Vec2 POS_SHADOW{
.x = posicions_originals_orni_[i].x + std::round(SHADOW_OX),
.y = posicions_originals_orni_[i].y + std::round(SHADOW_OY),
};
Rendering::renderShape(sdl_.getRenderer(), lletres_orni_[i].shape, POS_SHADOW, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, SHADOW_BRIGHTNESS);
}
for (std::size_t i = 0; i < lletres_attack_.size(); ++i) {
const Vec2 POS_SHADOW{
.x = posicions_originals_attack_[i].x + std::round(SHADOW_OX),
.y = posicions_originals_attack_[i].y + std::round(SHADOW_OY),
};
Rendering::renderShape(sdl_.getRenderer(), lletres_attack_[i].shape, POS_SHADOW, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, SHADOW_BRIGHTNESS);
}
}
for (const auto& lletra : lletres_orni_) {
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F);
}
for (const auto& lletra : lletres_attack_) {
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F);
}
const float SPACING = Defaults::Title::Layout::TEXT_SPACING;
bool mostrar_text = true;
if (estat_actual_ == TitleState::PLAYER_JOIN_PHASE) {
const float FASE = temps_acumulat_ * BLINK_FREQUENCY * 2.0F * std::numbers::pi_v<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 TitleScene3D::checkSkipButtonPressed() -> bool {
return Input::get()->checkAnyPlayerAction(ARCADE_BUTTONS);
}
auto TitleScene3D::checkStartGameButtonPressed() -> bool {
auto* input = Input::get();
bool any_pressed = false;
for (auto action : START_GAME_BUTTONS_3D) {
if (input->checkActionPlayer1(action, Input::DO_NOT_ALLOW_REPEAT)) {
if (!match_config_.jugador1_actiu) {
match_config_.jugador1_actiu = true;
any_pressed = true;
}
}
if (input->checkActionPlayer2(action, Input::DO_NOT_ALLOW_REPEAT)) {
if (!match_config_.jugador2_actiu) {
match_config_.jugador2_actiu = true;
any_pressed = true;
}
}
}
return any_pressed;
}
void TitleScene3D::handleEvent(const SDL_Event& event) {
(void)event;
}
-126
View File
@@ -1,126 +0,0 @@
// title_scene_3d.hpp - Variant 3D real de l'escena de títol
// © 2026 JailDesigner
//
// Clon de `TitleScene` (2D) que substitueix `Graphics::Starfield` per
// `Graphics::Starfield3D` i `Title::ShipAnimator` per `Title::ShipAnimator3D`,
// afegint una `Graphics::Camera3D` que projecta l'escena en perspectiva real.
// Tot el bloc d'overlay 2D (logo "ORNI ATTACK!", "PRESS START TO PLAY", peu
// "JAILGAMES + copyright") es manté idèntic.
//
// Trigger: env var `ORNI_TITLE_3D=1` interceptada al `Director::buildScene`,
// o transicions explícites a `SceneType::TITLE_3D`.
#pragma once
#include <SDL3/SDL.h>
#include <array>
#include <cstdint>
#include <memory>
#include <vector>
#include "core/graphics/camera3d.hpp"
#include "core/graphics/shape.hpp"
#include "core/graphics/starfield3d.hpp"
#include "core/graphics/vector_text.hpp"
#include "core/input/input_types.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/system/game_config.hpp"
#include "core/system/scene.hpp"
#include "core/system/scene_context.hpp"
#include "core/types.hpp"
#include "game/title/ship_animator3d.hpp"
class TitleScene3D final : public Scene {
public:
explicit TitleScene3D(SDLManager& sdl, SceneManager::SceneContext& context);
~TitleScene3D() override;
void handleEvent(const SDL_Event& event) override;
void update(float delta_time) override;
void draw() override;
[[nodiscard]] auto isFinished() const -> bool override;
private:
enum class TitleState : std::uint8_t {
STARFIELD_FADE_IN,
STARFIELD,
MAIN,
PLAYER_JOIN_PHASE,
BLACK_SCREEN,
};
struct LetraLogo {
std::shared_ptr<Graphics::Shape> shape;
Vec2 position;
float ancho;
float altura;
float offset_centre;
};
SDLManager& sdl_;
SceneManager::SceneContext& context_;
GameConfig::MatchConfig match_config_;
Graphics::VectorText text_;
std::unique_ptr<Graphics::Camera3D> camera_;
std::unique_ptr<Graphics::Starfield3D> starfield_;
std::unique_ptr<Title::ShipAnimator3D> ship_animator_;
TitleState estat_actual_{TitleState::STARFIELD_FADE_IN};
float temps_acumulat_{0.0F};
std::vector<LetraLogo> lletres_orni_;
std::vector<LetraLogo> lletres_attack_;
float y_attack_dinamica_{0.0F};
std::vector<LetraLogo> lletres_jailgames_;
float temps_animacio_{0.0F};
std::vector<Vec2> posicions_originals_orni_;
std::vector<Vec2> posicions_originals_attack_;
float temps_estat_main_{0.0F};
bool animacio_activa_{false};
float factor_lerp_{0.0F};
static constexpr float BRIGHTNESS_STARFIELD = 1.2F;
static constexpr float DURACIO_FADE_IN = 3.0F;
static constexpr float DURACIO_INIT = 4.0F;
static constexpr float DURACIO_TRANSITION = 2.5F;
static constexpr float ESPAI_ENTRE_LLETRES = 10.0F;
static constexpr float BLINK_FREQUENCY = 3.0F;
static constexpr float DURACIO_BLACK_SCREEN = 2.0F;
static constexpr int MUSIC_FADE = 1500;
static constexpr float ORBIT_AMPLITUDE_X = 4.0F;
static constexpr float ORBIT_AMPLITUDE_Y = 3.0F;
static constexpr float ORBIT_FREQUENCY_X = 0.8F;
static constexpr float ORBIT_FREQUENCY_Y = 1.2F;
static constexpr float ORBIT_PHASE_OFFSET = 1.57F;
static constexpr float SHADOW_DELAY = 0.5F;
static constexpr float SHADOW_BRIGHTNESS = 0.4F;
static constexpr float SHADOW_OFFSET_X = 2.0F;
static constexpr float SHADOW_OFFSET_Y = 2.0F;
static constexpr float DELAY_INICI_ANIMACIO = 10.0F;
static constexpr float DURACIO_LERP = 2.0F;
// Càmera 3D: FOV vertical en radians.
static constexpr float CAMERA_FOV_Y_RAD = 1.0472F; // 60°
void updateLogoAnimation(float delta_time);
static auto checkSkipButtonPressed() -> bool;
auto checkStartGameButtonPressed() -> bool;
void initTitle();
void inicialitzarJailgames();
void dibuixarPeuTitol(float spacing) const;
void updateStarfieldFadeInState(float delta_time);
void updateStarfieldState(float delta_time);
void updateMainState(float delta_time);
void updatePlayerJoinPhaseState(float delta_time);
void updateBlackScreenState(float delta_time);
void handleSkipInput();
void handleStartInput();
void triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix);
};
+122 -122
View File
@@ -16,152 +16,152 @@
namespace StageSystem { namespace StageSystem {
SpawnController::SpawnController() = default; SpawnController::SpawnController() = default;
void SpawnController::configure(const StageConfig* config) { void SpawnController::configure(const StageConfig* config) {
config_ = config; config_ = config;
}
void SpawnController::start() {
if (config_ == nullptr) {
std::cerr << "[SpawnController] Error: config_ es null" << '\n';
return;
} }
reset(); void SpawnController::start() {
generateSpawnEvents(); if (config_ == nullptr) {
std::cerr << "[SpawnController] Error: config_ es null" << '\n';
std::cout << "[SpawnController] Stage " << static_cast<int>(config_->stage_id) return;
<< ": generats " << spawn_queue_.size() << " spawn events" << '\n';
}
void SpawnController::reset() {
spawn_queue_.clear();
temps_transcorregut_ = 0.0F;
index_spawn_actual_ = 0;
}
void SpawnController::update(float delta_time, std::array<Enemy, 15>& orni_array, bool pausar) {
if ((config_ == nullptr) || spawn_queue_.empty()) {
return;
}
// Increment timer only when not paused
if (!pausar) {
temps_transcorregut_ += delta_time;
}
// Process spawn events
while (index_spawn_actual_ < spawn_queue_.size()) {
SpawnEvent& event = spawn_queue_[index_spawn_actual_];
if (event.spawnejat) {
index_spawn_actual_++;
continue;
} }
if (temps_transcorregut_ >= event.temps_spawn) { reset();
// Find first inactive enemy generateSpawnEvents();
for (auto& enemy : orni_array) {
if (!enemy.isActive()) { std::cout << "[SpawnController] Stage " << static_cast<int>(config_->stage_id)
spawnEnemy(enemy, event.type, ship_position_); << ": generats " << spawn_queue_.size() << " spawn events" << '\n';
event.spawnejat = true; }
index_spawn_actual_++;
void SpawnController::reset() {
spawn_queue_.clear();
temps_transcorregut_ = 0.0F;
index_spawn_actual_ = 0;
}
void SpawnController::update(float delta_time, std::array<Enemy, 15>& orni_array, bool pausar) {
if ((config_ == nullptr) || spawn_queue_.empty()) {
return;
}
// Increment timer only when not paused
if (!pausar) {
temps_transcorregut_ += delta_time;
}
// Process spawn events
while (index_spawn_actual_ < spawn_queue_.size()) {
SpawnEvent& event = spawn_queue_[index_spawn_actual_];
if (event.spawnejat) {
index_spawn_actual_++;
continue;
}
if (temps_transcorregut_ >= event.temps_spawn) {
// Find first inactive enemy
for (auto& enemy : orni_array) {
if (!enemy.isActive()) {
spawnEnemy(enemy, event.type, ship_position_);
event.spawnejat = true;
index_spawn_actual_++;
break;
}
}
// If no slot available, try next frame
if (!event.spawnejat) {
break; break;
} }
} } else {
// Not yet time for this spawn
// If no slot available, try next frame
if (!event.spawnejat) {
break; break;
} }
} else {
// Not yet time for this spawn
break;
} }
} }
}
auto SpawnController::allEnemiesSpawned() const -> bool { auto SpawnController::allEnemiesSpawned() const -> bool {
return index_spawn_actual_ >= spawn_queue_.size(); return index_spawn_actual_ >= spawn_queue_.size();
}
auto SpawnController::allEnemiesDestroyed(const std::array<Enemy, 15>& orni_array) const -> bool {
if (!allEnemiesSpawned()) {
return false;
} }
return std::ranges::all_of(orni_array, [](const Enemy& enemy) { return !enemy.isActive(); });
}
auto SpawnController::getAliveEnemyCount(const std::array<Enemy, 15>& orni_array) -> uint8_t { auto SpawnController::allEnemiesDestroyed(const std::array<Enemy, 15>& orni_array) const -> bool {
uint8_t count = 0; if (!allEnemiesSpawned()) {
for (const auto& enemy : orni_array) { return false;
if (enemy.isActive()) { }
count++; return std::ranges::all_of(orni_array, [](const Enemy& enemy) { return !enemy.isActive(); });
}
auto SpawnController::getAliveEnemyCount(const std::array<Enemy, 15>& orni_array) -> uint8_t {
uint8_t count = 0;
for (const auto& enemy : orni_array) {
if (enemy.isActive()) {
count++;
}
}
return count;
}
auto SpawnController::countSpawnedEnemies() const -> uint8_t {
return static_cast<uint8_t>(index_spawn_actual_);
}
void SpawnController::generateSpawnEvents() {
if (config_ == nullptr) {
return;
}
for (uint8_t i = 0; i < config_->total_enemies; i++) {
float spawn_time = config_->config_spawn.delay_inicial +
(i * config_->config_spawn.interval_spawn);
EnemyType type = selectRandomType();
spawn_queue_.push_back({spawn_time, type, false});
} }
} }
return count;
}
auto SpawnController::countSpawnedEnemies() const -> uint8_t { auto SpawnController::selectRandomType() const -> EnemyType {
return static_cast<uint8_t>(index_spawn_actual_); if (config_ == nullptr) {
} return EnemyType::PENTAGON;
}
void SpawnController::generateSpawnEvents() { // Weighted random selection based on distribution
if (config_ == nullptr) { int rand_val = std::rand() % 100;
return;
if (std::cmp_less(rand_val, config_->distribucio.pentagon)) {
return EnemyType::PENTAGON;
}
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado) {
return EnemyType::SQUARE;
}
return EnemyType::PINWHEEL;
} }
for (uint8_t i = 0; i < config_->total_enemies; i++) { void SpawnController::spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos) {
float spawn_time = config_->config_spawn.delay_inicial + // Initialize enemy (with safe spawn if ship_pos provided)
(i * config_->config_spawn.interval_spawn); enemy.init(type, ship_pos);
EnemyType type = selectRandomType(); // Apply difficulty multipliers
applyMultipliers(enemy);
spawn_queue_.push_back({spawn_time, type, false});
}
}
auto SpawnController::selectRandomType() const -> EnemyType {
if (config_ == nullptr) {
return EnemyType::PENTAGON;
} }
// Weighted random selection based on distribution void SpawnController::applyMultipliers(Enemy& enemy) const {
int rand_val = std::rand() % 100; if (config_ == nullptr) {
return;
}
if (std::cmp_less(rand_val, config_->distribucio.pentagon)) { // Apply velocity multiplier
return EnemyType::PENTAGON; float base_vel = enemy.getBaseVelocity();
enemy.setVelocity(base_vel * config_->multiplicadors.velocity);
// Apply rotation multiplier
float base_rot = enemy.getBaseRotation();
enemy.setRotation(base_rot * config_->multiplicadors.rotation);
// Apply tracking strength (only affects SQUARE)
enemy.setTrackingStrength(config_->multiplicadors.tracking_strength);
} }
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado) {
return EnemyType::QUADRAT;
}
return EnemyType::MOLINILLO;
}
void SpawnController::spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos) {
// Initialize enemy (with safe spawn if ship_pos provided)
enemy.init(type, ship_pos);
// Apply difficulty multipliers
applyMultipliers(enemy);
}
void SpawnController::applyMultipliers(Enemy& enemy) const {
if (config_ == nullptr) {
return;
}
// Apply velocity multiplier
float base_vel = enemy.getBaseVelocity();
enemy.setVelocity(base_vel * config_->multiplicadors.velocity);
// Apply rotation multiplier
float base_rot = enemy.getBaseRotation();
enemy.setRotation(base_rot * config_->multiplicadors.rotation);
// Apply tracking strength (only affects QUADRAT)
enemy.setTrackingStrength(config_->multiplicadors.tracking_strength);
}
} // namespace StageSystem } // namespace StageSystem
+1 -1
View File
@@ -36,7 +36,7 @@ namespace StageSystem {
struct MultiplicadorsDificultat { struct MultiplicadorsDificultat {
float velocity; // 0.5-2.0 típic float velocity; // 0.5-2.0 típic
float rotation; // 0.5-2.0 típic float rotation; // 0.5-2.0 típic
float tracking_strength; // 0.0-1.5 (aplicat a Cuadrado) float tracking_strength; // 0.0-1.5 (aplicat a Square)
}; };
// Metadades del file YAML // Metadades del file YAML
+121 -38
View File
@@ -7,6 +7,7 @@
#include "core/audio/audio.hpp" #include "core/audio/audio.hpp"
#include "core/physics/collision.hpp" #include "core/physics/collision.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/constants.hpp"
namespace Systems::Collision { namespace Systems::Collision {
@@ -19,10 +20,10 @@ namespace Systems::Collision {
switch (type) { switch (type) {
case EnemyType::PENTAGON: case EnemyType::PENTAGON:
return Defaults::Enemies::Scoring::PENTAGON_SCORE; return Defaults::Enemies::Scoring::PENTAGON_SCORE;
case EnemyType::QUADRAT: case EnemyType::SQUARE:
return Defaults::Enemies::Scoring::QUADRAT_SCORE; return Defaults::Enemies::Scoring::SQUARE_SCORE;
case EnemyType::MOLINILLO: case EnemyType::PINWHEEL:
return Defaults::Enemies::Scoring::MOLINILLO_SCORE; return Defaults::Enemies::Scoring::PINWHEEL_SCORE;
} }
return 0; return 0;
} }
@@ -31,10 +32,10 @@ namespace Systems::Collision {
switch (type) { switch (type) {
case EnemyType::PENTAGON: case EnemyType::PENTAGON:
return Defaults::Palette::PENTAGON; return Defaults::Palette::PENTAGON;
case EnemyType::QUADRAT: case EnemyType::SQUARE:
return Defaults::Palette::QUADRAT; return Defaults::Palette::SQUARE;
case EnemyType::MOLINILLO: case EnemyType::PINWHEEL:
return Defaults::Palette::MOLINILLO; return Defaults::Palette::PINWHEEL;
} }
return SDL_Color{}; return SDL_Color{};
} }
@@ -57,16 +58,16 @@ namespace Systems::Collision {
} }
ctx.floating_score_manager.crear(POINTS, ENEMY_POS); ctx.floating_score_manager.crear(POINTS, ENEMY_POS);
enemy.destruir(); enemy.destroy();
constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (explosión suave) constexpr float SPEED_EXPLOSIO = 80.0F; // px/s (explosión suave)
const Vec2 INHERITED_VEL = ENEMY_VEL * Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE; const Vec2 INHERITED_VEL = ENEMY_VEL * Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE;
ctx.debris_manager.explode( ctx.debris_manager.explode(
SHAPE, SHAPE,
ENEMY_POS, ENEMY_POS,
0.0F, // angle (rotación interna del enemy) 0.0F, // angle (rotación interna del enemy)
1.0F, // escala 1.0F, // escala
VELOCITAT_EXPLOSIO, SPEED_EXPLOSIO,
BRIGHTNESS, BRIGHTNESS,
INHERITED_VEL, INHERITED_VEL,
0.0F, // sense herència angular: evita que els 5 trossos curvin en bloc 0.0F, // sense herència angular: evita que els 5 trossos curvin en bloc
@@ -78,8 +79,37 @@ namespace Systems::Collision {
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER); Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER);
// Firework burst radial des del centro de l'enemic (efecte adicional al debris). // Firework burst radial des del centro de l'enemic (efecte adicional al debris).
// No heretem color: el burst usa el blanc per defecte per a un feel més lluminós. // Línia blanca + halo daurat (WOUNDED) per a feel d'espurnes.
ctx.firework_manager.spawn(ENEMY_POS); ctx.firework_manager.spawn(ENEMY_POS,
Defaults::FX::Firework::DEFAULT_COLOR,
Defaults::FX::Firework::SPEED,
Defaults::FX::Firework::N_POINTS,
Defaults::FX::Firework::INITIAL_BRIGHTNESS,
/*glow=*/true,
Defaults::Palette::WOUNDED);
}
// Trenca una bala en debris (8 fragments de l'octàgon) + so HIT + desactiva.
// S'invoca des de qualsevol desactivació de bala (impacte amb enemic, amb jugador,
// o sortida del PLAYAREA) per a un feedback visual i sonor consistent.
void breakBullet(Effects::DebrisManager& debris_manager, Bullet& bullet) {
constexpr float DEBRIS_VELOCITY = 60.0F;
debris_manager.explode(
bullet.getShape(),
bullet.getCenter(),
bullet.getAngle(),
1.0F, // scale
DEBRIS_VELOCITY,
bullet.getBrightness(),
Vec2{}, // sense herència de velocitat (fragments radials)
0.0F, // sense velocity angular heretada
0.0F, // sense rotació visual heretada
Defaults::Sound::HIT,
Defaults::Palette::BULLET,
Defaults::Physics::Debris::TEMPS_VIDA,
Defaults::Physics::Debris::ACCELERACIO,
1); // sense duplicat de segments
bullet.desactivar();
} }
} // anonymous namespace } // anonymous namespace
@@ -87,8 +117,11 @@ namespace Systems::Collision {
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER; constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER;
for (auto& bullet : ctx.bullets) { for (auto& bullet : ctx.bullets) {
if (!bullet.isActive()) {
continue;
}
for (auto& enemy : ctx.enemies) { for (auto& enemy : ctx.enemies) {
if (!Physics::checkCollision(bullet, enemy, AMPLIFIER)) { if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), Defaults::Entities::BULLET_RADIUS, enemy, AMPLIFIER)) {
continue; continue;
} }
@@ -108,10 +141,10 @@ namespace Systems::Collision {
explodeNow(ctx, enemy, SHOOTER); explodeNow(ctx, enemy, SHOOTER);
} else { } else {
// Primer impacto → entra en estado herido (explosión diferida). // Primer impacto → entra en estado herido (explosión diferida).
enemy.herir(SHOOTER); enemy.hurt(SHOOTER);
} }
bullet.desactivar(); breakBullet(ctx.debris_manager, bullet);
break; // Una bala impacta a un enemy y muere break; // Una bala impacta a un enemy y muere
} }
} }
@@ -144,14 +177,14 @@ namespace Systems::Collision {
if (A_WOUNDED == B_WOUNDED) { if (A_WOUNDED == B_WOUNDED) {
continue; // ambos sanos o ambos heridos: nada que propagar continue; // ambos sanos o ambos heridos: nada que propagar
} }
if (!Physics::checkCollision(a, b, 1.0F)) { if (!Physics::checkCollision(a, b, Defaults::Game::COLLISION_WOUNDED_CHAIN_AMPLIFIER)) {
continue; continue;
} }
// El sano queda herido, propagando el shooter original. // El sano queda herido, propagando el shooter original.
if (A_WOUNDED) { if (A_WOUNDED) {
b.herir(a.getLastHitBy()); b.hurt(a.getLastHitBy());
} else { } else {
a.herir(b.getLastHitBy()); a.hurt(b.getLastHitBy());
} }
} }
} }
@@ -161,22 +194,46 @@ namespace Systems::Collision {
constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER; constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER;
for (uint8_t i = 0; i < 2; i++) { for (uint8_t i = 0; i < 2; i++) {
// Skip si ya tocado / muerto / invulnerable // Skip si ya tocado / muerto / invulnerable. NO actualitzem el flag de contacte:
// mentre estem inactius no hi ha "frame anterior" rellevant, i el respawn ja el resetea.
if (ctx.hit_timer_per_player[i] > 0.0F || if (ctx.hit_timer_per_player[i] > 0.0F ||
!ctx.ships[i].isActive() || !ctx.ships[i].isActive() ||
ctx.ships[i].isInvulnerable()) { ctx.ships[i].isInvulnerable()) {
continue; continue;
} }
for (const auto& enemy : ctx.enemies) { // Comprovem si la nau toca QUALSEVOL enemic vulnerable aquest frame.
Enemy* touched_enemy = nullptr;
for (auto& enemy : ctx.enemies) {
if (enemy.isInvulnerable()) { if (enemy.isInvulnerable()) {
continue; continue;
} }
if (Physics::checkCollision(ctx.ships[i], enemy, AMPLIFIER)) { if (Physics::checkCollision(ctx.ships[i], enemy, AMPLIFIER)) {
ctx.on_player_hit(i); touched_enemy = &enemy;
break; // Solo una colisión por player por frame break;
} }
} }
const bool TOUCHING_NOW = touched_enemy != nullptr;
// Edge-trigger: només compta com a impacte la transició no-tocant → tocant.
// Així el contacte continu durant el rebot frame-a-frame no dispara HURT i mort
// en frames consecutius.
const bool RISING_EDGE = TOUCHING_NOW && !ctx.ships[i].wasTouchingEnemyPrevFrame();
if (RISING_EDGE) {
if (ctx.ships[i].isHurt()) {
// Segon impacte durant HURT → mort. Aplica un impuls afegit
// perquè l'enemic surti disparat (feedback visible).
const Vec2 SHIP_VEL = ctx.ships[i].getVelocityVector();
const Vec2 IMPULSE = SHIP_VEL * (Defaults::Ship::MASS * Defaults::Physics::Ship::DEATH_IMPACT_MOMENTUM_FACTOR);
touched_enemy->applyImpulse(IMPULSE);
ctx.on_player_hit(i);
} else {
// Primer impacte → estat HURT (rebot físic ja resolt per PhysicsWorld;
// l'enemic no rep dany per decisió de disseny).
ctx.ships[i].hurt();
}
}
ctx.ships[i].setTouchingEnemyPrevFrame(TOUCHING_NOW);
} }
} }
@@ -188,40 +245,44 @@ namespace Systems::Collision {
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER; constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER;
for (auto& bullet : ctx.bullets) { for (auto& bullet : ctx.bullets) {
if (!bullet.isActive() || bullet.getGraceTimer() > 0.0F) { if (!bullet.isActive()) {
continue; continue;
} }
const uint8_t BULLET_OWNER = bullet.getOwnerId(); const uint8_t BULLET_OWNER = bullet.getOwnerId();
for (uint8_t player_id = 0; player_id < 2; player_id++) { for (uint8_t player_id = 0; player_id < 2; player_id++) {
// Una bala mai no impacta al seu propi shooter: les bales d'aquest joc no
// reboten ni el shooter pot atrapar-les, així que la prevenció és per disseny.
if (BULLET_OWNER == player_id) {
continue;
}
if (ctx.hit_timer_per_player[player_id] > 0.0F || if (ctx.hit_timer_per_player[player_id] > 0.0F ||
!ctx.ships[player_id].isActive() || !ctx.ships[player_id].isActive() ||
ctx.ships[player_id].isInvulnerable()) { ctx.ships[player_id].isInvulnerable()) {
continue; continue;
} }
const bool JUGADOR_ACTIU = (player_id == 0) const bool JUGADOR_ACTIU = (player_id == 0)
? ctx.match_config.jugador1_actiu ? ctx.match_config.player1_active
: ctx.match_config.jugador2_actiu; : ctx.match_config.player2_active;
if (!JUGADOR_ACTIU) { if (!JUGADOR_ACTIU) {
continue; continue;
} }
if (!Physics::checkCollision(bullet, ctx.ships[player_id], AMPLIFIER)) { if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), Defaults::Entities::BULLET_RADIUS, ctx.ships[player_id], AMPLIFIER)) {
continue; continue;
} }
// *** FRIENDLY FIRE HIT *** // *** TEAMMATE HIT (friendly fire) ***
if (BULLET_OWNER == player_id) { // Víctima perd 1 vida, atacant en guanya 1. Apliquem l'impuls
// Self-hit: víctima pierde 1 vida. // de la bala a la nau ABANS de on_player_hit perquè tocado()
ctx.on_player_hit(player_id); // captura la velocitat per als debris (si no, queden quiets).
} else { const Vec2 BULLET_IMPULSE = bullet.getBody().velocity *
// Teammate hit: víctima pierde 1, atacante gana 1. (bullet.getBody().mass * Defaults::Physics::Bullet::IMPACT_MOMENTUM_FACTOR);
ctx.on_player_hit(player_id); ctx.ships[player_id].getBody().applyImpulse(BULLET_IMPULSE);
ctx.lives_per_player[BULLET_OWNER]++; ctx.on_player_hit(player_id);
} ctx.lives_per_player[BULLET_OWNER]++;
Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME); Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME);
bullet.desactivar(); breakBullet(ctx.debris_manager, bullet);
break; // Una bullet solo impacta una vez por frame break; // Una bullet solo impacta una vez por frame
} }
} }
@@ -235,4 +296,26 @@ namespace Systems::Collision {
detectBulletPlayer(ctx); detectBulletPlayer(ctx);
} }
void desactivateOutOfBoundsBullets(
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS) * 2>& bullets,
Effects::DebrisManager& debris_manager) {
float min_x;
float max_x;
float min_y;
float max_y;
Constants::getPlayAreaBounds(min_x, max_x, min_y, max_y);
constexpr float R = Defaults::Entities::BULLET_RADIUS;
for (auto& bullet : bullets) {
if (!bullet.isActive()) {
continue;
}
const Vec2& pos = bullet.getCenter();
if (pos.x < min_x + R || pos.x > max_x - R ||
pos.y < min_y + R || pos.y > max_y - R) {
breakBullet(debris_manager, bullet);
}
}
}
} // namespace Systems::Collision } // namespace Systems::Collision
+8 -1
View File
@@ -32,7 +32,7 @@ namespace Systems::Collision {
struct Context { struct Context {
std::array<Ship, 2>& ships; std::array<Ship, 2>& ships;
std::array<Enemy, Defaults::Entities::MAX_ORNIS>& enemies; std::array<Enemy, Defaults::Entities::MAX_ORNIS>& enemies;
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BALES) * 2>& bullets; std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS) * 2>& bullets;
std::array<float, 2>& hit_timer_per_player; std::array<float, 2>& hit_timer_per_player;
std::array<int, 2>& score_per_player; std::array<int, 2>& score_per_player;
std::array<int, 2>& lives_per_player; std::array<int, 2>& lives_per_player;
@@ -70,4 +70,11 @@ namespace Systems::Collision {
// Las tres en orden lógico del frame. // Las tres en orden lógico del frame.
void detectAll(Context& ctx); void detectAll(Context& ctx);
// Desactiva les bales que han sortit del PLAYAREA, generant debris visual
// (8 fragments de l'octàgon) i el so HIT. Cal cridar-la després de detectAll()
// perquè una bala que el mateix frame xoca i alhora surt es comptabilitzi com a impacte.
void desactivateOutOfBoundsBullets(
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS) * 2>& bullets,
Effects::DebrisManager& debris_manager);
} // namespace Systems::Collision } // namespace Systems::Collision
+79 -79
View File
@@ -11,99 +11,99 @@
#include "game/scenes/game_scene.hpp" // GameOverState (definición completa) #include "game/scenes/game_scene.hpp" // GameOverState (definición completa)
namespace Systems::ContinueScreen { namespace Systems::ContinueScreen {
namespace { namespace {
// Si el countdown ha bajado de 0, transiciona a GAME_OVER con su timer. // Si el countdown ha bajado de 0, transiciona a GAME_OVER con su timer.
void checkAndApplyTimeout(Context& ctx) { void checkAndApplyTimeout(Context& ctx) {
if (ctx.counter < 0) { if (ctx.counter < 0) {
ctx.state = GameOverState::GAME_OVER; ctx.state = GameOverState::GAME_OVER;
ctx.game_over_timer = Defaults::Game::GAME_OVER_DURATION; ctx.game_over_timer = Defaults::Game::GAME_OVER_DURATION;
} }
} }
void revivePlayer(Context& ctx, uint8_t player_id) { void revivePlayer(Context& ctx, uint8_t player_id) {
ctx.score_per_player[player_id] = 0; ctx.score_per_player[player_id] = 0;
ctx.lives_per_player[player_id] = Defaults::Game::STARTING_LIVES; ctx.lives_per_player[player_id] = Defaults::Game::STARTING_LIVES;
ctx.hit_timer_per_player[player_id] = 0.0F; ctx.hit_timer_per_player[player_id] = 0.0F;
if (player_id == 0) { if (player_id == 0) {
ctx.match_config.jugador1_actiu = true; ctx.match_config.player1_active = true;
} else { } else {
ctx.match_config.jugador2_actiu = true; ctx.match_config.player2_active = true;
} }
const Vec2 SPAWN = ctx.get_spawn_point(player_id); const Vec2 SPAWN = ctx.get_spawn_point(player_id);
ctx.ships[player_id].init(&SPAWN, /*activar_invulnerabilitat=*/true); ctx.ships[player_id].init(&SPAWN, /*activar_invulnerabilitat=*/true);
} }
} // namespace } // namespace
void update(Context& ctx, float delta_time) { void update(Context& ctx, float delta_time) {
ctx.tick_timer -= delta_time; ctx.tick_timer -= delta_time;
if (ctx.tick_timer > 0.0F) { if (ctx.tick_timer > 0.0F) {
return;
}
ctx.counter--;
ctx.tick_timer = Defaults::Game::CONTINUE_TICK_DURATION;
checkAndApplyTimeout(ctx);
// Solo pita el tick si seguimos en CONTINUE (no transitamos a GAME_OVER)
if (ctx.state == GameOverState::CONTINUE) {
Audio::get()->playSound(Defaults::Sound::CONTINUE, Audio::Group::GAME);
}
}
void processInput(Context& ctx) {
auto* input = Input::get();
const bool P1_START = input->checkActionPlayer1(InputAction::START, Input::DO_NOT_ALLOW_REPEAT);
const bool P2_START = input->checkActionPlayer2(InputAction::START, Input::DO_NOT_ALLOW_REPEAT);
if (P1_START || P2_START) {
// ¿Quedan continues?
if (!Defaults::Game::INFINITE_CONTINUES &&
ctx.continues_used >= Defaults::Game::MAX_CONTINUES) {
ctx.state = GameOverState::GAME_OVER;
ctx.game_over_timer = Defaults::Game::GAME_OVER_DURATION;
return; return;
} }
if (!Defaults::Game::INFINITE_CONTINUES) {
ctx.continues_used++;
}
const uint8_t PRIMARY = P1_START ? 0 : 1;
revivePlayer(ctx, PRIMARY);
// Si ambos pulsan START, revivimos a los dos.
if (P1_START && P2_START) {
revivePlayer(ctx, 1);
}
// Reanudar partida.
ctx.state = GameOverState::NONE;
ctx.counter = 0;
ctx.tick_timer = 0.0F;
Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME);
return;
}
// THRUST/SHOOT acelera el countdown manualmente.
const bool ACCEL = input->checkActionPlayer1(InputAction::THRUST, Input::DO_NOT_ALLOW_REPEAT) ||
input->checkActionPlayer2(InputAction::THRUST, Input::DO_NOT_ALLOW_REPEAT) ||
input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT) ||
input->checkActionPlayer2(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT);
if (ACCEL) {
ctx.counter--; ctx.counter--;
ctx.tick_timer = Defaults::Game::CONTINUE_TICK_DURATION;
checkAndApplyTimeout(ctx); checkAndApplyTimeout(ctx);
// Solo pita el tick si seguimos en CONTINUE (no transitamos a GAME_OVER)
if (ctx.state == GameOverState::CONTINUE) { if (ctx.state == GameOverState::CONTINUE) {
Audio::get()->playSound(Defaults::Sound::CONTINUE, Audio::Group::GAME); Audio::get()->playSound(Defaults::Sound::CONTINUE, Audio::Group::GAME);
} }
ctx.tick_timer = Defaults::Game::CONTINUE_TICK_DURATION;
} }
}
void processInput(Context& ctx) {
auto* input = Input::get();
const bool P1_START = input->checkActionPlayer1(InputAction::START, Input::DO_NOT_ALLOW_REPEAT);
const bool P2_START = input->checkActionPlayer2(InputAction::START, Input::DO_NOT_ALLOW_REPEAT);
if (P1_START || P2_START) {
// ¿Quedan continues?
if (!Defaults::Game::INFINITE_CONTINUES &&
ctx.continues_used >= Defaults::Game::MAX_CONTINUES) {
ctx.state = GameOverState::GAME_OVER;
ctx.game_over_timer = Defaults::Game::GAME_OVER_DURATION;
return;
}
if (!Defaults::Game::INFINITE_CONTINUES) {
ctx.continues_used++;
}
const uint8_t PRIMARY = P1_START ? 0 : 1;
revivePlayer(ctx, PRIMARY);
// Si ambos pulsan START, revivimos a los dos.
if (P1_START && P2_START) {
revivePlayer(ctx, 1);
}
// Reanudar partida.
ctx.state = GameOverState::NONE;
ctx.counter = 0;
ctx.tick_timer = 0.0F;
Audio::get()->playSound(Defaults::Sound::START, Audio::Group::GAME);
return;
}
// THRUST/SHOOT acelera el countdown manualmente.
const bool ACCEL = input->checkActionPlayer1(InputAction::THRUST, Input::DO_NOT_ALLOW_REPEAT) ||
input->checkActionPlayer2(InputAction::THRUST, Input::DO_NOT_ALLOW_REPEAT) ||
input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT) ||
input->checkActionPlayer2(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT);
if (ACCEL) {
ctx.counter--;
checkAndApplyTimeout(ctx);
if (ctx.state == GameOverState::CONTINUE) {
Audio::get()->playSound(Defaults::Sound::CONTINUE, Audio::Group::GAME);
}
ctx.tick_timer = Defaults::Game::CONTINUE_TICK_DURATION;
}
}
} // namespace Systems::ContinueScreen } // namespace Systems::ContinueScreen
+1 -1
View File
@@ -31,7 +31,7 @@ namespace Systems::InitHud {
auto computeShipPosition(float progress, const Vec2& final_position) -> Vec2 { auto computeShipPosition(float progress, const Vec2& final_position) -> Vec2 {
const float EASED = Easing::easeOutQuad(progress); const float EASED = Easing::easeOutQuad(progress);
const SDL_FRect& zone = Defaults::Zones::PLAYAREA; const SDL_FRect& zone = Defaults::Zones::PLAYAREA;
// Y inicial: bajo la zona de juego (sale desde fuera). // Y inicial: bajo la zone de juego (sale desde fuera).
const float Y_INI = zone.y + zone.h + Defaults::Hud::InitAnim::SHIP_SPAWN_Y_OFFSET; const float Y_INI = zone.y + zone.h + Defaults::Hud::InitAnim::SHIP_SPAWN_Y_OFFSET;
const float Y_ANIM = Y_INI + ((final_position.y - Y_INI) * EASED); const float Y_ANIM = Y_INI + ((final_position.y - Y_INI) * EASED);
return Vec2{.x = final_position.x, .y = Y_ANIM}; return Vec2{.x = final_position.x, .y = Y_ANIM};
+22 -22
View File
@@ -4,7 +4,7 @@
// Cubre la animación INIT_HUD del comienzo de cada partida/stage: // Cubre la animación INIT_HUD del comienzo de cada partida/stage:
// 1. Crecimiento de los marcos del PLAYAREA con efecto pincel en 3 fases. // 1. Crecimiento de los marcos del PLAYAREA con efecto pincel en 3 fases.
// 2. Marcador subiendo desde abajo. // 2. Marcador subiendo desde abajo.
// 3. Naves entrando desde la zona inferior hacia su spawn. // 3. Naves entrando desde la zone inferior hacia su spawn.
// //
// Todas las funciones son puras (sin estado interno propio). GameScene aporta // Todas las funciones son puras (sin estado interno propio). GameScene aporta
// el contexto que necesitan: posiciones finales, texto del scoreboard y el // el contexto que necesitan: posiciones finales, texto del scoreboard y el
@@ -21,29 +21,29 @@
namespace Systems::InitHud { namespace Systems::InitHud {
// Convierte un progreso global 0..1 al sub-progreso de un elemento que solo // Convierte un progreso global 0..1 al sub-progreso de un elemento que solo
// se anima en la ventana [ratio_init, ratio_end]. // se anima en la ventana [ratio_init, ratio_end].
// < ratio_init → 0.0 (no empezó) // < ratio_init → 0.0 (no empezó)
// > ratio_end → 1.0 (terminó) // > ratio_end → 1.0 (terminó)
// en rango → interpolación lineal 0..1 // en rango → interpolación lineal 0..1
[[nodiscard]] auto computeRangeProgress(float global_progress, [[nodiscard]] auto computeRangeProgress(float global_progress,
float ratio_init, float ratio_init,
float ratio_end) -> float; float ratio_end) -> float;
// Calcula posición Y animada de una nave durante INIT_HUD. La nave sube // Calcula posición Y animada de una nave durante INIT_HUD. La nave sube
// desde 50 px bajo el PLAYAREA hasta `final_position` con easing. // desde 50 px bajo el PLAYAREA hasta `final_position` con easing.
[[nodiscard]] auto computeShipPosition(float progress, const Vec2& final_position) -> Vec2; [[nodiscard]] auto computeShipPosition(float progress, const Vec2& final_position) -> Vec2;
// Dibuja los 4 lados del PLAYAREA con efecto pincel en 3 fases: // Dibuja los 4 lados del PLAYAREA con efecto pincel en 3 fases:
// 0..33% → línea superior crece desde el centro hacia los lados. // 0..33% → línea superior crece desde el centro hacia los lados.
// 33..66% → líneas verticales bajan por los laterales. // 33..66% → líneas verticales bajan por los laterales.
// 66..100% → línea inferior crece desde los lados hacia el centro. // 66..100% → línea inferior crece desde los lados hacia el centro.
void drawBordersAnimated(Rendering::Renderer* renderer, float progress); void drawBordersAnimated(Rendering::Renderer* renderer, float progress);
// Dibuja el scoreboard centrado, subiendo desde fuera de la pantalla // Dibuja el scoreboard centrado, subiendo desde fuera de la pantalla
// hasta su posición final con easing. // hasta su posición final con easing.
void drawScoreboardAnimated(const Graphics::VectorText& text, void drawScoreboardAnimated(const Graphics::VectorText& text,
const std::string& scoreboard_text, const std::string& scoreboard_text,
float progress); float progress);
} // namespace Systems::InitHud } // namespace Systems::InitHud
+307 -278
View File
@@ -1,327 +1,356 @@
// ship_animator.cpp - Implementació del sistema de animación de naves // ship_animator.cpp - Implementació de l'animador de naus 3D
// © 2026 JailDesigner // © 2026 JailDesigner
#include "ship_animator.hpp" #include "ship_animator.hpp"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstddef>
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/graphics/shape_loader.hpp" #include "core/graphics/shape_loader.hpp"
#include "core/math/easing.hpp" #include "core/math/easing.hpp"
#include "core/rendering/shape_renderer.hpp"
namespace Title { namespace Title {
ShipAnimator::ShipAnimator(Rendering::Renderer* renderer) namespace {
: renderer_(renderer) {
}
void ShipAnimator::init() { // Profunditat d'extrusió de la silueta 2D de la nau (en unitats mundials).
// Carregar formes de naves con perspectiva pre-calculada // 0.0F → emet només la silueta plana. >0 emet volum extrudit.
auto forma_p1 = Graphics::ShapeLoader::load("ship_perspective.shp"); // Perspectiva izquierda constexpr float SHIP_EXTRUSION_DEPTH = 1.0F;
auto forma_p2 = Graphics::ShapeLoader::load("ship2_perspective.shp"); // Perspectiva derecha
// Configurar ship P1 // VP lògic per definir forward_dir / direcció del path. Tots els paths
ships_[0].player_id = 1; // s'allunyen cap a aquest punt; les naus exiting continuen MÉS ENLLÀ
ships_[0].shape = forma_p1; // (vegeu SHIP_EXIT_TRAVEL) per no desaparèixer en arribar al VP.
configureShipP1(ships_[0]); constexpr float SHIP_EXIT_Z = 800.0F;
constexpr Vec3 VANISHING_POINT{.x = 0.0F, .y = 0.0F, .z = SHIP_EXIT_Z};
// Configurar ship P2 // Profunditat addicional darrere del VP cap a la qual les naus exiting
ships_[1].player_id = 2; // convergeixen. Així P1 (X<0) i P2 (X>0) mantenen sempre els seus
ships_[1].shape = forma_p2; // hemisferis i no es creuen al passar pel VP — totes dues acaben al
configureShipP2(ships_[1]); // centre projectat (640, 360) sense travessar-lo.
} constexpr float SHIP_EXIT_OVERFLOW = 700.0F;
void ShipAnimator::update(float delta_time) { // Directors VP → origen de cada nau, normalitzats. P1 ve des de "les 7"
// Dispatcher segons state de cada ship // del rellotge (baix-esquerra), P2 des de "les 5" (baix-dreta). Els
for (auto& ship : ships_) { // components estan calibrats perquè a TARGET_DIST el pixel projectat
if (!ship.visible) { // caigui aprox sota la "P de PRESS" / "Y de PLAY" del text del títol.
continue; 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) { auto safeNormalize(const Vec3& v, const Vec3& fallback) -> Vec3 {
case ShipState::ENTERING: return v.lengthSquared() > 0.0F ? v.normalized() : fallback;
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;
} }
// Renderizar ship (perspectiva ya incorporada a la shape) auto entryForward(const TitleShip& ship) -> Vec3 {
Rendering::renderShape( return safeNormalize(ship.target_position - ship.initial_position,
renderer_, Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F});
ship.shape, }
ship.current_position,
0.0F, // angle (rotación 2D no utilitzada) auto floatingForward(const Vec3& target) -> Vec3 {
ship.current_scale, return safeNormalize(VANISHING_POINT - target,
1.0F, // progress (siempre visible) Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F});
1.0F // brightness (brightness màxima) }
);
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
// ENTRY_DURATION viu a Defaults::Title::Ships::ENTRY_DURATION (compartit
// amb title_scene.cpp per calcular el threshold T_SHIPS_LANDED).
constexpr float ENTRY_DURATION = Defaults::Title::Ships::ENTRY_DURATION;
constexpr float EXIT_DURATION = Defaults::Title::Ships::EXIT_DURATION;
// 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() { void ShipAnimator::init() {
using namespace Defaults::Title::Ships; 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].player_id = 1;
ships_[0].state = ShipState::ENTERING; if (shape_p1 && shape_p1->isValid()) {
ships_[0].state_time = 0.0F; ships_[0].mesh = Graphics::extrudeShape2D(*shape_p1, SHIP_EXTRUSION_DEPTH);
ships_[0].initial_position = computeOffscreenPosition(CLOCK_8_ANGLE); }
ships_[0].current_position = ships_[0].initial_position; configureShipP1(ships_[0]);
ships_[0].current_scale = ships_[0].initial_scale;
// Configurar ship P2 para l'animación de entrada ships_[1].player_id = 2;
ships_[1].state = ShipState::ENTERING; if (shape_p2 && shape_p2->isValid()) {
ships_[1].state_time = 0.0F; ships_[1].mesh = Graphics::extrudeShape2D(*shape_p2, SHIP_EXTRUSION_DEPTH);
ships_[1].initial_position = computeOffscreenPosition(CLOCK_4_ANGLE); }
ships_[1].current_position = ships_[1].initial_position; configureShipP2(ships_[1]);
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)
} }
}
void ShipAnimator::skipToFloatingState() { void ShipAnimator::update(float delta_time) {
// Posar ambdues naves directament en state FLOATING for (auto& ship : ships_) {
for (auto& ship : ships_) { if (!ship.visible) {
ship.state = ShipState::FLOATING; continue;
ship.state_time = 0.0F; }
ship.oscillation_phase = 0.0F; switch (ship.state) {
case ShipState::ENTERING:
// Posar en posición objetivo (sin animación) updateEntering(ship, delta_time);
ship.current_position = ship.target_position; break;
ship.current_scale = ship.target_scale; case ShipState::FLOATING:
updateFloating(ship, delta_time);
// NO establir visibilitat aquí - ya ho hace el caller break;
// (evita fer visibles ambdues naves cuando solo una ha premut START) case ShipState::EXITING: {
updateExiting(ship, delta_time);
// Transició a invisible: la nau acaba d'arribar al VP.
if (!ship.visible && on_ship_disappear_) {
on_ship_disappear_(ship.player_id);
}
break;
}
}
}
} }
}
auto ShipAnimator::isVisible() const -> bool { void ShipAnimator::draw() const {
// Retorna true si almenys una ship es visible if (camera_ == nullptr || renderer_ == nullptr) {
return std::ranges::any_of(ships_, [](const TitleShip& ship) { return ship.visible; }); return;
} }
for (std::size_t i = 0; i < ships_.size(); ++i) {
const auto& ship = ships_[i];
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,
};
const SDL_Color SHIP_COLOR = (i == 0)
? Defaults::Title::Colors::SHIP_P1
: Defaults::Title::Colors::SHIP_P2;
Graphics::drawWireframe(renderer_, *camera_, ship.mesh, TRANSFORM, 1.0F, SHIP_COLOR);
}
}
void ShipAnimator::triggerExitAnimationForPlayer(int player_id) { void ShipAnimator::startEntryAnimation() {
// Trobar la ship del player especificat for (auto& ship : ships_) {
for (auto& ship : ships_) { ship.state = ShipState::ENTERING;
if (ship.player_id == player_id) { ship.state_time = 0.0F;
// Canviar state a EXITING solo per esta ship 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 = ShipState::EXITING;
ship.state_time = 0.0F; 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; ship.initial_position = ship.current_position;
ship.forward_dir = exitForward(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
} }
} }
}
void ShipAnimator::setVisible(bool visible) { void ShipAnimator::triggerExitAnimationForPlayer(int player_id) {
for (auto& ship : ships_) { for (auto& ship : ships_) {
ship.visible = visible; if (ship.player_id == player_id) {
} ship.state = ShipState::EXITING;
} ship.state_time = 0.0F;
ship.initial_position = ship.current_position;
auto ShipAnimator::isAnimationComplete() const -> bool { ship.forward_dir = exitForward(ship.current_position);
// Comprovar si todas las naves són invisibles (han completat l'animación de salida) break;
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;
} }
// Cálculo del progrés (restant el delay) void ShipAnimator::skipToFloatingState() {
float elapsed = ship.state_time - ship.entry_delay; for (auto& ship : ships_) {
float progress = std::min(1.0F, elapsed / ENTRY_DURATION); 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) void ShipAnimator::setVisible(bool visible) {
float eased_progress = Easing::easeOutQuad(progress); for (auto& ship : ships_) {
ship.visible = visible;
}
}
// Lerp posición (inicial → objetivo) auto ShipAnimator::isVisible() const -> bool {
ship.current_position.x = Easing::lerp(ship.initial_position.x, ship.target_position.x, eased_progress); return std::ranges::any_of(ships_,
ship.current_position.y = Easing::lerp(ship.initial_position.y, ship.target_position.y, eased_progress); [](const TitleShip& s) { return s.visible; });
}
// Lerp scale (grande → normal) auto ShipAnimator::isAnimationComplete() const -> bool {
ship.current_scale = Easing::lerp(ship.initial_scale, ship.target_scale, eased_progress); return std::ranges::all_of(ships_,
[](const TitleShip& s) { return !s.visible; });
}
// Transicionar a FLOATING cuando completi void ShipAnimator::updateEntering(TitleShip& ship, float delta_time) {
if (elapsed >= ENTRY_DURATION) { 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 = ShipState::FLOATING;
ship.state_time = 0.0F; 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) { void ShipAnimator::configureShipP2(TitleShip& ship) {
using namespace Defaults::Title::Ships; ship.state = ShipState::FLOATING;
ship.state_time = 0.0F;
// Actualitzar time i fase de oscil·lació // Target i initial sobre el path VP → "les 5" del rellotge (P2).
ship.state_time += delta_time; ship.target_position = VANISHING_POINT + (PATH_DIR_P2 * TARGET_DIST);
ship.oscillation_phase += delta_time; ship.initial_position = VANISHING_POINT + (PATH_DIR_P2 * ENTRY_DIST);
ship.current_position = ship.initial_position;
// Oscil·lació sinusoïdal X/Y (parámetros específics per ship) ship.target_scale = SHIP_FLOAT_SCALE;
float offset_x = ship.amplitude_x * std::sin(2.0F * Defaults::Math::PI * ship.frequency_x * ship.oscillation_phase); ship.current_scale = SHIP_FLOAT_SCALE;
float offset_y = ship.amplitude_y * std::sin((2.0F * Defaults::Math::PI * ship.frequency_y * ship.oscillation_phase) + FLOAT_PHASE_OFFSET); ship.initial_scale = SHIP_ENTRY_SCALE;
ship.oscillation_phase = 0.0F;
// Aplicar oscil·lació a la posición objetivo ship.entry_delay = P2_ENTRY_DELAY;
ship.current_position.x = ship.target_position.x + offset_x; ship.amplitude_x = FLOAT_AMPLITUDE_X;
ship.current_position.y = ship.target_position.y + offset_y; ship.amplitude_y = FLOAT_AMPLITUDE_Y;
ship.frequency_x = FLOAT_FREQUENCY_X_BASE * P2_FREQUENCY_MULTIPLIER;
// Escala constant (sin "breathing" per ara) ship.frequency_y = FLOAT_FREQUENCY_Y_BASE * P2_FREQUENCY_MULTIPLIER;
ship.current_scale = ship.target_scale; ship.forward_dir = entryForward(ship);
} ship.visible = true;
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;
} }
}
// 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 } // namespace Title
+49 -61
View File
@@ -1,104 +1,92 @@
// 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 // © 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 #pragma once
#include "core/rendering/render_context.hpp"
#include <SDL3/SDL.h>
#include <array> #include <array>
#include <cstdint> #include <cstdint>
#include <memory> #include <functional>
#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" #include "core/types.hpp"
namespace Title { namespace Title {
// Estats de l'animación de la ship enum class ShipState : std::uint8_t {
enum class ShipState : std::uint8_t { ENTERING,
ENTERING, // Entrant desde fuera de pantalla FLOATING,
FLOATING, // Flotante en posición estàtica EXITING,
EXITING // Volant hacia el point de fuga };
};
// Dades de una ship individual al título. struct TitleShip {
// Todos los miembros tienen inicializador por defecto: ShipAnimator::ships_ int player_id{0};
// 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
ShipState state{ShipState::ENTERING}; ShipState state{ShipState::ENTERING};
float state_time{0.0F}; // Temps acumulat en l'state actual float state_time{0.0F};
// Posicions Vec3 initial_position{};
Vec2 initial_position{}; // Posición de start (fuera de pantalla per ENTERING) Vec3 target_position{};
Vec2 target_position{}; // Posición objetivo (rellotge 8 o 4) Vec3 current_position{};
Vec2 current_position{}; // Posición interpolada actual
// Escales (simulació eix Z) float initial_scale{1.0F};
float initial_scale{1.0F}; // Escala de start (més grande = més a prop) float target_scale{1.0F};
float target_scale{1.0F}; // Escala objetivo (mida flotació) float current_scale{1.0F};
float current_scale{1.0F}; // Escala interpolada actual
// Flotació float oscillation_phase{0.0F};
float oscillation_phase{0.0F}; // Acumulador de fase per movement sinusoïdal 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_x{0.0F};
float amplitude_y{0.0F}; float amplitude_y{0.0F};
float frequency_x{0.0F}; float frequency_x{0.0F};
float frequency_y{0.0F}; float frequency_y{0.0F};
// Forma Graphics::Mesh3D mesh;
std::shared_ptr<Graphics::Shape> shape; // 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
// Visibilitat // direcció del seu path actual.
Vec3 forward_dir{.x = 0.0F, .y = 0.0F, .z = 1.0F};
bool visible{false}; bool visible{false};
}; };
// Gestor de animación de naves para l'escena de título class ShipAnimator {
class ShipAnimator { public:
public: ShipAnimator(Rendering::Renderer* renderer, const Graphics::Camera3D* camera);
explicit ShipAnimator(Rendering::Renderer* renderer);
// Cicle de vida
void init(); void init();
void update(float delta_time); void update(float delta_time);
void draw() const; void draw() const;
// Control de state (cridat per TitleScene)
void startEntryAnimation(); void startEntryAnimation();
void triggerExitAnimation(); // Anima todas las naves void triggerExitAnimation();
void triggerExitAnimationForPlayer(int player_id); // Anima solo una ship (P1=1, P2=2) void triggerExitAnimationForPlayer(int player_id);
void skipToFloatingState(); // Salta directament a FLOATING sin animación void skipToFloatingState();
// Control de visibilitat
void setVisible(bool visible); void setVisible(bool visible);
[[nodiscard]] auto isAnimationComplete() const -> bool; [[nodiscard]] auto isAnimationComplete() const -> bool;
[[nodiscard]] auto isVisible() const -> bool; // Comprova si alguna ship es visible [[nodiscard]] auto isVisible() const -> bool;
private: // Callback disparat quan una nau acaba l'EXITING (es torna invisible
// al VP). Útil per a un destell que tapi el pop final.
using ShipDisappearCallback = std::function<void(int player_id)>;
void setOnShipDisappear(ShipDisappearCallback cb) { on_ship_disappear_ = std::move(cb); }
private:
Rendering::Renderer* renderer_; Rendering::Renderer* renderer_;
std::array<TitleShip, 2> ships_; // Naves P1 i P2 const Graphics::Camera3D* camera_;
std::array<TitleShip, 2> ships_;
ShipDisappearCallback on_ship_disappear_;
// 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 updateEntering(TitleShip& ship, float delta_time);
static void updateFloating(TitleShip& ship, float delta_time); static void updateFloating(TitleShip& ship, float delta_time);
static void updateExiting(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 configureShipP1(TitleShip& ship);
static void configureShipP2(TitleShip& ship); static void configureShipP2(TitleShip& ship);
[[nodiscard]] static auto computeOffscreenPosition(float angle_rellotge) -> Vec2; };
};
} // namespace Title } // namespace Title
-328
View File
@@ -1,328 +0,0 @@
// ship_animator3d.cpp - Implementació de l'animador de naus 3D
// © 2026 JailDesigner
#include "ship_animator3d.hpp"
#include <algorithm>
#include <cmath>
#include "core/defaults.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/math/easing.hpp"
namespace Title {
namespace {
// Profunditat d'extrusió de la silueta 2D de la nau (en unitats mundials).
// 0.0F → emet només la silueta plana. >0 emet volum extrudit.
constexpr float SHIP_EXTRUSION_DEPTH = 1.0F;
// Punt de fuga (al fons, centre projectat). Tots els paths convergeixen aquí.
constexpr float SHIP_EXIT_Z = 800.0F;
constexpr Vec3 VANISHING_POINT{.x = 0.0F, .y = 0.0F, .z = SHIP_EXIT_Z};
// Directors VP → origen de cada nau, normalitzats. P1 ve des de "les 7"
// del rellotge (baix-esquerra), P2 des de "les 5" (baix-dreta). Els
// components estan calibrats perquè a TARGET_DIST el pixel projectat
// caigui aprox sota la "P de PRESS" / "Y de PLAY" del text del títol.
constexpr Vec3 PATH_DIR_P1{.x = -0.0887F, .y = -0.0683F, .z = -0.9938F};
constexpr Vec3 PATH_DIR_P2{.x = +0.0887F, .y = -0.0683F, .z = -0.9938F};
// Distàncies des del VP al llarg del path (unitats mundials).
constexpr float TARGET_DIST = 563.5F; // Descans a Z≈240 → pixel ≈ (510, 460)
constexpr float ENTRY_DIST = 750.0F; // Inicial a Z≈54 → fora pantalla baix-esq.
// Pitch addicional sobre el look-at pur per fer que el dors de la nau
// s'incline cap a la càmera (~-14° afegits). Amb forward quasi paral·lel
// a +Z, el pitch look-at és ~-94°; afegint això queda al voltant de -108°,
// que és l'angle visualment validat com a "bo" per l'usuari.
constexpr float PITCH_LIFT_RAD = -0.25F;
// Look-at: calcula pitch+yaw que duen (0,-1,0) local a forward_dir mundial.
// Requereix l'ordre de rotació X→Y→Z al applyTransform de wireframe3d.
// Si forward és quasi vertical (sin(pitch) ≈ 0), retorna yaw=0 (qualsevol).
auto computePitchYawForLookAt(const Vec3& forward_dir) -> Vec2 {
const float DY = std::clamp(forward_dir.y, -1.0F, 1.0F);
const float PITCH_LOOKAT = -std::acos(-DY); // ∈ [-π, 0]
const float SIN_PITCH = std::sin(PITCH_LOOKAT);
float yaw = 0.0F;
if (std::abs(SIN_PITCH) >= 1.0E-5F) {
const float SY = -forward_dir.x / SIN_PITCH;
const float CY = -forward_dir.z / SIN_PITCH;
yaw = std::atan2(SY, CY);
}
return Vec2{.x = PITCH_LOOKAT + PITCH_LIFT_RAD, .y = yaw};
}
auto safeNormalize(const Vec3& v, const Vec3& fallback) -> Vec3 {
return v.lengthSquared() > 0.0F ? v.normalized() : fallback;
}
auto entryForward(const TitleShip3D& ship) -> Vec3 {
return safeNormalize(ship.target_position - ship.initial_position,
Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F});
}
auto floatingForward(const Vec3& target) -> Vec3 {
return safeNormalize(VANISHING_POINT - target,
Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F});
}
auto exitForward(const Vec3& current) -> Vec3 {
return safeNormalize(VANISHING_POINT - current,
Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F});
}
// Mida visual i animació.
constexpr float SHIP_FLOAT_SCALE = 1.1F;
constexpr float SHIP_ENTRY_SCALE = 1.1F; // Mida mundial idèntica; la perspectiva fa la resta
constexpr float ENTRY_DURATION = 2.0F;
constexpr float EXIT_DURATION = 1.0F;
// Oscil·lació en unitats mundials (al voltant del target_position).
constexpr float FLOAT_AMPLITUDE_X = 1.5F;
constexpr float FLOAT_AMPLITUDE_Y = 1.0F;
constexpr float FLOAT_FREQUENCY_X_BASE = 0.5F;
constexpr float FLOAT_FREQUENCY_Y_BASE = 0.7F;
constexpr float FLOAT_PHASE_OFFSET = 1.57F;
constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F;
constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F;
constexpr float P1_ENTRY_DELAY = 0.0F;
constexpr float P2_ENTRY_DELAY = 0.5F;
} // namespace
ShipAnimator3D::ShipAnimator3D(Rendering::Renderer* renderer,
const Graphics::Camera3D* camera)
: renderer_(renderer),
camera_(camera) {
}
void ShipAnimator3D::init() {
auto shape_p1 = Graphics::ShapeLoader::load("ship.shp");
auto shape_p2 = Graphics::ShapeLoader::load("ship2.shp");
ships_[0].player_id = 1;
if (shape_p1 && shape_p1->isValid()) {
ships_[0].mesh = Graphics::extrudeShape2D(*shape_p1, SHIP_EXTRUSION_DEPTH);
}
configureShipP1(ships_[0]);
ships_[1].player_id = 2;
if (shape_p2 && shape_p2->isValid()) {
ships_[1].mesh = Graphics::extrudeShape2D(*shape_p2, SHIP_EXTRUSION_DEPTH);
}
configureShipP2(ships_[1]);
}
void ShipAnimator3D::update(float delta_time) {
for (auto& ship : ships_) {
if (!ship.visible) {
continue;
}
switch (ship.state) {
case ShipState3D::ENTERING:
updateEntering(ship, delta_time);
break;
case ShipState3D::FLOATING:
updateFloating(ship, delta_time);
break;
case ShipState3D::EXITING:
updateExiting(ship, delta_time);
break;
}
}
}
void ShipAnimator3D::draw() const {
if (camera_ == nullptr || renderer_ == nullptr) {
return;
}
for (const auto& ship : ships_) {
if (!ship.visible) {
continue;
}
const Vec2 EULER = computePitchYawForLookAt(ship.forward_dir);
const Graphics::Transform3D TRANSFORM{
.position = ship.current_position,
.rotation_euler = Vec3{.x = EULER.x, .y = EULER.y, .z = 0.0F},
.scale = ship.current_scale,
};
Graphics::drawWireframe(renderer_, *camera_, ship.mesh, TRANSFORM, 1.0F);
}
}
void ShipAnimator3D::startEntryAnimation() {
for (auto& ship : ships_) {
ship.state = ShipState3D::ENTERING;
ship.state_time = 0.0F;
ship.current_position = ship.initial_position;
ship.current_scale = ship.initial_scale;
ship.forward_dir = entryForward(ship);
}
}
void ShipAnimator3D::triggerExitAnimation() {
for (auto& ship : ships_) {
ship.state = ShipState3D::EXITING;
ship.state_time = 0.0F;
ship.initial_position = ship.current_position;
ship.forward_dir = exitForward(ship.current_position);
}
}
void ShipAnimator3D::triggerExitAnimationForPlayer(int player_id) {
for (auto& ship : ships_) {
if (ship.player_id == player_id) {
ship.state = ShipState3D::EXITING;
ship.state_time = 0.0F;
ship.initial_position = ship.current_position;
ship.forward_dir = exitForward(ship.current_position);
break;
}
}
}
void ShipAnimator3D::skipToFloatingState() {
for (auto& ship : ships_) {
ship.state = ShipState3D::FLOATING;
ship.state_time = 0.0F;
ship.oscillation_phase = 0.0F;
ship.current_position = ship.target_position;
ship.current_scale = ship.target_scale;
ship.forward_dir = floatingForward(ship.target_position);
}
}
void ShipAnimator3D::setVisible(bool visible) {
for (auto& ship : ships_) {
ship.visible = visible;
}
}
auto ShipAnimator3D::isVisible() const -> bool {
return std::ranges::any_of(ships_,
[](const TitleShip3D& s) { return s.visible; });
}
auto ShipAnimator3D::isAnimationComplete() const -> bool {
return std::ranges::all_of(ships_,
[](const TitleShip3D& s) { return !s.visible; });
}
void ShipAnimator3D::updateEntering(TitleShip3D& ship, float delta_time) {
ship.state_time += delta_time;
if (ship.state_time < ship.entry_delay) {
ship.current_position = ship.initial_position;
ship.current_scale = ship.initial_scale;
return;
}
const float ELAPSED = ship.state_time - ship.entry_delay;
const float PROGRESS = std::min(1.0F, ELAPSED / ENTRY_DURATION);
const float EASED = Easing::easeOutQuad(PROGRESS);
// Acumula la fase d'oscil·lació també durant ENTERING; sense això,
// al passar a FLOATING la posició salta d'amplitud_y de cop perquè
// l'offset Y comença a sin(π/2) = 1. Acumulant-la abans, la nau
// ja oscil·la mentre s'aproxima i la transició és contínua.
ship.oscillation_phase += delta_time;
const float INTERP_X = Easing::lerp(ship.initial_position.x, ship.target_position.x, EASED);
const float INTERP_Y = Easing::lerp(ship.initial_position.y, ship.target_position.y, EASED);
const float INTERP_Z = Easing::lerp(ship.initial_position.z, ship.target_position.z, EASED);
const float TWO_PI = 2.0F * Defaults::Math::PI;
const float OFFSET_X = ship.amplitude_x *
std::sin(TWO_PI * ship.frequency_x * ship.oscillation_phase);
const float OFFSET_Y = ship.amplitude_y *
std::sin((TWO_PI * ship.frequency_y * ship.oscillation_phase) + FLOAT_PHASE_OFFSET);
ship.current_position.x = INTERP_X + OFFSET_X;
ship.current_position.y = INTERP_Y + OFFSET_Y;
ship.current_position.z = INTERP_Z;
ship.current_scale = Easing::lerp(ship.initial_scale, ship.target_scale, EASED);
if (ELAPSED >= ENTRY_DURATION) {
ship.state = ShipState3D::FLOATING;
ship.state_time = 0.0F;
// No resetegem oscillation_phase: així updateFloating continua
// l'oscil·lació iniciada durant ENTERING sense salt.
ship.forward_dir = floatingForward(ship.target_position);
}
}
void ShipAnimator3D::updateFloating(TitleShip3D& ship, float delta_time) {
ship.state_time += delta_time;
ship.oscillation_phase += delta_time;
const float TWO_PI = 2.0F * Defaults::Math::PI;
const float OFFSET_X = ship.amplitude_x *
std::sin(TWO_PI * ship.frequency_x * ship.oscillation_phase);
const float OFFSET_Y = ship.amplitude_y *
std::sin((TWO_PI * ship.frequency_y * ship.oscillation_phase) + FLOAT_PHASE_OFFSET);
ship.current_position.x = ship.target_position.x + OFFSET_X;
ship.current_position.y = ship.target_position.y + OFFSET_Y;
ship.current_position.z = ship.target_position.z;
ship.current_scale = ship.target_scale;
}
void ShipAnimator3D::updateExiting(TitleShip3D& ship, float delta_time) {
ship.state_time += delta_time;
const float PROGRESS = std::min(1.0F, ship.state_time / EXIT_DURATION);
const float EASED = Easing::easeInQuad(PROGRESS);
// Vola cap al centre projectat (x=0, y=0) i a Z gran (lluny).
ship.current_position.x = Easing::lerp(ship.initial_position.x, 0.0F, EASED);
ship.current_position.y = Easing::lerp(ship.initial_position.y, 0.0F, EASED);
ship.current_position.z = Easing::lerp(ship.initial_position.z, SHIP_EXIT_Z, EASED);
ship.current_scale = ship.target_scale; // L'escala visual baixa via la perspectiva
if (PROGRESS >= 1.0F) {
ship.visible = false;
}
}
void ShipAnimator3D::configureShipP1(TitleShip3D& ship) {
ship.state = ShipState3D::FLOATING;
ship.state_time = 0.0F;
// Target i initial sobre el path VP → "les 7" del rellotge (P1).
ship.target_position = VANISHING_POINT + (PATH_DIR_P1 * TARGET_DIST);
ship.initial_position = VANISHING_POINT + (PATH_DIR_P1 * ENTRY_DIST);
ship.current_position = ship.initial_position;
ship.target_scale = SHIP_FLOAT_SCALE;
ship.current_scale = SHIP_FLOAT_SCALE;
ship.initial_scale = SHIP_ENTRY_SCALE;
ship.oscillation_phase = 0.0F;
ship.entry_delay = P1_ENTRY_DELAY;
ship.amplitude_x = FLOAT_AMPLITUDE_X;
ship.amplitude_y = FLOAT_AMPLITUDE_Y;
ship.frequency_x = FLOAT_FREQUENCY_X_BASE * P1_FREQUENCY_MULTIPLIER;
ship.frequency_y = FLOAT_FREQUENCY_Y_BASE * P1_FREQUENCY_MULTIPLIER;
ship.forward_dir = entryForward(ship);
ship.visible = true;
}
void ShipAnimator3D::configureShipP2(TitleShip3D& ship) {
ship.state = ShipState3D::FLOATING;
ship.state_time = 0.0F;
// Target i initial sobre el path VP → "les 5" del rellotge (P2).
ship.target_position = VANISHING_POINT + (PATH_DIR_P2 * TARGET_DIST);
ship.initial_position = VANISHING_POINT + (PATH_DIR_P2 * ENTRY_DIST);
ship.current_position = ship.initial_position;
ship.target_scale = SHIP_FLOAT_SCALE;
ship.current_scale = SHIP_FLOAT_SCALE;
ship.initial_scale = SHIP_ENTRY_SCALE;
ship.oscillation_phase = 0.0F;
ship.entry_delay = P2_ENTRY_DELAY;
ship.amplitude_x = FLOAT_AMPLITUDE_X;
ship.amplitude_y = FLOAT_AMPLITUDE_Y;
ship.frequency_x = FLOAT_FREQUENCY_X_BASE * P2_FREQUENCY_MULTIPLIER;
ship.frequency_y = FLOAT_FREQUENCY_Y_BASE * P2_FREQUENCY_MULTIPLIER;
ship.forward_dir = entryForward(ship);
ship.visible = true;
}
} // namespace Title
-85
View File
@@ -1,85 +0,0 @@
// ship_animator3d.hpp - Sistema d'animació de naus 3D per a l'escena de títol
// © 2026 JailDesigner
//
// Equivalent 3D del `Title::ShipAnimator`. Manté la mateixa màquina d'estats
// (ENTERING → FLOATING → EXITING) però treballa amb posicions Vec3 i emet
// wireframes a través d'una `Camera3D`. La geometria s'extrau de `ship.shp`
// (P1) i `ship2.shp` (P2) per extrusió en Z.
#pragma once
#include <array>
#include <cstdint>
#include "core/graphics/camera3d.hpp"
#include "core/graphics/wireframe3d.hpp"
#include "core/rendering/render_context.hpp"
#include "core/types.hpp"
namespace Title {
enum class ShipState3D : std::uint8_t {
ENTERING,
FLOATING,
EXITING,
};
struct TitleShip3D {
int player_id{0};
ShipState3D state{ShipState3D::ENTERING};
float state_time{0.0F};
Vec3 initial_position{};
Vec3 target_position{};
Vec3 current_position{};
float initial_scale{1.0F};
float target_scale{1.0F};
float current_scale{1.0F};
float oscillation_phase{0.0F};
float entry_delay{0.0F};
float amplitude_x{0.0F};
float amplitude_y{0.0F};
float frequency_x{0.0F};
float frequency_y{0.0F};
Graphics::Mesh3D mesh;
// Vector mundial cap a on apunta el front del shape. Recalculat a cada
// transició d'estat perquè draw() oriente la nau (look-at) en la
// direcció del seu path actual.
Vec3 forward_dir{.x = 0.0F, .y = 0.0F, .z = 1.0F};
bool visible{false};
};
class ShipAnimator3D {
public:
ShipAnimator3D(Rendering::Renderer* renderer, const Graphics::Camera3D* camera);
void init();
void update(float delta_time);
void draw() const;
void startEntryAnimation();
void triggerExitAnimation();
void triggerExitAnimationForPlayer(int player_id);
void skipToFloatingState();
void setVisible(bool visible);
[[nodiscard]] auto isAnimationComplete() const -> bool;
[[nodiscard]] auto isVisible() const -> bool;
private:
Rendering::Renderer* renderer_;
const Graphics::Camera3D* camera_;
std::array<TitleShip3D, 2> ships_;
static void updateEntering(TitleShip3D& ship, float delta_time);
static void updateFloating(TitleShip3D& ship, float delta_time);
static void updateExiting(TitleShip3D& ship, float delta_time);
static void configureShipP1(TitleShip3D& ship);
static void configureShipP2(TitleShip3D& ship);
};
} // namespace Title
+29 -26
View File
@@ -1,33 +1,36 @@
// main.cpp - Punt d'entrada de l'aplicació // main.cpp - Punt d'entrada amb SDL_MAIN_USE_CALLBACKS
// © 2026 JailDesigner // © 2026 JailDesigner
// //
// Aquí orquestrem la capa de persistència (YAML via game/ConfigYaml::*) i // El Director és EL programa: posseeix la configuració, els subsistemes i
// injectem el resultat al Director. El Director queda independent de // l'estat. Aquestes 4 callbacks són la fontaneria mínima que SDL3 demana
// game/config_yaml.hpp i pot operar només amb Config::EngineConfig. // per arrencar, processar events, iterar i tancar.
#include <string> #define SDL_MAIN_USE_CALLBACKS 1
#include <vector>
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <memory>
#include "core/config/engine_config.hpp"
#include "core/system/director.hpp" #include "core/system/director.hpp"
#include "game/config_yaml.hpp"
auto main(int argc, char* argv[]) -> int { auto SDL_AppInit(void** appstate, int argc, char* argv[]) -> SDL_AppResult {
// Convertir arguments a std::vector<std::string> auto director = std::make_unique<Director>(argc, argv);
std::vector<std::string> args(argv, argv + argc); *appstate = director.release();
return SDL_APP_CONTINUE;
// Capa de persistència delegada: lambdes prim que enllacen el contracte }
// de Config::ConfigPersistence amb la implementació YAML de ConfigYaml::*.
const Config::ConfigPersistence PERSISTENCE{ auto SDL_AppEvent(void* appstate, SDL_Event* event) -> SDL_AppResult {
.init_defaults = [] { ConfigYaml::init(); }, auto* director = static_cast<Director*>(appstate);
.set_path = [](const std::string& path) { ConfigYaml::setConfigFile(path); }, return director->handleEvent(*event);
.load = [] { return ConfigYaml::loadFromFile(); }, }
.save = [] { return ConfigYaml::saveToFile(); },
}; auto SDL_AppIterate(void* appstate) -> SDL_AppResult {
auto* director = static_cast<Director*>(appstate);
// El Director rep la struct d'engine_config + la capa de persistència. return director->iterate();
// No coneix ConfigYaml:: directament. }
Director director(args, ConfigYaml::engine_config, PERSISTENCE);
void SDL_AppQuit(void* appstate, SDL_AppResult /*result*/) {
return director.run(); // Reabsorbim la propietat: el destructor del Director allibera tot.
std::unique_ptr<Director> director(static_cast<Director*>(appstate));
} }