Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 87f818ef96 | |||
| 7eafe21623 | |||
| 22827c28fa | |||
| 8c21345f14 | |||
| 56d7d4af52 | |||
| 71c43ec6fe | |||
| 443b461974 | |||
| cc16908b86 | |||
| c4c6881bd6 | |||
| 35d720bb77 | |||
| 274ce1ca63 | |||
| 252e881e93 | |||
| d36ad7d1c5 | |||
| 7305d2f5dc | |||
| 4cfad053f0 | |||
| 807f71ffa7 | |||
| d12f24d798 | |||
| f9d2539a45 | |||
| 87bfccd14f | |||
| e5e3729215 | |||
| 6210985548 | |||
| 20250a0d6d | |||
| e5616f7c3a | |||
| 3b1e469a4f | |||
| 70ca19eb87 | |||
| 7e52eaeddb | |||
| d618b6d561 | |||
| e954d4ea59 | |||
| b1ee23cd20 | |||
| d86b10c14e | |||
| 1ea38d4f6a | |||
| 26bd5a9efa | |||
| 4b0d85c010 | |||
| 149b485a9b | |||
| 6b1f064cda | |||
| 1cef6a2c23 | |||
| 007460dc51 | |||
| 10057a82de | |||
| 73fa5bf1d1 | |||
| c32b564da1 | |||
| 7b9b5ce569 | |||
| f0b3a1fbc4 | |||
| 869b4374ba | |||
| ea192cd9de | |||
| 5d30f6be68 | |||
| a342d79b86 | |||
| 1db7368c9f | |||
| 88b002b277 | |||
| 044a3a3bbf | |||
| 49070aa843 | |||
| 18e05e36e6 | |||
| bf79eecca0 | |||
| b80216dce1 | |||
| 87138f9a1f | |||
| c6560514d8 | |||
| 839f73e1ef | |||
| 2ca2062011 | |||
| 03209ee23b | |||
| c61299f17f | |||
| 880af293ef | |||
| 67c59992c9 | |||
| be3d696f60 | |||
| 6b8f6a267d | |||
| 120b8ada38 | |||
| 8bb052981d | |||
| 7fc8e48596 | |||
| ff518195f8 | |||
| 54d3e683a1 | |||
| a29c2b9cc2 | |||
| 85e7e70767 | |||
| 3f10c61e22 | |||
| 5de9a5003b |
+12
-6
@@ -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 ---
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Orni Attack - locale: Catala (valencia)
|
||||||
|
# Interficie traduida; pool in-game identic a en.yaml (es queda en angles).
|
||||||
|
# Tots els textos en ASCII: VectorText no suporta caracters accentuats.
|
||||||
|
|
||||||
|
notification:
|
||||||
|
press_again_exit: "PREMEU ESC UN ALTRE COP PER EIXIR"
|
||||||
|
zoom: "ZOOM: {z}X"
|
||||||
|
fullscreen_on: "PANTALLA COMPLETA"
|
||||||
|
fullscreen_off: "MODE FINESTRA"
|
||||||
|
vsync_on: "VSYNC ACTIU"
|
||||||
|
vsync_off: "VSYNC INACTIU"
|
||||||
|
antialias_on: "AA ACTIU"
|
||||||
|
antialias_off: "AA INACTIU"
|
||||||
|
postfx_on: "POSTPROCESSAT ACTIU"
|
||||||
|
postfx_off: "POSTPROCESSAT INACTIU"
|
||||||
|
locale_switched: "IDIOMA: {lang}"
|
||||||
|
|
||||||
|
language:
|
||||||
|
ca: "CATALA"
|
||||||
|
en: "ANGLES"
|
||||||
|
|
||||||
|
hud:
|
||||||
|
level: "NIVELL "
|
||||||
|
|
||||||
|
title:
|
||||||
|
press_start: "PREMEU START PER JUGAR"
|
||||||
|
|
||||||
|
game_screen:
|
||||||
|
game_over: "FI DEL JOC"
|
||||||
|
continue: "CONTINUAR"
|
||||||
|
continues_left: "CONTINUACIONS: {n}"
|
||||||
|
|
||||||
|
stage:
|
||||||
|
start:
|
||||||
|
- "ORNI ALERT!"
|
||||||
|
- "INCOMING ORNIS!"
|
||||||
|
- "ROLLING THREAT!"
|
||||||
|
- "ENEMY WAVE!"
|
||||||
|
- "WAVE OF ORNIS DETECTED!"
|
||||||
|
- "NEXT SWARM APPROACHING!"
|
||||||
|
- "BRACE FOR THE NEXT WAVE!"
|
||||||
|
- "ANOTHER ATTACK INCOMING!"
|
||||||
|
- "SENSORS DETECT HOSTILE ORNIS..."
|
||||||
|
- "UNIDENTIFIED ROLLING OBJECTS INBOUND!"
|
||||||
|
- "ENEMY FORCES MOBILIZING!"
|
||||||
|
- "PREPARE FOR IMPACT!"
|
||||||
|
completed: "GOOD JOB COMMANDER!"
|
||||||
|
|
||||||
|
service_menu:
|
||||||
|
title: "MENU DE SERVEI"
|
||||||
|
video: "VIDEO"
|
||||||
|
audio: "AUDIO"
|
||||||
|
options: "OPCIONS"
|
||||||
|
system: "SISTEMA"
|
||||||
|
controls: "CONTROLS"
|
||||||
|
back: "ENRERE"
|
||||||
|
exit: "EIXIR DEL JOC"
|
||||||
|
# Items del submenu VIDEO
|
||||||
|
video_zoom: "ZOOM"
|
||||||
|
video_fullscreen: "PANTALLA COMPLETA"
|
||||||
|
video_vsync: "VSYNC"
|
||||||
|
video_aa: "ANTIALIAS"
|
||||||
|
video_postfx: "POSTPROCESSAT"
|
||||||
|
video_resolution: "RESOLUCIO"
|
||||||
|
# Items del submenu OPCIONS
|
||||||
|
options_language: "IDIOMA"
|
||||||
|
options_show_info: "MOSTRAR INFO"
|
||||||
|
# Items del submenu AUDIO
|
||||||
|
audio_master: "AUDIO"
|
||||||
|
audio_master_volume: "VOLUM GENERAL"
|
||||||
|
audio_music: "MUSICA"
|
||||||
|
audio_music_volume: "VOLUM MUSICA"
|
||||||
|
audio_sound: "EFECTES"
|
||||||
|
audio_sound_volume: "VOLUM EFECTES"
|
||||||
|
# Items del submenu SISTEMA
|
||||||
|
system_restart: "REINICIAR"
|
||||||
|
# Pagines de confirmacio (estructura: titol + NO/SI)
|
||||||
|
confirm_restart: "ESTAS SEGUR DE REINICIAR?"
|
||||||
|
confirm_exit: "ESTAS SEGUR DE EIXIR?"
|
||||||
|
confirm_no: "NO"
|
||||||
|
confirm_yes: "SI"
|
||||||
|
# Valors comuns
|
||||||
|
value_on: "ACTIU"
|
||||||
|
value_off: "INACTIU"
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Orni Attack - locale: English
|
||||||
|
# In-game pool kept English in both locales per design.
|
||||||
|
|
||||||
|
notification:
|
||||||
|
press_again_exit: "PRESS ESC AGAIN TO EXIT"
|
||||||
|
zoom: "ZOOM: {z}X"
|
||||||
|
fullscreen_on: "FULLSCREEN"
|
||||||
|
fullscreen_off: "WINDOWED"
|
||||||
|
vsync_on: "VSYNC ON"
|
||||||
|
vsync_off: "VSYNC OFF"
|
||||||
|
antialias_on: "AA ON"
|
||||||
|
antialias_off: "AA OFF"
|
||||||
|
postfx_on: "POSTPROCESS ON"
|
||||||
|
postfx_off: "POSTPROCESS OFF"
|
||||||
|
locale_switched: "LANGUAGE: {lang}"
|
||||||
|
|
||||||
|
language:
|
||||||
|
ca: "CATALAN"
|
||||||
|
en: "ENGLISH"
|
||||||
|
|
||||||
|
hud:
|
||||||
|
level: "LEVEL "
|
||||||
|
|
||||||
|
title:
|
||||||
|
press_start: "PRESS START TO PLAY"
|
||||||
|
|
||||||
|
game_screen:
|
||||||
|
game_over: "GAME OVER"
|
||||||
|
continue: "CONTINUE"
|
||||||
|
continues_left: "CONTINUES LEFT: {n}"
|
||||||
|
|
||||||
|
stage:
|
||||||
|
start:
|
||||||
|
- "ORNI ALERT!"
|
||||||
|
- "INCOMING ORNIS!"
|
||||||
|
- "ROLLING THREAT!"
|
||||||
|
- "ENEMY WAVE!"
|
||||||
|
- "WAVE OF ORNIS DETECTED!"
|
||||||
|
- "NEXT SWARM APPROACHING!"
|
||||||
|
- "BRACE FOR THE NEXT WAVE!"
|
||||||
|
- "ANOTHER ATTACK INCOMING!"
|
||||||
|
- "SENSORS DETECT HOSTILE ORNIS..."
|
||||||
|
- "UNIDENTIFIED ROLLING OBJECTS INBOUND!"
|
||||||
|
- "ENEMY FORCES MOBILIZING!"
|
||||||
|
- "PREPARE FOR IMPACT!"
|
||||||
|
completed: "GOOD JOB COMMANDER!"
|
||||||
|
|
||||||
|
service_menu:
|
||||||
|
title: "SERVICE MENU"
|
||||||
|
video: "VIDEO"
|
||||||
|
audio: "AUDIO"
|
||||||
|
options: "OPTIONS"
|
||||||
|
system: "SYSTEM"
|
||||||
|
controls: "CONTROLS"
|
||||||
|
back: "BACK"
|
||||||
|
exit: "EXIT GAME"
|
||||||
|
# Items of VIDEO submenu
|
||||||
|
video_zoom: "ZOOM"
|
||||||
|
video_fullscreen: "FULLSCREEN"
|
||||||
|
video_vsync: "VSYNC"
|
||||||
|
video_aa: "ANTIALIAS"
|
||||||
|
video_postfx: "POSTPROCESS"
|
||||||
|
video_resolution: "RESOLUTION"
|
||||||
|
# Items of OPTIONS submenu
|
||||||
|
options_language: "LANGUAGE"
|
||||||
|
options_show_info: "SHOW INFO"
|
||||||
|
# Items of AUDIO submenu
|
||||||
|
audio_master: "AUDIO"
|
||||||
|
audio_master_volume: "MASTER VOLUME"
|
||||||
|
audio_music: "MUSIC"
|
||||||
|
audio_music_volume: "MUSIC VOLUME"
|
||||||
|
audio_sound: "SOUNDS"
|
||||||
|
audio_sound_volume: "SOUND VOLUME"
|
||||||
|
# Items of SYSTEM submenu
|
||||||
|
system_restart: "RESTART"
|
||||||
|
# Confirmation pages (structure: title + NO/YES)
|
||||||
|
confirm_restart: "REALLY RESTART?"
|
||||||
|
confirm_exit: "REALLY EXIT?"
|
||||||
|
confirm_no: "NO"
|
||||||
|
confirm_yes: "YES"
|
||||||
|
# Common values
|
||||||
|
value_on: "ON"
|
||||||
|
value_off: "OFF"
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
# ship2_perspective.shp - Nave P2 con perspectiva pre-calculada
|
|
||||||
# Posición optimizada: "4 del reloj" (Abajo-Derecha)
|
|
||||||
# Dirección: Volando hacia el fondo (centro pantalla)
|
|
||||||
|
|
||||||
name: ship2_perspective
|
|
||||||
scale: 1.0
|
|
||||||
center: 0, 0
|
|
||||||
|
|
||||||
# TRANSFORMACIÓN APLICADA:
|
|
||||||
# 1. Rotación -45° (apuntando al centro desde abajo-dcha)
|
|
||||||
# 2. Proyección de perspectiva:
|
|
||||||
# - Punta (p1): Reducida al 60% (simula lejanía)
|
|
||||||
# - Base (p2, p3): Aumentada al 110% (simula cercanía)
|
|
||||||
# 3. Flip horizontal (simétrica a ship_starfield.shp)
|
|
||||||
#
|
|
||||||
# Nuevos Punts (aprox):
|
|
||||||
# p1 (Punta): (-4, -4) -> Lejos, pequeña y apuntando arriba-izq
|
|
||||||
# p2 (Ala Izq): (-3, 11) -> Cerca, lado interior
|
|
||||||
# p4 (Base Cnt): (3, 5) -> Centro base
|
|
||||||
# p3 (Ala Dcha): (11, 2) -> Cerca, lado exterior (más grande)
|
|
||||||
|
|
||||||
#polyline: -4,-4 -3,11 3,5 11,2 -4,-4
|
|
||||||
polyline: -4,-4 -3,11 11,2 -4,-4
|
|
||||||
|
|
||||||
# Circulito central (octàgon r=2.5)
|
|
||||||
# Distintiu visual del jugador 2
|
|
||||||
# Sin perspectiva (está en el centro de la nave)
|
|
||||||
polyline: 0,-2.5 1.77,-1.77 2.5,0 1.77,1.77 0,2.5 -1.77,1.77 -2.5,0 -1.77,-1.77 0,-2.5
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# ship_perspective.shp - Nave con perspectiva pre-calculada
|
|
||||||
# Posición optimizada: "8 del reloj" (Abajo-Izquierda)
|
|
||||||
# Dirección: Volando hacia el fondo (centro pantalla)
|
|
||||||
|
|
||||||
name: ship_perspective
|
|
||||||
scale: 1.0
|
|
||||||
center: 0, 0
|
|
||||||
|
|
||||||
# TRANSFORMACIÓN APLICADA:
|
|
||||||
# 1. Rotación +45° (apuntando al centro desde abajo-izq)
|
|
||||||
# 2. Proyección de perspectiva:
|
|
||||||
# - Punta (p1): Reducida al 60% (simula lejanía)
|
|
||||||
# - Base (p2, p3): Aumentada al 110% (simula cercanía)
|
|
||||||
#
|
|
||||||
# Nuevos Puntos (aprox):
|
|
||||||
# p1 (Punta): (4, -4) -> Lejos, pequeña y apuntando arriba-dcha
|
|
||||||
# p2 (Ala Dcha): (3, 11) -> Cerca, lado interior
|
|
||||||
# p4 (Base Cnt): (-3, 5) -> Centro base
|
|
||||||
# p3 (Ala Izq): (-11, 2) -> Cerca, lado exterior (más grande)
|
|
||||||
|
|
||||||
polyline: 4,-4 3,11 -3,5 -11,2 4,-4
|
|
||||||
@@ -0,0 +1,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.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
|
|||||||
@@ -238,14 +238,27 @@ auto Audio::effectiveVolume(float volume, bool channel_enabled) const -> float {
|
|||||||
return (enabled_ && channel_enabled) ? volume * config_.volume : 0.0F;
|
return (enabled_ && channel_enabled) ? volume * config_.volume : 0.0F;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estableix el volum dels sons (float 0.0..1.0)
|
// Estableix el volum dels sons (float 0.0..1.0). Actualitza el valor cachejat
|
||||||
|
// a config_ perquè els getters i les re-aplicacions internes (enableSound,
|
||||||
|
// setMasterVolume) puguin tornar al volum que l'usuari va triar.
|
||||||
void Audio::setSoundVolume(float sound_volume, Group group) {
|
void Audio::setSoundVolume(float sound_volume, Group group) {
|
||||||
engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group));
|
config_.sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
|
||||||
|
engine_->setSoundVolume(effectiveVolume(config_.sound_volume, sound_enabled_), static_cast<int>(group));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estableix el volum de la música (float 0.0..1.0)
|
// Estableix el volum de la música (float 0.0..1.0). Cf. setSoundVolume.
|
||||||
void Audio::setMusicVolume(float music_volume) {
|
void Audio::setMusicVolume(float music_volume) {
|
||||||
engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_));
|
config_.music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
|
||||||
|
engine_->setMusicVolume(effectiveVolume(config_.music_volume, music_enabled_));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estableix el volum master (multiplicador aplicat a sound + music). Re-aplica
|
||||||
|
// els canals perquè el canvi tingui efecte immediat sense esperar al següent
|
||||||
|
// setSoundVolume/setMusicVolume explícit.
|
||||||
|
void Audio::setMasterVolume(float master_volume) {
|
||||||
|
config_.volume = std::clamp(master_volume, MIN_VOLUME, MAX_VOLUME);
|
||||||
|
setSoundVolume(config_.sound_volume);
|
||||||
|
setMusicVolume(config_.music_volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aplica una nueva configuración (substitueix la config cachejada i reaplica enables/volums)
|
// Aplica una nueva configuración (substitueix la config cachejada i reaplica enables/volums)
|
||||||
|
|||||||
@@ -101,6 +101,14 @@ class Audio {
|
|||||||
// --- Control de volum (API interna: float 0.0..1.0) ---
|
// --- Control de volum (API interna: float 0.0..1.0) ---
|
||||||
void setSoundVolume(float volume, Group group = Group::ALL); // Ajusta el volum dels efectes
|
void setSoundVolume(float volume, Group group = Group::ALL); // Ajusta el volum dels efectes
|
||||||
void setMusicVolume(float volume); // Ajusta el volum de la música
|
void setMusicVolume(float volume); // Ajusta el volum de la música
|
||||||
|
void setMasterVolume(float volume); // Ajusta el master (re-aplica sound + music)
|
||||||
|
|
||||||
|
// Getters dels volums actuals (lectura de la config_ cachejada). Reflexen
|
||||||
|
// el valor que l'usuari ha triat l'última vegada, independent del gating
|
||||||
|
// d'enabled/channel.
|
||||||
|
[[nodiscard]] auto getMasterVolume() const -> float { return config_.volume; }
|
||||||
|
[[nodiscard]] auto getSoundVolume() const -> float { return config_.sound_volume; }
|
||||||
|
[[nodiscard]] auto getMusicVolume() const -> float { return config_.music_volume; }
|
||||||
|
|
||||||
// --- Helpers de conversió para la capa de presentació ---
|
// --- Helpers de conversió para la capa de presentació ---
|
||||||
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
|
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace Config {
|
namespace Config {
|
||||||
@@ -65,16 +64,7 @@ namespace Config {
|
|||||||
KeyboardBindings keyboard_controls{}; // Defaults globals per Input
|
KeyboardBindings keyboard_controls{}; // Defaults globals per Input
|
||||||
GamepadBindings gamepad_controls{};
|
GamepadBindings gamepad_controls{};
|
||||||
bool console{false};
|
bool console{false};
|
||||||
};
|
std::string locale{"ca"}; // "ca" | "en" — fixat a l'arrencada, sense hot-swap
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -94,8 +94,8 @@ 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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -17,13 +17,18 @@ namespace Defaults::Game {
|
|||||||
// 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)
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ namespace Defaults::Palette {
|
|||||||
// 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
|
||||||
|
|||||||
@@ -18,13 +18,21 @@ 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)
|
||||||
@@ -40,8 +48,8 @@ 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).
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// service_menu.hpp - Constants del menu de servei (F12)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
namespace Defaults::ServiceMenu {
|
||||||
|
|
||||||
|
// ---- Mides en coordenades logiques del joc (1280×720) ----
|
||||||
|
// BOX_WIDTH_MIN es el minim: si el titol o algun item no hi caben, el
|
||||||
|
// marc s'expandeix dinamicament amb animacio (cf. WIDTH_RATE).
|
||||||
|
constexpr int BOX_WIDTH_MIN = 460;
|
||||||
|
constexpr int GAP_Y = 22;
|
||||||
|
constexpr int TITLE_HEIGHT = 36; // scale 0.85 ≈ 34 px de text
|
||||||
|
constexpr int SUBTITLE_HEIGHT = 18; // scale 0.4 ≈ 16 px de text
|
||||||
|
constexpr int SEPARATOR_HEIGHT = 1;
|
||||||
|
constexpr int ITEM_HEIGHT = 38; // scale 0.55 ≈ 22 px de text + padding per al highlight
|
||||||
|
constexpr int ITEM_GAP_Y = 6;
|
||||||
|
|
||||||
|
// Brackets als 4 cantons (substitueixen la vora completa: estètica sci-fi).
|
||||||
|
constexpr int CORNER_ARM_H = 48;
|
||||||
|
constexpr int CORNER_ARM_V = 28;
|
||||||
|
constexpr int CORNER_THICKNESS = 2;
|
||||||
|
|
||||||
|
// ---- Animacio open/close (mateixos parametres que aee_arcade) ----
|
||||||
|
constexpr float OPEN_SPEED = 8.0F; // ~125 ms a obrir
|
||||||
|
constexpr float CLOSE_SPEED = 10.0F; // ~100 ms a tancar
|
||||||
|
constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada de la caixa
|
||||||
|
constexpr float WIDTH_RATE = 12.0F; // smoothing per a canvis d'ample entre pagines
|
||||||
|
|
||||||
|
// ---- Animacio del highlight (rectangle del cursor) ----
|
||||||
|
// Rate=18 dona settling ~0.17 s al 95% (ease-out exponencial).
|
||||||
|
constexpr float HIGHLIGHT_RATE = 18.0F;
|
||||||
|
constexpr int HIGHLIGHT_TICK_LEN = 10; // longitud dels ticks a cada cantonada
|
||||||
|
constexpr int HIGHLIGHT_THICKNESS = 1;
|
||||||
|
constexpr int HIGHLIGHT_PAD_X = 18; // padding lateral del rect respecte al text
|
||||||
|
constexpr int HIGHLIGHT_PAD_Y = 4; // padding vertical
|
||||||
|
constexpr int TEXT_INSET_X = 16; // marge intern del text dins del highlight (label esq / valor dre)
|
||||||
|
constexpr int MIN_LABEL_VALUE_GAP = 30; // mínim gap entre label i valor (per al càlcul d'ample dinàmic)
|
||||||
|
|
||||||
|
// ---- Colors RGBA ----
|
||||||
|
constexpr SDL_Color BG_COLOR{.r = 0, .g = 12, .b = 24, .a = 215};
|
||||||
|
constexpr SDL_Color CORNER_COLOR{.r = 120, .g = 220, .b = 255, .a = 255}; // cian neon
|
||||||
|
constexpr SDL_Color TITLE_COLOR{.r = 200, .g = 240, .b = 255, .a = 255};
|
||||||
|
constexpr SDL_Color SUBTITLE_COLOR{.r = 110, .g = 170, .b = 210, .a = 220}; // cian apagat
|
||||||
|
constexpr SDL_Color SEPARATOR_COLOR{.r = 60, .g = 120, .b = 180, .a = 180};
|
||||||
|
constexpr SDL_Color LABEL_COLOR{.r = 170, .g = 210, .b = 240, .a = 255};
|
||||||
|
constexpr SDL_Color CURSOR_COLOR{.r = 255, .g = 230, .b = 120, .a = 255}; // groc per al text sel·leccionat
|
||||||
|
constexpr SDL_Color HIGHLIGHT_OUTLINE{.r = 255, .g = 230, .b = 120, .a = 255}; // mateix groc, opac
|
||||||
|
constexpr SDL_Color HIGHLIGHT_FILL{.r = 255, .g = 230, .b = 120, .a = 36}; // wash translucid
|
||||||
|
|
||||||
|
// ---- Tipografia (VectorText). Scale 1.0 = caracter 20×40 px ----
|
||||||
|
constexpr float TITLE_SCALE = 0.85F; // mateixa escala que el HUD del scoreboard
|
||||||
|
constexpr float SUBTITLE_SCALE = 0.40F; // sota el titol, info decorativa (versio/hash)
|
||||||
|
constexpr float ITEM_SCALE = 0.55F; // mateixa escala que les notificacions
|
||||||
|
constexpr float TEXT_SPACING = 2.0F;
|
||||||
|
|
||||||
|
// ---- Sons UI (relatius a data/sounds/), portats d'aee_arcade ----
|
||||||
|
constexpr const char* SELECT_SOUND = "ui/menu_select.wav";
|
||||||
|
constexpr const char* ACCEPT_SOUND = "ui/menu_accept.wav";
|
||||||
|
|
||||||
|
} // namespace Defaults::ServiceMenu
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
+207
-168
@@ -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;
|
|
||||||
const float ORBIT_DEC = Defaults::Playfield::ORBIT_DECAY_PER_S * delta_time;
|
|
||||||
for (auto& line : lines_) {
|
|
||||||
line.orbit_phase += ORBIT_DELTA_PHASE;
|
|
||||||
line.orbit_amplitude = std::max(0.0F, line.orbit_amplitude - ORBIT_DEC);
|
|
||||||
|
|
||||||
// Avança els pulses; els desactiva quan acaben de vida.
|
|
||||||
for (auto& pulse : line.pulses) {
|
|
||||||
if (!pulse.active) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
pulse.age_s += delta_time;
|
ripple.age_s += delta_time;
|
||||||
if (pulse.age_s >= Defaults::Playfield::PULSE_LIFETIME_S) {
|
if (ripple.age_s >= ripple.lifetime_s) {
|
||||||
pulse.active = false;
|
ripple.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);
|
}
|
||||||
|
if (oldest == nullptr || ripple.age_s > oldest->age_s) {
|
||||||
|
oldest = &ripple;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return oldest; // pool ple: substituïm la més vella
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playfield::spawnBig(Vec2 pos) {
|
||||||
|
Ripple* r = findFreeRipple();
|
||||||
|
if (r == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
r->center = pos;
|
||||||
// Cap slot lliure: substituïm el més vell.
|
r->age_s = 0.0F;
|
||||||
Pulse* oldest = line.pulses.data();
|
r->lifetime_s = Defaults::Playfield::Ripple::BIG_LIFETIME_S;
|
||||||
for (auto& pulse : line.pulses) {
|
r->speed_px_s = Defaults::Playfield::Ripple::BIG_SPEED_PX_S;
|
||||||
if (pulse.age_s > oldest->age_s) {
|
r->amplitude_px = Defaults::Playfield::Ripple::BIG_AMPLITUDE_PX;
|
||||||
oldest = &pulse;
|
r->thickness_px = Defaults::Playfield::Ripple::BIG_THICKNESS_PX;
|
||||||
}
|
r->active = true;
|
||||||
}
|
|
||||||
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::spawnSmall(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::SMALL_LIFETIME_S;
|
||||||
const float DIST = line.is_vertical
|
r->speed_px_s = Defaults::Playfield::Ripple::SMALL_SPEED_PX_S;
|
||||||
? std::abs(pos.x - line.start.x)
|
r->amplitude_px = Defaults::Playfield::Ripple::SMALL_AMPLITUDE_PX;
|
||||||
: std::abs(pos.y - line.start.y);
|
r->thickness_px = Defaults::Playfield::Ripple::SMALL_THICKNESS_PX;
|
||||||
if (DIST < MAX_DIST) {
|
r->active = true;
|
||||||
line.orbit_amplitude = Defaults::Playfield::ORBIT_AMPLITUDE_MAX_PX;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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_) {
|
||||||
|
drawLine(line, active.data(), n_active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Playfield::drawLine(const Line& line, const Ripple* const* active, int n_active) const {
|
||||||
const float RAW_P = computeLineProgress(line);
|
const float RAW_P = computeLineProgress(line);
|
||||||
if (RAW_P <= 0.0F) {
|
if (RAW_P <= 0.0F) {
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
const float P = easeOutCubic(RAW_P);
|
const float P = easeOutCubic(RAW_P);
|
||||||
|
|
||||||
// Desplaçament perpendicular per orbit (verticals → x, horitzontals → y).
|
const float START_X = line.start.x;
|
||||||
const float ORBIT_OFFSET = line.orbit_amplitude * std::sin(line.orbit_phase);
|
const float START_Y = line.start.y;
|
||||||
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 DX = line.end.x - line.start.x;
|
||||||
const float DY = line.end.y - line.start.y;
|
const float DY = line.end.y - line.start.y;
|
||||||
const float CURRENT_X = START_X + (DX * P);
|
const float END_X = START_X + (DX * P);
|
||||||
const float CURRENT_Y = START_Y + (DY * P);
|
const float END_Y = START_Y + (DY * P);
|
||||||
|
|
||||||
// Tram base (brillo de la línia).
|
// 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;
|
||||||
|
}
|
||||||
|
hits[n_hits++] = &r;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n_hits == 0) {
|
||||||
|
// Camí ràpid: una sola crida com abans.
|
||||||
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;
|
|
||||||
const float INTENSITY = std::max(
|
|
||||||
0.0F,
|
|
||||||
1.0F - (pulse.age_s / Defaults::Playfield::PULSE_LIFETIME_S));
|
|
||||||
const float T1 = std::clamp(pulse.center_t - HALF_WIDTH_T, 0.0F, 1.0F);
|
|
||||||
const float T2 = std::clamp(pulse.center_t + HALF_WIDTH_T, 0.0F, 1.0F);
|
|
||||||
if (T2 <= T1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const float P1_X = START_X + (DX * T1);
|
|
||||||
const float P1_Y = START_Y + (DY * T1);
|
|
||||||
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(
|
Rendering::linea(
|
||||||
renderer_,
|
renderer_,
|
||||||
static_cast<int>(P1_X),
|
static_cast<int>(prev_x),
|
||||||
static_cast<int>(P1_Y),
|
static_cast<int>(prev_y),
|
||||||
static_cast<int>(P2_X),
|
static_cast<int>(NX),
|
||||||
static_cast<int>(P2_Y),
|
static_cast<int>(NY),
|
||||||
1.0F,
|
line.brightness,
|
||||||
0.0F,
|
0.0F,
|
||||||
SEG_COLOR);
|
Defaults::Playfield::GRID_COLOR);
|
||||||
}
|
prev_x = NX;
|
||||||
|
prev_y = NY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#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>
|
||||||
@@ -85,9 +86,23 @@ auto Shape::parseFile(const std::string& contingut) -> bool {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bounding_radius_ = computeBoundingRadius(primitives_, center_);
|
||||||
return true;
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::sqrt(max_dist_sq);
|
||||||
|
}
|
||||||
|
|
||||||
// Helper: trim whitespace
|
// Helper: trim whitespace
|
||||||
auto Shape::trim(const std::string& str) -> std::string {
|
auto Shape::trim(const std::string& str) -> std::string {
|
||||||
const char* whitespace = " \t\n\r";
|
const char* whitespace = " \t\n\r";
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ 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ó
|
||||||
@@ -53,6 +56,7 @@ class Shape {
|
|||||||
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.
|
||||||
|
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ó)
|
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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
// Helper: número aleatori en [0, 1) usant rand()/RAND_MAX (mateixa convenció
|
||||||
const SDL_FRect& area,
|
// que la resta del joc — veure starfield.cpp).
|
||||||
int densitat)
|
auto randFloat01() -> float {
|
||||||
: shape_estrella_(ShapeLoader::load("star.shp")),
|
return static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
|
||||||
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
|
auto randRange(float lo, float hi) -> float {
|
||||||
// Capa 0: Fons llunyà (lenta, pequeña)
|
return lo + (randFloat01() * (hi - lo));
|
||||||
capes_.push_back({20.0F, 0.3F, 0.8F, densitat / 3});
|
|
||||||
|
|
||||||
// Capa 1: Profunditat mitjana
|
|
||||||
capes_.push_back({40.0F, 0.5F, 1.2F, densitat / 3});
|
|
||||||
|
|
||||||
// Capa 2: Primer pla (ràpida, grande)
|
|
||||||
capes_.push_back({80.0F, 0.8F, 2.0F, densitat / 3});
|
|
||||||
|
|
||||||
// Calcular radi màxim (distancia del centro al racó més llunyà)
|
|
||||||
float dx = std::max(punt_fuga_.x, area_.w - punt_fuga_.x);
|
|
||||||
float dy = std::max(punt_fuga_.y, area_.h - punt_fuga_.y);
|
|
||||||
radi_max_ = std::sqrt((dx * dx) + (dy * dy));
|
|
||||||
|
|
||||||
// Inicialitzar estrelles con posicions distribuïdes (pre-omplir pantalla)
|
|
||||||
for (int capa_idx = 0; capa_idx < 3; capa_idx++) {
|
|
||||||
int num = capes_[capa_idx].num_estrelles;
|
|
||||||
for (int i = 0; i < num; i++) {
|
|
||||||
Estrella estrella;
|
|
||||||
estrella.capa = capa_idx;
|
|
||||||
|
|
||||||
// Angle aleatori
|
|
||||||
estrella.angle = (static_cast<float>(rand()) / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI;
|
|
||||||
|
|
||||||
// Distancia aleatòria (0.0 a 1.0) per omplir toda la pantalla
|
|
||||||
estrella.distancia_centre = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
|
|
||||||
|
|
||||||
// Calcular posición desde la distancia
|
|
||||||
float radi = estrella.distancia_centre * radi_max_;
|
|
||||||
estrella.position.x = punt_fuga_.x + (radi * std::cos(estrella.angle));
|
|
||||||
estrella.position.y = punt_fuga_.y + (radi * std::sin(estrella.angle));
|
|
||||||
|
|
||||||
estrelles_.push_back(estrella);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inicialitzar una estrella (nueva o regenerada)
|
} // namespace
|
||||||
void Starfield::initStar(Estrella& estrella) const {
|
|
||||||
// Angle aleatori des del point de fuga hacia fuera
|
|
||||||
estrella.angle = (static_cast<float>(rand()) / static_cast<float>(RAND_MAX)) * 2.0F * Defaults::Math::PI;
|
|
||||||
|
|
||||||
// Distancia inicial pequeña (5% del radi màxim) - neix prop del centro
|
Starfield::Starfield(Rendering::Renderer* renderer, const Camera3D* camera, int density)
|
||||||
estrella.distancia_centre = 0.05F;
|
: renderer_(renderer),
|
||||||
|
camera_(camera),
|
||||||
// Posición inicial: mucho prop del point de fuga
|
octahedron_(makeOctahedron()) {
|
||||||
float radi = estrella.distancia_centre * radi_max_;
|
stars_.resize(static_cast<std::size_t>(std::max(0, density)));
|
||||||
estrella.position.x = punt_fuga_.x + (radi * std::cos(estrella.angle));
|
for (auto& star : stars_) {
|
||||||
estrella.position.y = punt_fuga_.y + (radi * std::sin(estrella.angle));
|
// Pre-omplir amb estrelles distribuïdes per tot el rang Z, no només al far.
|
||||||
|
initStar(star, /*spawn_at_far=*/false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verificar si una estrella está fuera de l'àrea
|
void Starfield::initStar(Star& star, bool spawn_at_far) {
|
||||||
auto Starfield::isOutsideArea(const Estrella& estrella) const -> bool {
|
star.position.x = randRange(-HALF_SPAWN_X, HALF_SPAWN_X);
|
||||||
return (estrella.position.x < area_.x ||
|
star.position.y = randRange(-HALF_SPAWN_Y, HALF_SPAWN_Y);
|
||||||
estrella.position.x > area_.x + area_.w ||
|
star.position.z = spawn_at_far
|
||||||
estrella.position.y < area_.y ||
|
? Z_FAR_SPAWN
|
||||||
estrella.position.y > area_.y + area_.h);
|
: 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calcular scale dinàmica segons distancia del centro
|
auto Starfield::computeBrightness(const Star& star) const -> float {
|
||||||
auto Starfield::computeScale(const Estrella& estrella) const -> float {
|
// Lerp segons distància Z normalitzada [Z_NEAR_RESPAWN .. Z_FAR_SPAWN].
|
||||||
const CapaConfig& capa = capes_[estrella.capa];
|
const float SPAN = Z_FAR_SPAWN - Z_NEAR_RESPAWN;
|
||||||
|
const float T_RAW = (star.position.z - Z_NEAR_RESPAWN) / SPAN;
|
||||||
// Interpolació lineal basada en distancia del centro
|
const float T = std::clamp(T_RAW, 0.0F, 1.0F);
|
||||||
// distancia_centre: 0.0 (centro) → 1.0 (vora)
|
const float BRIGHTNESS = BRIGHTNESS_NEAR + (T * (BRIGHTNESS_FAR - BRIGHTNESS_NEAR));
|
||||||
return capa.escala_min +
|
return std::clamp(BRIGHTNESS * brightness_mult_, 0.0F, 1.0F);
|
||||||
((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) {
|
void Starfield::update(float delta_time) {
|
||||||
for (auto& estrella : estrelles_) {
|
for (auto& star : stars_) {
|
||||||
// Obtenir configuración de la capa
|
star.position.z += star.velocity_z * delta_time;
|
||||||
const CapaConfig& capa = capes_[estrella.capa];
|
star.rot_phase_y += star.rot_speed_y * delta_time;
|
||||||
|
star.rot_phase_x += star.rot_speed_x * delta_time;
|
||||||
|
|
||||||
// Moure hacia fuera des del centro
|
if (star.position.z < Z_NEAR_RESPAWN) {
|
||||||
float velocity = capa.velocitat_base;
|
initStar(star, /*spawn_at_far=*/true);
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dibuixar todas las estrelles
|
|
||||||
void Starfield::draw() {
|
|
||||||
if (!shape_estrella_->isValid()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Ordena de més lluny a més a prop perquè el blending additiu acumule
|
||||||
for (const auto& estrella : estrelles_) {
|
// brightness sense que els elements de davant queden tapats pels de
|
||||||
// Calcular scale i brightness dinàmicament
|
// darrere. Còpia d'índexs per no modificar l'ordre intern de stars_.
|
||||||
float scale = computeScale(estrella);
|
std::vector<std::size_t> order(stars_.size());
|
||||||
float brightness = computeBrightness(estrella);
|
for (std::size_t i = 0; i < order.size(); ++i) {
|
||||||
|
order[i] = i;
|
||||||
// Renderizar estrella sin rotación
|
|
||||||
Rendering::renderShape(
|
|
||||||
renderer_,
|
|
||||||
shape_estrella_,
|
|
||||||
estrella.position,
|
|
||||||
0.0F, // angle (las estrelles no giren)
|
|
||||||
scale, // scale dinàmica
|
|
||||||
1.0F, // progress (siempre visible)
|
|
||||||
brightness // brightness dinàmica
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
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), color_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Starfield::setBrightness(float multiplier) {
|
||||||
|
brightness_mult_ = std::max(0.0F, multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Starfield::setColor(SDL_Color color) {
|
||||||
|
color_ = color;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Graphics
|
} // namespace Graphics
|
||||||
|
|||||||
@@ -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
|
|
||||||
struct CapaConfig {
|
|
||||||
float velocitat_base; // Velocidad base de esta capa (px/s)
|
|
||||||
float escala_min; // Escala mínima prop del centro
|
|
||||||
float escala_max; // Escala màxima al límit de pantalla
|
|
||||||
int num_estrelles; // Nombre de estrelles en esta capa
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clase Starfield - camp de estrelles animat con efecte de profunditat
|
|
||||||
class Starfield {
|
class Starfield {
|
||||||
public:
|
public:
|
||||||
// Constructor
|
Starfield(Rendering::Renderer* renderer, const Camera3D* camera, int density = 200);
|
||||||
// - 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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// locale.cpp - Implementació del sistema de locale
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "core/locale/locale.hpp"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <exception>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Recorre el node YAML i aplana jerarquies en claus "a.b.c". Suporta
|
||||||
|
// mappings (recursió) i seqüències de strings (desa "a.b.0", "a.b.1"...).
|
||||||
|
// Altres tipus (nombres, booleans solts) s'ignoren silenciosament.
|
||||||
|
void flatten(const fkyaml::node& node, const std::string& prefix, std::unordered_map<std::string, std::string>& out) {
|
||||||
|
if (node.is_mapping()) {
|
||||||
|
for (auto it = node.begin(); it != node.end(); ++it) {
|
||||||
|
const std::string KEY = prefix.empty()
|
||||||
|
? it.key().get_value<std::string>()
|
||||||
|
: prefix + "." + it.key().get_value<std::string>();
|
||||||
|
flatten(it.value(), KEY, out);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (node.is_sequence()) {
|
||||||
|
std::size_t index = 0;
|
||||||
|
for (const auto& item : node) {
|
||||||
|
const std::string KEY = prefix + "." + std::to_string(index);
|
||||||
|
flatten(item, KEY, out);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (node.is_string()) {
|
||||||
|
out[prefix] = node.get_value<std::string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto Locale::get() -> Locale& {
|
||||||
|
static Locale instance_;
|
||||||
|
return instance_;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Locale::load(const std::string& file_path) -> bool {
|
||||||
|
// Normalitza traient prefix "data/" com fa StageLoader: el pack de
|
||||||
|
// recursos indexa rutes relatives a `data/`.
|
||||||
|
std::string normalized = file_path;
|
||||||
|
if (normalized.starts_with("data/")) {
|
||||||
|
normalized = normalized.substr(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> bytes = Resource::Helper::loadFile(normalized);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
std::cerr << "[Locale] no s'ha pogut load " << normalized << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::string yaml_content(bytes.begin(), bytes.end());
|
||||||
|
std::stringstream stream(yaml_content);
|
||||||
|
fkyaml::node yaml = fkyaml::node::deserialize(stream);
|
||||||
|
strings_.clear();
|
||||||
|
flatten(yaml, "", strings_);
|
||||||
|
std::cout << "[Locale] " << strings_.size() << " traduccions des de " << normalized << '\n';
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[Locale] error parsejant " << normalized << ": " << e.what() << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Locale::switchTo(const std::string& lang) -> bool {
|
||||||
|
return load("locale/" + lang + ".yaml");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Locale::text(const std::string& key) const -> std::string {
|
||||||
|
auto it = strings_.find(key);
|
||||||
|
if (it != strings_.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
std::cerr << "[Locale] clau no trobada: " << key << '\n';
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Locale::count(const std::string& prefix) const -> std::size_t {
|
||||||
|
std::size_t n = 0;
|
||||||
|
while (strings_.contains(prefix + "." + std::to_string(n))) {
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto localeSubstitute(std::string tpl, std::string_view placeholder, std::string_view value) -> std::string {
|
||||||
|
auto pos = tpl.find(placeholder);
|
||||||
|
if (pos != std::string::npos) {
|
||||||
|
tpl.replace(pos, placeholder.size(), value);
|
||||||
|
}
|
||||||
|
return tpl;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// locale.hpp - Sistema d'internacionalització (i18n) basat en YAML
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Locale amb claus en notació de punts ("notification.fullscreen_on"). El YAML
|
||||||
|
// pot ser jerarquitzat i s'aplana en càrrega, així el consumidor només veu
|
||||||
|
// claus planes. Suporta seqüències de strings (es desen com prefix.0,
|
||||||
|
// prefix.1, ...). No hi ha hot-swap d'idioma: es fixa a l'arrencada des de
|
||||||
|
// `config.yaml` (camp `locale`) i només es recarrega reiniciant el joc.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
class Locale {
|
||||||
|
public:
|
||||||
|
static auto get() -> Locale&;
|
||||||
|
|
||||||
|
Locale(const Locale&) = delete;
|
||||||
|
Locale(Locale&&) = delete;
|
||||||
|
auto operator=(const Locale&) -> Locale& = delete;
|
||||||
|
auto operator=(Locale&&) -> Locale& = delete;
|
||||||
|
|
||||||
|
// Llig el fitxer YAML i emplena el mapping intern. Si hi ha un error de
|
||||||
|
// parse o el fitxer no existeix, deixa el mapping com estava i ho
|
||||||
|
// notifica per stderr. Retorna true només si la càrrega ha tingut èxit.
|
||||||
|
auto load(const std::string& file_path) -> bool;
|
||||||
|
|
||||||
|
// Canvi d'idioma en runtime. Recarrega `locale/<lang>.yaml`. Retorna true
|
||||||
|
// si la càrrega ha tingut èxit. Els lookups posteriors (tots els draw*
|
||||||
|
// criden Locale::text() cada frame) ja veuen el nou idioma. Els missatges
|
||||||
|
// ja capturats (toast actiu, banner de stage start ja triat) sobreviuen
|
||||||
|
// fins al seu cicle natural.
|
||||||
|
auto switchTo(const std::string& lang) -> bool;
|
||||||
|
|
||||||
|
// Retorna la traducció; si la clau no existeix, retorna la pròpia clau
|
||||||
|
// com a fallback visible (així una clau mal escrita es detecta sense
|
||||||
|
// trencar el render).
|
||||||
|
[[nodiscard]] auto text(const std::string& key) const -> std::string;
|
||||||
|
|
||||||
|
// Compta quantes claus consecutives existeixen amb el prefix donat
|
||||||
|
// (prefix.0, prefix.1, ...). Útil per pools indexats com stage.start.N.
|
||||||
|
[[nodiscard]] auto count(const std::string& prefix) const -> std::size_t;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Locale() = default;
|
||||||
|
~Locale() = default;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::string> strings_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Substitució simple d'un placeholder dins una plantilla (p.ex. "{n}" → "3").
|
||||||
|
// S'usa per interpolar valors runtime en strings traduïdes.
|
||||||
|
[[nodiscard]] auto localeSubstitute(std::string tpl, std::string_view placeholder, std::string_view value) -> std::string;
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "core/entities/entity.hpp"
|
#include "core/entities/entity.hpp"
|
||||||
#include "core/types.hpp"
|
#include "core/types.hpp"
|
||||||
|
|
||||||
@@ -15,7 +17,7 @@ inline auto checkCollision(const Entities::Entity& a, const Entities::Entity& b,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calcular radi combinat (con amplificador per hitbox generós)
|
// Calcular radius combinat (con amplificador per hitbox generós)
|
||||||
float suma_radis = (a.getCollisionRadius() + b.getCollisionRadius()) * amplifier;
|
float suma_radis = (a.getCollisionRadius() + b.getCollisionRadius()) * amplifier;
|
||||||
float suma_radis_sq = suma_radis * suma_radis;
|
float suma_radis_sq = suma_radis * suma_radis;
|
||||||
|
|
||||||
@@ -29,4 +31,34 @@ inline auto checkCollision(const Entities::Entity& a, const Entities::Entity& b,
|
|||||||
return dist_sq <= suma_radis_sq;
|
return dist_sq <= suma_radis_sq;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Swept collision: una entitat mòbil (radius r_a) s'ha desplaçat de p0 a p1 aquest
|
||||||
|
// frame. Comprova si el segment expandit pel radius conjunt (r_a + radius de b, amb
|
||||||
|
// amplificador) toca el cercle de l'entity b. Equival al check discrete quan
|
||||||
|
// p0 == p1 (sense moviment). Evita tunneling a velocitats altes.
|
||||||
|
inline auto checkCollisionSwept(const Vec2& p0, const Vec2& p1, float r_a, const Entities::Entity& b, float amplifier = 1.0F) -> bool {
|
||||||
|
if (!b.isCollidable()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const float SUM_R = (r_a + b.getCollisionRadius()) * amplifier;
|
||||||
|
const float SUM_R_SQ = SUM_R * SUM_R;
|
||||||
|
const Vec2& center_b = b.getCenter();
|
||||||
|
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
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <SDL3/SDL_gpu.h>
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -390,6 +391,10 @@ namespace Rendering::GPU {
|
|||||||
color_target.store_op = SDL_GPU_STOREOP_STORE;
|
color_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||||
render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr);
|
render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr);
|
||||||
|
|
||||||
|
// L'scissor és per render pass: en reobrir cal restaurar-lo des del top
|
||||||
|
// de la pila si pushClip/popClip s'han usat mid-frame.
|
||||||
|
applyCurrentScissor();
|
||||||
|
|
||||||
SDL_BindGPUGraphicsPipeline(render_pass_, line_pipeline_.get());
|
SDL_BindGPUGraphicsPipeline(render_pass_, line_pipeline_.get());
|
||||||
|
|
||||||
// UBO de líneas usa el tamaño lógico (también del offscreen).
|
// UBO de líneas usa el tamaño lógico (también del offscreen).
|
||||||
@@ -415,6 +420,11 @@ namespace Rendering::GPU {
|
|||||||
SDL_ReleaseGPUBuffer(dev, vbo);
|
SDL_ReleaseGPUBuffer(dev, vbo);
|
||||||
SDL_ReleaseGPUBuffer(dev, ibo);
|
SDL_ReleaseGPUBuffer(dev, ibo);
|
||||||
SDL_ReleaseGPUTransferBuffer(dev, tbo);
|
SDL_ReleaseGPUTransferBuffer(dev, tbo);
|
||||||
|
|
||||||
|
// Buidem el batch perquè pushClip/popClip puguin emetre seccions
|
||||||
|
// separades dins el mateix frame sense re-enviar geometria.
|
||||||
|
vertices_.clear();
|
||||||
|
indices_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GpuFrameRenderer::bloomPass() {
|
void GpuFrameRenderer::bloomPass() {
|
||||||
@@ -603,6 +613,51 @@ namespace Rendering::GPU {
|
|||||||
SDL_DrawGPUPrimitives(render_pass_, 3, 1, 0, 0);
|
SDL_DrawGPUPrimitives(render_pass_, 3, 1, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GpuFrameRenderer::pushClip(int logical_x, int logical_y, int logical_w, int logical_h) {
|
||||||
|
// Convertim coordenades lògiques (espai del joc, 1280×720) a píxels
|
||||||
|
// físics del offscreen (render_w_ × render_h_). Si l'usuari hi treballa
|
||||||
|
// amb upscale (p.ex. 1920×1080), l'scissor escala proporcionalment.
|
||||||
|
const float SX = render_w_ / logical_w_;
|
||||||
|
const float SY = render_h_ / logical_h_;
|
||||||
|
SDL_Rect rect{
|
||||||
|
.x = static_cast<int>(static_cast<float>(logical_x) * SX),
|
||||||
|
.y = static_cast<int>(static_cast<float>(logical_y) * SY),
|
||||||
|
.w = std::max(0, static_cast<int>(static_cast<float>(logical_w) * SX)),
|
||||||
|
.h = std::max(0, static_cast<int>(static_cast<float>(logical_h) * SY)),
|
||||||
|
};
|
||||||
|
// Emetem tot el batch acumulat *abans* d'activar l'scissor perquè quedi
|
||||||
|
// dibuixat sense retallar.
|
||||||
|
flushBatch();
|
||||||
|
clip_stack_.push_back(rect);
|
||||||
|
applyCurrentScissor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuFrameRenderer::popClip() {
|
||||||
|
// Emetem el batch que s'ha acumulat *dins* del clip actiu.
|
||||||
|
flushBatch();
|
||||||
|
if (!clip_stack_.empty()) {
|
||||||
|
clip_stack_.pop_back();
|
||||||
|
}
|
||||||
|
applyCurrentScissor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuFrameRenderer::applyCurrentScissor() {
|
||||||
|
if (render_pass_ == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SDL_Rect rect{};
|
||||||
|
if (clip_stack_.empty()) {
|
||||||
|
// Sense clips: scissor cobreix tot el offscreen.
|
||||||
|
rect.x = 0;
|
||||||
|
rect.y = 0;
|
||||||
|
rect.w = static_cast<int>(render_w_);
|
||||||
|
rect.h = static_cast<int>(render_h_);
|
||||||
|
} else {
|
||||||
|
rect = clip_stack_.back();
|
||||||
|
}
|
||||||
|
SDL_SetGPUScissor(render_pass_, &rect);
|
||||||
|
}
|
||||||
|
|
||||||
void GpuFrameRenderer::endFrame() {
|
void GpuFrameRenderer::endFrame() {
|
||||||
if (cmd_buffer_ == nullptr) {
|
if (cmd_buffer_ == nullptr) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -94,6 +94,15 @@ namespace Rendering::GPU {
|
|||||||
// d'UI (notificacions, panels).
|
// d'UI (notificacions, panels).
|
||||||
void pushRect(float x, float y, float w, float h, float r, float g, float b, float a);
|
void pushRect(float x, float y, float w, float h, float r, float g, float b, float a);
|
||||||
|
|
||||||
|
// Clipping rectangular per a UI (scissor a SDL_GPU). pushClip/popClip
|
||||||
|
// forcen un flush intermedi del batch i activen/restauren l'scissor del
|
||||||
|
// pase actiu. Coordenades en píxels lògics del joc (1280×720); es
|
||||||
|
// converteixen a píxels físics del offscreen automàticament. Stack
|
||||||
|
// d'scissors per a clips niats. Quan la pila queda buida, l'scissor
|
||||||
|
// torna a cobrir el target sencer.
|
||||||
|
void pushClip(int logical_x, int logical_y, int logical_w, int logical_h);
|
||||||
|
void popClip();
|
||||||
|
|
||||||
// endFrame: flush del batch de líneas → composite postpro → submit + presenta.
|
// endFrame: flush del batch de líneas → composite postpro → submit + presenta.
|
||||||
void endFrame();
|
void endFrame();
|
||||||
|
|
||||||
@@ -168,6 +177,10 @@ namespace Rendering::GPU {
|
|||||||
std::vector<LineVertex> vertices_;
|
std::vector<LineVertex> vertices_;
|
||||||
std::vector<uint16_t> indices_;
|
std::vector<uint16_t> indices_;
|
||||||
|
|
||||||
|
// Pila d'scissors actius en píxels físics del offscreen. Buida = sense
|
||||||
|
// clip (full target). Cada push/pop fa un flushBatch i reaplica scissor.
|
||||||
|
std::vector<SDL_Rect> clip_stack_;
|
||||||
|
|
||||||
// Estado del frame en curso.
|
// Estado del frame en curso.
|
||||||
SDL_GPUCommandBuffer* cmd_buffer_{nullptr};
|
SDL_GPUCommandBuffer* cmd_buffer_{nullptr};
|
||||||
SDL_GPUTexture* swapchain_texture_{nullptr};
|
SDL_GPUTexture* swapchain_texture_{nullptr};
|
||||||
@@ -190,6 +203,7 @@ namespace Rendering::GPU {
|
|||||||
void bloomPass(); // pre-composite: H + V passes sobre les bloom textures
|
void bloomPass(); // pre-composite: H + V passes sobre les bloom textures
|
||||||
void compositePass();
|
void compositePass();
|
||||||
void applyFinalViewport();
|
void applyFinalViewport();
|
||||||
|
void applyCurrentScissor(); // re-aplica el top de clip_stack_ al render_pass_
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Rendering::GPU
|
} // namespace Rendering::GPU
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "core/defaults/rendering.hpp"
|
#include "core/defaults/rendering.hpp"
|
||||||
#include "core/defaults/window.hpp"
|
#include "core/defaults/window.hpp"
|
||||||
#include "core/input/mouse.hpp"
|
#include "core/input/mouse.hpp"
|
||||||
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/rendering/coordinate_transform.hpp"
|
#include "core/rendering/coordinate_transform.hpp"
|
||||||
#include "core/system/notifier.hpp"
|
#include "core/system/notifier.hpp"
|
||||||
#include "project.h"
|
#include "project.h"
|
||||||
@@ -250,7 +251,10 @@ void SDLManager::increaseWindowSize() {
|
|||||||
float new_zoom = zoom_factor_ + Defaults::Window::ZOOM_INCREMENT;
|
float new_zoom = zoom_factor_ + Defaults::Window::ZOOM_INCREMENT;
|
||||||
applyZoom(new_zoom);
|
applyZoom(new_zoom);
|
||||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||||
notifier->notifyInfo(std::format("ZOOM: {:.1f}X", zoom_factor_));
|
notifier->notifyInfo(localeSubstitute(
|
||||||
|
Locale::get().text("notification.zoom"),
|
||||||
|
"{z}",
|
||||||
|
std::format("{:.1f}", zoom_factor_)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,7 +265,10 @@ void SDLManager::decreaseWindowSize() {
|
|||||||
float new_zoom = zoom_factor_ - Defaults::Window::ZOOM_INCREMENT;
|
float new_zoom = zoom_factor_ - Defaults::Window::ZOOM_INCREMENT;
|
||||||
applyZoom(new_zoom);
|
applyZoom(new_zoom);
|
||||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||||
notifier->notifyInfo(std::format("ZOOM: {:.1f}X", zoom_factor_));
|
notifier->notifyInfo(localeSubstitute(
|
||||||
|
Locale::get().text("notification.zoom"),
|
||||||
|
"{z}",
|
||||||
|
std::format("{:.1f}", zoom_factor_)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +317,7 @@ void SDLManager::toggleFullscreen() {
|
|||||||
Mouse::setForceHidden(is_fullscreen_);
|
Mouse::setForceHidden(is_fullscreen_);
|
||||||
|
|
||||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||||
notifier->notifyInfo(is_fullscreen_ ? "PANTALLA COMPLETA" : "MODE FINESTRA");
|
notifier->notifyInfo(Locale::get().text(is_fullscreen_ ? "notification.fullscreen_on" : "notification.fullscreen_off"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,7 +371,7 @@ void SDLManager::toggleVSync() {
|
|||||||
on_persist_();
|
on_persist_();
|
||||||
}
|
}
|
||||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||||
notifier->notifyInfo(cfg_->rendering.vsync != 0 ? "VSYNC ACTIU" : "VSYNC INACTIU");
|
notifier->notifyInfo(Locale::get().text(cfg_->rendering.vsync != 0 ? "notification.vsync_on" : "notification.vsync_off"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,7 +381,27 @@ void SDLManager::toggleAntialias() {
|
|||||||
// No persistim: l'AA és toggleable runtime però el seu estat no es
|
// No persistim: l'AA és toggleable runtime però el seu estat no es
|
||||||
// guarda al YAML de moment (decisió volgudament conservadora).
|
// guarda al YAML de moment (decisió volgudament conservadora).
|
||||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||||
notifier->notifyInfo(cfg_->rendering.antialias != 0 ? "AA ACTIU" : "AA INACTIU");
|
notifier->notifyInfo(Locale::get().text(cfg_->rendering.antialias != 0 ? "notification.antialias_on" : "notification.antialias_off"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDLManager::setRenderResolution(int w, int h) {
|
||||||
|
if (!Defaults::Rendering::isValidRenderResolution(w, h)) {
|
||||||
|
std::cerr << "[SDLManager] Resolucio no valida (" << w << "x" << h
|
||||||
|
<< "), ignorant.\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (w == cfg_->rendering.render_width && h == cfg_->rendering.render_height) {
|
||||||
|
return; // ja era l'actual
|
||||||
|
}
|
||||||
|
if (!gpu_renderer_.resizeRenderTarget(static_cast<float>(w), static_cast<float>(h))) {
|
||||||
|
std::cerr << "[SDLManager] resizeRenderTarget ha fallat.\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cfg_->rendering.render_width = w;
|
||||||
|
cfg_->rendering.render_height = h;
|
||||||
|
if (on_persist_) {
|
||||||
|
on_persist_();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,6 +411,6 @@ void SDLManager::togglePostFx() {
|
|||||||
// No persistim: el toggle és per A/B testing visual, l'estat per defecte
|
// No persistim: el toggle és per A/B testing visual, l'estat per defecte
|
||||||
// del joc continua sent "postfx ON" segons defaults/YAML.
|
// del joc continua sent "postfx ON" segons defaults/YAML.
|
||||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||||
notifier->notifyInfo(NEW_STATE ? "POSTPROCESSAT ACTIU" : "POSTPROCESSAT INACTIU");
|
notifier->notifyInfo(Locale::get().text(NEW_STATE ? "notification.postfx_on" : "notification.postfx_off"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ class SDLManager {
|
|||||||
void toggleVSync(); // F4
|
void toggleVSync(); // F4
|
||||||
void toggleAntialias(); // F5
|
void toggleAntialias(); // F5
|
||||||
void togglePostFx(); // F6
|
void togglePostFx(); // F6
|
||||||
|
// Canvia la resolució del render target offscreen (recrea la textura).
|
||||||
|
// Cal cridar-lo fora d'un frame (event phase, no draw phase). Si el
|
||||||
|
// valor no es un preset valid o ja es l'actual, es no-op.
|
||||||
|
void setRenderResolution(int w, int h);
|
||||||
auto handleWindowEvent(const SDL_Event& event) -> bool; // Per a SDL_EVENT_WINDOW_RESIZED
|
auto handleWindowEvent(const SDL_Event& event) -> bool; // Per a SDL_EVENT_WINDOW_RESIZED
|
||||||
|
|
||||||
// Funciones principals (renderizado).
|
// Funciones principals (renderizado).
|
||||||
@@ -47,6 +51,8 @@ class SDLManager {
|
|||||||
// Getters
|
// Getters
|
||||||
auto getRenderer() -> Rendering::Renderer* { return &gpu_renderer_; }
|
auto getRenderer() -> Rendering::Renderer* { return &gpu_renderer_; }
|
||||||
[[nodiscard]] auto getScaleFactor() const -> float { return zoom_factor_; }
|
[[nodiscard]] auto getScaleFactor() const -> float { return zoom_factor_; }
|
||||||
|
[[nodiscard]] auto isFullscreen() const -> bool { return is_fullscreen_; }
|
||||||
|
[[nodiscard]] auto isPostFxEnabled() const -> bool { return gpu_renderer_.isPostFxEnabled(); }
|
||||||
|
|
||||||
// [NUEVO] Actualitzar context de renderizado (factor de scale global)
|
// [NUEVO] Actualitzar context de renderizado (factor de scale global)
|
||||||
void updateRenderingContext() const;
|
void updateRenderingContext() const;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
+155
-134
@@ -15,16 +15,18 @@
|
|||||||
#include "core/defaults/window.hpp"
|
#include "core/defaults/window.hpp"
|
||||||
#include "core/input/input.hpp"
|
#include "core/input/input.hpp"
|
||||||
#include "core/input/mouse.hpp"
|
#include "core/input/mouse.hpp"
|
||||||
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/rendering/sdl_manager.hpp"
|
#include "core/rendering/sdl_manager.hpp"
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "core/resources/resource_loader.hpp"
|
#include "core/resources/resource_loader.hpp"
|
||||||
#include "core/system/notifier.hpp"
|
#include "core/system/notifier.hpp"
|
||||||
|
#include "core/system/service_menu.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 +42,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 +96,14 @@ 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();
|
||||||
|
|
||||||
|
// Carregar locale segons la config (per defecte "ca"). Si la càrrega
|
||||||
|
// falla, Locale::text() retorna la clau crua i el joc segueix funcionant.
|
||||||
|
Locale::get().load(std::string("locale/") + cfg_->locale + ".yaml");
|
||||||
|
|
||||||
// Inicialitzar sistema de input
|
// Inicialitzar sistema de input
|
||||||
Input::init("data/gamecontrollerdb.txt");
|
Input::init("data/gamecontrollerdb.txt");
|
||||||
@@ -117,22 +121,75 @@ 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());
|
||||||
|
System::ServiceMenu::init(sdl_->getRenderer(), sdl_.get(), debug_overlay_.get());
|
||||||
|
|
||||||
|
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::ServiceMenu::destroy();
|
||||||
|
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 +202,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 +275,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 +290,97 @@ 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) {
|
||||||
|
SceneManager::actual = SceneType::EXIT;
|
||||||
|
return SDL_APP_SUCCESS;
|
||||||
|
}
|
||||||
|
SceneManager::actual = NEXT;
|
||||||
|
current_scene_ = buildScene(NEXT, *sdl_, *context_);
|
||||||
|
if (!current_scene_) {
|
||||||
|
SceneManager::actual = SceneType::EXIT;
|
||||||
|
return SDL_APP_SUCCESS;
|
||||||
|
}
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
while (!scene.isFinished()) {
|
auto Director::handleEvent(const SDL_Event& event) -> SDL_AppResult {
|
||||||
// Delta time real, capeado a 50ms para evitar grandes saltos.
|
// 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();
|
const Uint64 NOW = SDL_GetTicks();
|
||||||
float delta_time = static_cast<float>(NOW - last_time) / 1000.0F;
|
float delta_time = static_cast<float>(NOW - last_ticks_ms_) / 1000.0F;
|
||||||
last_time = NOW;
|
last_ticks_ms_ = NOW;
|
||||||
delta_time = std::min(delta_time, 0.05F);
|
delta_time = std::min(delta_time, 0.05F);
|
||||||
|
|
||||||
Mouse::updateCursorVisibility();
|
Mouse::updateCursorVisibility();
|
||||||
Input::get()->update();
|
Input::get()->update();
|
||||||
|
|
||||||
// Event loop: primero ventana, después globales, después F11
|
current_scene_->update(delta_time);
|
||||||
// (toggle del overlay), después escena.
|
debug_overlay_->update(delta_time);
|
||||||
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) {
|
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||||
notifier->update(delta_time);
|
notifier->update(delta_time);
|
||||||
}
|
}
|
||||||
|
if (auto* menu = System::ServiceMenu::get(); menu != nullptr) {
|
||||||
|
menu->update(delta_time);
|
||||||
|
}
|
||||||
Audio::update();
|
Audio::update();
|
||||||
|
|
||||||
// Si la swapchain no está disponible (ventana minimizada, etc.),
|
// Si la swapchain no està disponible (finestra minimitzada, etc.),
|
||||||
// saltarse draw+present ese frame: dibujar dejaría vértices
|
// saltar-se draw+present aquest frame.
|
||||||
// colgando en el batch interno sin nadie que los presente.
|
if (!sdl_->clear(0, 0, 0)) {
|
||||||
if (!sdl.clear(0, 0, 0)) {
|
return SDL_APP_CONTINUE;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
sdl.updateRenderingContext();
|
sdl_->updateRenderingContext();
|
||||||
scene.draw();
|
current_scene_->draw();
|
||||||
debug_overlay.draw(); // sempre per damunt de l'escena
|
debug_overlay_->draw(); // sempre per damunt de l'escena
|
||||||
if (const auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
if (const auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||||
notifier->draw(); // toast: per damunt de tot
|
notifier->draw(); // toast: per damunt de tot
|
||||||
}
|
}
|
||||||
sdl.present();
|
if (const auto* menu = System::ServiceMenu::get(); menu != nullptr) {
|
||||||
|
menu->draw(); // service menu: per damunt fins i tot dels toasts
|
||||||
}
|
}
|
||||||
|
sdl_->present();
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,40 +12,40 @@ enum class Mode : std::uint8_t {
|
|||||||
|
|
||||||
// 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)
|
||||||
|
|||||||
@@ -7,8 +7,11 @@
|
|||||||
|
|
||||||
#include "core/input/input.hpp"
|
#include "core/input/input.hpp"
|
||||||
#include "core/input/mouse.hpp"
|
#include "core/input/mouse.hpp"
|
||||||
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/rendering/sdl_manager.hpp"
|
#include "core/rendering/sdl_manager.hpp"
|
||||||
#include "core/system/notifier.hpp"
|
#include "core/system/notifier.hpp"
|
||||||
|
#include "core/system/service_menu.hpp"
|
||||||
|
#include "game/config_yaml.hpp"
|
||||||
#include "scene_context.hpp"
|
#include "scene_context.hpp"
|
||||||
|
|
||||||
// Using declarations per simplificar el codi
|
// Using declarations per simplificar el codi
|
||||||
@@ -17,6 +20,31 @@ using SceneType = SceneContext::SceneType;
|
|||||||
|
|
||||||
namespace GlobalEvents {
|
namespace GlobalEvents {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Reenvia el KEY_DOWN al menu de servei si esta obert i la tecla no
|
||||||
|
// es F1-F12 ni ESC (que sempre passen com a globals). Retorna true si
|
||||||
|
// el menu l'ha consumit.
|
||||||
|
auto forwardToServiceMenu(const SDL_Event& event) -> bool {
|
||||||
|
if (event.type != SDL_EVENT_KEY_DOWN) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto* menu = System::ServiceMenu::get();
|
||||||
|
if (menu == nullptr || !menu->isOpen()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const SDL_Scancode SC = event.key.scancode;
|
||||||
|
const bool PASSTHROUGH = (SC == SDL_SCANCODE_ESCAPE) ||
|
||||||
|
(SC >= SDL_SCANCODE_F1 && SC <= SDL_SCANCODE_F12);
|
||||||
|
if (PASSTHROUGH) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
menu->handleEvent(event);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
auto handle(const SDL_Event& event, SDLManager& sdl, SceneContext& context) -> bool {
|
auto handle(const SDL_Event& event, SDLManager& sdl, SceneContext& context) -> bool {
|
||||||
// 1. Permitir que Input procese el evento (para hotplug de gamepads)
|
// 1. Permitir que Input procese el evento (para hotplug de gamepads)
|
||||||
auto event_msg = Input::get()->handleEvent(event);
|
auto event_msg = Input::get()->handleEvent(event);
|
||||||
@@ -34,7 +62,15 @@ namespace GlobalEvents {
|
|||||||
// 3. Gestió del ratolí (auto-ocultar)
|
// 3. Gestió del ratolí (auto-ocultar)
|
||||||
Mouse::handleEvent(event);
|
Mouse::handleEvent(event);
|
||||||
|
|
||||||
// 4. Procesar acciones globales directamente desde eventos SDL
|
// 4. Service Menu (F12): consumeix tot KEY_DOWN excepte tecles de
|
||||||
|
// funció (F1-F12) i ESC, que continuen sent globals (zoom, fullscreen,
|
||||||
|
// vsync, AA, postfx, locale, exit prompt). Aixi el menu captura
|
||||||
|
// ENTER/BACKSPACE/UP/DOWN/LEFT/RIGHT i lletres mentre esta obert.
|
||||||
|
if (forwardToServiceMenu(event)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Procesar acciones globales directamente desde eventos SDL
|
||||||
// (NO usar Input::checkAction() para evitar desfase de timing)
|
// (NO usar Input::checkAction() para evitar desfase de timing)
|
||||||
if (event.type == SDL_EVENT_KEY_DOWN) {
|
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||||
switch (event.key.scancode) {
|
switch (event.key.scancode) {
|
||||||
@@ -62,6 +98,35 @@ namespace GlobalEvents {
|
|||||||
sdl.togglePostFx();
|
sdl.togglePostFx();
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case SDL_SCANCODE_F7: {
|
||||||
|
// Toggle d'idioma en runtime entre català i anglès. Els
|
||||||
|
// strings ja capturats (toast actiu, banner stage start)
|
||||||
|
// sobreviuen fins al seu cicle; la resta (HUD, pantalles,
|
||||||
|
// pròxims toasts) es refresquen al següent frame perquè
|
||||||
|
// criden Locale::text() cada draw.
|
||||||
|
const std::string NEW_LANG = (ConfigYaml::engine_config.locale == "ca") ? "en" : "ca";
|
||||||
|
if (Locale::get().switchTo(NEW_LANG)) {
|
||||||
|
ConfigYaml::engine_config.locale = NEW_LANG;
|
||||||
|
ConfigYaml::saveToFile();
|
||||||
|
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||||
|
notifier->notifyInfo(localeSubstitute(
|
||||||
|
Locale::get().text("notification.locale_switched"),
|
||||||
|
"{lang}",
|
||||||
|
Locale::get().text("language." + NEW_LANG)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SDL_SCANCODE_F12: {
|
||||||
|
// Toggle del menu de servei. Sempre passa com a global
|
||||||
|
// (alterna obert/tancat des de qualsevol escena).
|
||||||
|
if (auto* menu = System::ServiceMenu::get(); menu != nullptr) {
|
||||||
|
menu->toggle();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
case SDL_SCANCODE_ESCAPE: {
|
case SDL_SCANCODE_ESCAPE: {
|
||||||
// Doble pulsació per confirmar sortida: la primera ESC
|
// Doble pulsació per confirmar sortida: la primera ESC
|
||||||
// dispara un toast d'avís; només si aquest toast concret
|
// dispara un toast d'avís; només si aquest toast concret
|
||||||
@@ -71,7 +136,7 @@ namespace GlobalEvents {
|
|||||||
// sortida en lloc de tancar.
|
// sortida en lloc de tancar.
|
||||||
auto* notifier = System::Notifier::get();
|
auto* notifier = System::Notifier::get();
|
||||||
if (notifier != nullptr && !notifier->isExitPromptActive()) {
|
if (notifier != nullptr && !notifier->isExitPromptActive()) {
|
||||||
notifier->notifyExit("PREMEU ESC UN ALTRE COP PER EIXIR");
|
notifier->notifyExit(Locale::get().text("notification.press_again_exit"));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Notifier inexistent (degradació elegant) o segona ESC
|
// Notifier inexistent (degradació elegant) o segona ESC
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// relaunch.cpp - Implementacio del reinici en calent
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "core/system/relaunch.hpp"
|
||||||
|
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <process.h> // _execv
|
||||||
|
#else
|
||||||
|
#include <unistd.h> // execv
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Estat global (process-scope). Aquesta TU es la unica que gestiona el
|
||||||
|
// reinici, aixi que els static interns no s'escapen.
|
||||||
|
char** g_argv = nullptr;
|
||||||
|
bool g_requested = false;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace System::Relaunch {
|
||||||
|
|
||||||
|
void setArgv(int /*argc*/, char** argv) {
|
||||||
|
g_argv = argv;
|
||||||
|
}
|
||||||
|
|
||||||
|
void request() {
|
||||||
|
g_requested = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto isRequested() -> bool {
|
||||||
|
return g_requested;
|
||||||
|
}
|
||||||
|
|
||||||
|
void execIfRequested() {
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
// Al navegador el reinici real seria location.reload(); aqui no fem res.
|
||||||
|
return;
|
||||||
|
#else
|
||||||
|
if (!g_requested || g_argv == nullptr || g_argv[0] == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::cout << "[Relaunch] Reiniciant " << g_argv[0] << "...\n";
|
||||||
|
#ifdef _WIN32
|
||||||
|
_execv(g_argv[0], g_argv);
|
||||||
|
#else
|
||||||
|
execv(g_argv[0], g_argv);
|
||||||
|
#endif
|
||||||
|
// Si arribem aqui, execv ha fallat. Tots els subsistemes ja estan
|
||||||
|
// destruits; sortim amb error i el shell rebra el codi.
|
||||||
|
std::cerr << "[Relaunch] Ha fallat: " << std::strerror(errno) << '\n';
|
||||||
|
std::exit(EXIT_FAILURE);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace System::Relaunch
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// relaunch.hpp - Reinici en calent del proces (execv)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Helper desacoblat per a permetre que el menu de servei demani un reinici
|
||||||
|
// sense conèixer Director ni main.cpp. Patro:
|
||||||
|
//
|
||||||
|
// main() → Relaunch::setArgv(argc, argv) (a l'arrencada)
|
||||||
|
// ServiceMenu → Relaunch::request() (en activar REINICIAR)
|
||||||
|
// main() → Relaunch::execIfRequested() (a SDL_AppQuit)
|
||||||
|
//
|
||||||
|
// L'execv() reemplaca el proces actual: si torna, ha fallat. A EMSCRIPTEN
|
||||||
|
// no es pot reiniciar; isRequested() seguira dient true pero execIfRequested
|
||||||
|
// sera no-op.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace System::Relaunch {
|
||||||
|
|
||||||
|
// Emmagatzema l'argv original. Cal cridar-ho una vegada des de main.
|
||||||
|
void setArgv(int argc, char** argv);
|
||||||
|
|
||||||
|
// Demana un reinici (no actua immediatament; nomes marca el flag).
|
||||||
|
void request();
|
||||||
|
|
||||||
|
// Consulta del flag.
|
||||||
|
[[nodiscard]] auto isRequested() -> bool;
|
||||||
|
|
||||||
|
// Si hi ha reinici demanat i tenim argv valid, fa execv. En cas d'exit
|
||||||
|
// no torna. Si execv falla, registra l'error i torna; el caller hauria
|
||||||
|
// de sortir normalment.
|
||||||
|
void execIfRequested();
|
||||||
|
|
||||||
|
} // namespace System::Relaunch
|
||||||
@@ -16,10 +16,7 @@ namespace SceneManager {
|
|||||||
// 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
|
|
||||||
// redirigeix aquest valor a TitleScene3D.
|
|
||||||
TITLE_3D, // Pantalla de títol 3D real (variant en proves)
|
|
||||||
GAME, // Juego principal (Asteroids)
|
GAME, // Juego principal (Asteroids)
|
||||||
EXIT // Salir del programa
|
EXIT // Salir del programa
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,930 @@
|
|||||||
|
// service_menu.cpp - Implementacio del menu de servei
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "core/system/service_menu.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <format>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "core/audio/audio.hpp"
|
||||||
|
#include "core/config/engine_config.hpp"
|
||||||
|
#include "core/defaults/audio.hpp"
|
||||||
|
#include "core/defaults/rendering.hpp"
|
||||||
|
#include "core/defaults/service_menu.hpp"
|
||||||
|
#include "core/locale/locale.hpp"
|
||||||
|
#include "core/rendering/sdl_manager.hpp"
|
||||||
|
#include "core/system/debug_overlay.hpp"
|
||||||
|
#include "core/system/relaunch.hpp"
|
||||||
|
#include "core/types.hpp"
|
||||||
|
#include "game/config_yaml.hpp"
|
||||||
|
#include "project.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Easing ease-out quadratic per a l'obertura/tancament. Identic a
|
||||||
|
// aee_arcade service_menu.cpp:114-120.
|
||||||
|
auto easeOutQuad(float t) -> float {
|
||||||
|
t = std::clamp(t, 0.0F, 1.0F);
|
||||||
|
const float INV = 1.0F - t;
|
||||||
|
return 1.0F - (INV * INV);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canvas logic del joc (constants compartides amb la resta del renderer).
|
||||||
|
constexpr float CANVAS_W = 1280.0F;
|
||||||
|
constexpr float CANVAS_H = 720.0F;
|
||||||
|
|
||||||
|
// Crida pushRect amb un SDL_Color (els components s'escalen a [0..1]).
|
||||||
|
void fillRect(Rendering::Renderer* renderer, float x, float y, float w, float h, SDL_Color color) {
|
||||||
|
renderer->pushRect(x, y, w, h, static_cast<float>(color.r) / 255.0F, static_cast<float>(color.g) / 255.0F, static_cast<float>(color.b) / 255.0F, static_cast<float>(color.a) / 255.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
void playSelectSound() {
|
||||||
|
if (auto* audio = Audio::get(); audio != nullptr) {
|
||||||
|
audio->playSound(Defaults::ServiceMenu::SELECT_SOUND, Audio::Group::INTERFACE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void playAcceptSound() {
|
||||||
|
if (auto* audio = Audio::get(); audio != nullptr) {
|
||||||
|
audio->playSound(Defaults::ServiceMenu::ACCEPT_SOUND, Audio::Group::INTERFACE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VectorText nomes admet ASCII en majuscules. El git hash sortit de git
|
||||||
|
// rev-parse es lowercase (a-f), aixi que el passem a uppercase per al
|
||||||
|
// display sense modificar Project::GIT_HASH.
|
||||||
|
auto toUpperAscii(const std::string& s) -> std::string {
|
||||||
|
std::string result = s;
|
||||||
|
for (char& c : result) {
|
||||||
|
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resol el text del label d'un item: prioritza label_text (literal) sobre
|
||||||
|
// label_key (locale). Retorna cadena buida si tots dos son buits.
|
||||||
|
auto resolveLabel(const System::ServiceMenu::Item& item) -> std::string {
|
||||||
|
if (!item.label_text.empty()) {
|
||||||
|
return item.label_text;
|
||||||
|
}
|
||||||
|
if (item.label_key.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return Locale::get().text(item.label_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace System {
|
||||||
|
|
||||||
|
std::unique_ptr<ServiceMenu> ServiceMenu::instance;
|
||||||
|
|
||||||
|
void ServiceMenu::init(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay) {
|
||||||
|
instance.reset(new ServiceMenu(renderer, sdl, debug_overlay));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ServiceMenu::destroy() {
|
||||||
|
instance.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ServiceMenu::get() -> ServiceMenu* {
|
||||||
|
return instance.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceMenu::ServiceMenu(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay)
|
||||||
|
: renderer_(renderer),
|
||||||
|
sdl_(sdl),
|
||||||
|
debug_overlay_(debug_overlay),
|
||||||
|
text_(renderer) {}
|
||||||
|
|
||||||
|
auto ServiceMenu::isOpen() const -> bool {
|
||||||
|
return open_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ServiceMenu::toggle() {
|
||||||
|
if (!open_) {
|
||||||
|
open_ = true;
|
||||||
|
closing_ = false;
|
||||||
|
open_anim_ = 0.0F;
|
||||||
|
animated_h_ = 0.0F;
|
||||||
|
highlight_snap_ = true; // primera frame: enganxar el highlight al cursor
|
||||||
|
buildRootPage();
|
||||||
|
// L'ample comença ja al valor objectiu (la caixa surt amb l'amplada
|
||||||
|
// final i nomes anima l'alçada). L'ample s'animarà despres entre
|
||||||
|
// pagines (push/pop).
|
||||||
|
animated_w_ = computeTargetWidth();
|
||||||
|
playAcceptSound();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Ja obert: iniciem tancament. open_ es mante a true fins que l'animacio
|
||||||
|
// arriba a 0, per a permetre que update() segueixi avançant open_anim_.
|
||||||
|
closing_ = true;
|
||||||
|
playAcceptSound();
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Helper local: construeix un item de tipus SUBMENU amb el callback
|
||||||
|
// d'entrada. Es manté local a aquesta TU per a poder construir la
|
||||||
|
// pagina arrel a buildRootPage sense designed-initializers parcials
|
||||||
|
// (clang-tidy es queixa quan en falten).
|
||||||
|
auto makeSubmenu(const std::string& label_key, std::function<void()> on_activate) -> ServiceMenu::Item {
|
||||||
|
return ServiceMenu::Item{
|
||||||
|
.kind = ServiceMenu::Kind::SUBMENU,
|
||||||
|
.label_key = label_key,
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = std::move(on_activate),
|
||||||
|
.get_value_text = {},
|
||||||
|
.on_change = {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ServiceMenu::buildRootPage() {
|
||||||
|
Page root;
|
||||||
|
root.title_key = "service_menu.title";
|
||||||
|
root.items = {
|
||||||
|
makeSubmenu("service_menu.video", [this] { pushPage(buildVideoPage()); }),
|
||||||
|
makeSubmenu("service_menu.audio", [this] { pushPage(buildAudioPage()); }),
|
||||||
|
makeSubmenu("service_menu.options", [this] { pushPage(buildOptionsPage()); }),
|
||||||
|
makeSubmenu("service_menu.system", [this] { pushPage(buildSystemPage()); }),
|
||||||
|
};
|
||||||
|
stack_.clear();
|
||||||
|
stack_.push_back(std::move(root));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ServiceMenu::buildVideoPage() -> Page {
|
||||||
|
// Helper: localitza ON/OFF per a TOGGLE items.
|
||||||
|
auto on_off_text = [](bool v) -> std::string {
|
||||||
|
return Locale::get().text(v ? "service_menu.value_on" : "service_menu.value_off");
|
||||||
|
};
|
||||||
|
|
||||||
|
SDLManager* sdl = sdl_;
|
||||||
|
|
||||||
|
Page page;
|
||||||
|
page.title_key = "service_menu.video";
|
||||||
|
page.items = {
|
||||||
|
// ZOOM (INT_RANGE-style: ± delega a sdl.increase/decreaseWindowSize).
|
||||||
|
Item{
|
||||||
|
.kind = Kind::INT_RANGE,
|
||||||
|
.label_key = "service_menu.video_zoom",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = {},
|
||||||
|
.get_value_text = [sdl] { return std::format("{:.1f}X", sdl->getScaleFactor()); },
|
||||||
|
.on_change = [sdl](int dir) {
|
||||||
|
if (dir > 0) {
|
||||||
|
sdl->increaseWindowSize();
|
||||||
|
} else {
|
||||||
|
sdl->decreaseWindowSize();
|
||||||
|
} },
|
||||||
|
},
|
||||||
|
// FULLSCREEN
|
||||||
|
Item{
|
||||||
|
.kind = Kind::TOGGLE,
|
||||||
|
.label_key = "service_menu.video_fullscreen",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = {},
|
||||||
|
.get_value_text = [sdl, on_off_text] { return on_off_text(sdl->isFullscreen()); },
|
||||||
|
.on_change = [sdl](int) { sdl->toggleFullscreen(); },
|
||||||
|
},
|
||||||
|
// VSYNC
|
||||||
|
Item{
|
||||||
|
.kind = Kind::TOGGLE,
|
||||||
|
.label_key = "service_menu.video_vsync",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = {},
|
||||||
|
.get_value_text = [on_off_text] { return on_off_text(ConfigYaml::engine_config.rendering.vsync != 0); },
|
||||||
|
.on_change = [sdl](int) { sdl->toggleVSync(); },
|
||||||
|
},
|
||||||
|
// RESOLUCIO (sub-submenu amb els 5 presets; mostra l'actual com a valor)
|
||||||
|
Item{
|
||||||
|
.kind = Kind::SUBMENU,
|
||||||
|
.label_key = "service_menu.video_resolution",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = [this] { pushPage(buildResolutionPage()); },
|
||||||
|
.get_value_text = [] { return std::format("{}X{}",
|
||||||
|
ConfigYaml::engine_config.rendering.render_width,
|
||||||
|
ConfigYaml::engine_config.rendering.render_height); },
|
||||||
|
.on_change = {},
|
||||||
|
},
|
||||||
|
// ANTIALIAS
|
||||||
|
Item{
|
||||||
|
.kind = Kind::TOGGLE,
|
||||||
|
.label_key = "service_menu.video_aa",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = {},
|
||||||
|
.get_value_text = [on_off_text] { return on_off_text(ConfigYaml::engine_config.rendering.antialias != 0); },
|
||||||
|
.on_change = [sdl](int) { sdl->toggleAntialias(); },
|
||||||
|
},
|
||||||
|
// POSTPROCESSAT
|
||||||
|
Item{
|
||||||
|
.kind = Kind::TOGGLE,
|
||||||
|
.label_key = "service_menu.video_postfx",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = {},
|
||||||
|
.get_value_text = [sdl, on_off_text] { return on_off_text(sdl->isPostFxEnabled()); },
|
||||||
|
.on_change = [sdl](int) { sdl->togglePostFx(); },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ServiceMenu::buildResolutionPage() const -> Page {
|
||||||
|
Page page;
|
||||||
|
page.title_key = "service_menu.video_resolution";
|
||||||
|
// El cursor arrenca sobre el preset actual perquè l'usuari vegi quin
|
||||||
|
// esta seleccionat sense buscar-lo.
|
||||||
|
const int CURR_W = ConfigYaml::engine_config.rendering.render_width;
|
||||||
|
const int CURR_H = ConfigYaml::engine_config.rendering.render_height;
|
||||||
|
std::size_t cursor = 0;
|
||||||
|
SDLManager* sdl = sdl_;
|
||||||
|
for (std::size_t i = 0; i < Defaults::Rendering::RESOLUTION_PRESETS.size(); ++i) {
|
||||||
|
const auto& preset = Defaults::Rendering::RESOLUTION_PRESETS[i];
|
||||||
|
if (preset.w == CURR_W && preset.h == CURR_H) {
|
||||||
|
cursor = i;
|
||||||
|
}
|
||||||
|
const int PW = preset.w;
|
||||||
|
const int PH = preset.h;
|
||||||
|
page.items.push_back(Item{
|
||||||
|
.kind = Kind::ACTION,
|
||||||
|
.label_key = {},
|
||||||
|
.label_text = std::format("{}X{}", PW, PH),
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = [sdl, PW, PH] { sdl->setRenderResolution(PW, PH); },
|
||||||
|
.get_value_text = {},
|
||||||
|
.on_change = {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
page.cursor = cursor;
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ServiceMenu::buildAudioPage() -> Page {
|
||||||
|
auto on_off_text = [](bool v) -> std::string {
|
||||||
|
return Locale::get().text(v ? "service_menu.value_on" : "service_menu.value_off");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Aplica un step de volum (±VOLUME_STEP) a un valor 0..1 i retorna el
|
||||||
|
// resultat clampat. El motor s'encarrega d'aplicar-lo amb el getter.
|
||||||
|
auto step_volume = [](float current, int dir) -> float {
|
||||||
|
const float STEP = Defaults::Audio::VOLUME_STEP;
|
||||||
|
return std::clamp(current + (static_cast<float>(dir) * STEP), 0.0F, 1.0F);
|
||||||
|
};
|
||||||
|
|
||||||
|
Page page;
|
||||||
|
page.title_key = "service_menu.audio";
|
||||||
|
page.items = {
|
||||||
|
// AUDIO (master ON/OFF)
|
||||||
|
Item{
|
||||||
|
.kind = Kind::TOGGLE,
|
||||||
|
.label_key = "service_menu.audio_master",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = {},
|
||||||
|
.get_value_text = [on_off_text] {
|
||||||
|
const Audio* a = Audio::get();
|
||||||
|
return on_off_text(a != nullptr && a->isEnabled()); },
|
||||||
|
.on_change = [](int) {
|
||||||
|
if (auto* a = Audio::get(); a != nullptr) {
|
||||||
|
a->toggleEnabled();
|
||||||
|
} },
|
||||||
|
},
|
||||||
|
// VOLUM GENERAL (master)
|
||||||
|
Item{
|
||||||
|
.kind = Kind::INT_RANGE,
|
||||||
|
.label_key = "service_menu.audio_master_volume",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = {},
|
||||||
|
.get_value_text = [] {
|
||||||
|
const Audio* a = Audio::get();
|
||||||
|
const float V = (a != nullptr) ? a->getMasterVolume() : 0.0F;
|
||||||
|
return std::to_string(Audio::toPercent(V)); },
|
||||||
|
.on_change = [step_volume](int dir) {
|
||||||
|
if (auto* a = Audio::get(); a != nullptr) {
|
||||||
|
a->setMasterVolume(step_volume(a->getMasterVolume(), dir));
|
||||||
|
} },
|
||||||
|
},
|
||||||
|
// MUSICA ON/OFF
|
||||||
|
Item{
|
||||||
|
.kind = Kind::TOGGLE,
|
||||||
|
.label_key = "service_menu.audio_music",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = {},
|
||||||
|
.get_value_text = [on_off_text] {
|
||||||
|
const Audio* a = Audio::get();
|
||||||
|
return on_off_text(a != nullptr && a->isMusicEnabled()); },
|
||||||
|
.on_change = [](int) {
|
||||||
|
if (auto* a = Audio::get(); a != nullptr) {
|
||||||
|
a->toggleMusic();
|
||||||
|
} },
|
||||||
|
},
|
||||||
|
// VOLUM MUSICA
|
||||||
|
Item{
|
||||||
|
.kind = Kind::INT_RANGE,
|
||||||
|
.label_key = "service_menu.audio_music_volume",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = {},
|
||||||
|
.get_value_text = [] {
|
||||||
|
const Audio* a = Audio::get();
|
||||||
|
const float V = (a != nullptr) ? a->getMusicVolume() : 0.0F;
|
||||||
|
return std::to_string(Audio::toPercent(V)); },
|
||||||
|
.on_change = [step_volume](int dir) {
|
||||||
|
if (auto* a = Audio::get(); a != nullptr) {
|
||||||
|
a->setMusicVolume(step_volume(a->getMusicVolume(), dir));
|
||||||
|
} },
|
||||||
|
},
|
||||||
|
// SONS ON/OFF
|
||||||
|
Item{
|
||||||
|
.kind = Kind::TOGGLE,
|
||||||
|
.label_key = "service_menu.audio_sound",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = {},
|
||||||
|
.get_value_text = [on_off_text] {
|
||||||
|
const Audio* a = Audio::get();
|
||||||
|
return on_off_text(a != nullptr && a->isSoundEnabled()); },
|
||||||
|
.on_change = [](int) {
|
||||||
|
if (auto* a = Audio::get(); a != nullptr) {
|
||||||
|
a->toggleSound();
|
||||||
|
} },
|
||||||
|
},
|
||||||
|
// VOLUM SONS
|
||||||
|
Item{
|
||||||
|
.kind = Kind::INT_RANGE,
|
||||||
|
.label_key = "service_menu.audio_sound_volume",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = {},
|
||||||
|
.get_value_text = [] {
|
||||||
|
const Audio* a = Audio::get();
|
||||||
|
const float V = (a != nullptr) ? a->getSoundVolume() : 0.0F;
|
||||||
|
return std::to_string(Audio::toPercent(V)); },
|
||||||
|
.on_change = [step_volume](int dir) {
|
||||||
|
if (auto* a = Audio::get(); a != nullptr) {
|
||||||
|
a->setSoundVolume(step_volume(a->getSoundVolume(), dir));
|
||||||
|
} },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ServiceMenu::buildOptionsPage() const -> Page {
|
||||||
|
auto on_off_text = [](bool v) -> std::string {
|
||||||
|
return Locale::get().text(v ? "service_menu.value_on" : "service_menu.value_off");
|
||||||
|
};
|
||||||
|
|
||||||
|
DebugOverlay* debug = debug_overlay_;
|
||||||
|
|
||||||
|
Page page;
|
||||||
|
page.title_key = "service_menu.options";
|
||||||
|
page.items = {
|
||||||
|
// IDIOMA (cycle entre ca i en, mateix codi que F7).
|
||||||
|
Item{
|
||||||
|
.kind = Kind::CYCLE,
|
||||||
|
.label_key = "service_menu.options_language",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = {},
|
||||||
|
.get_value_text = [] { return Locale::get().text("language." + ConfigYaml::engine_config.locale); },
|
||||||
|
.on_change = [](int) {
|
||||||
|
const std::string NEW_LANG = (ConfigYaml::engine_config.locale == "ca") ? "en" : "ca";
|
||||||
|
if (Locale::get().switchTo(NEW_LANG)) {
|
||||||
|
ConfigYaml::engine_config.locale = NEW_LANG;
|
||||||
|
ConfigYaml::saveToFile();
|
||||||
|
} },
|
||||||
|
},
|
||||||
|
// MOSTRAR INFO (debug overlay, equivalent a F11).
|
||||||
|
Item{
|
||||||
|
.kind = Kind::TOGGLE,
|
||||||
|
.label_key = "service_menu.options_show_info",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = {},
|
||||||
|
.get_value_text = [debug, on_off_text] { return on_off_text(debug != nullptr && debug->isVisible()); },
|
||||||
|
.on_change = [debug](int) {
|
||||||
|
if (debug != nullptr) {
|
||||||
|
debug->toggle();
|
||||||
|
} },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ServiceMenu::buildSystemPage() -> Page {
|
||||||
|
Page page;
|
||||||
|
page.title_key = "service_menu.system";
|
||||||
|
// Versio + hash com a subtitol sota el titol (apagat, mes petit).
|
||||||
|
// Uppercase del hash perque VectorText nomes admet majuscules.
|
||||||
|
page.subtitle_provider = [] {
|
||||||
|
return std::format("V{} - {}", Project::VERSION, toUpperAscii(Project::GIT_HASH));
|
||||||
|
};
|
||||||
|
page.items = {
|
||||||
|
// REINICIAR (amb confirmacio).
|
||||||
|
Item{
|
||||||
|
.kind = Kind::ACTION,
|
||||||
|
.label_key = "service_menu.system_restart",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = [this] {
|
||||||
|
pushConfirmPage("service_menu.confirm_restart", [] {
|
||||||
|
System::Relaunch::request();
|
||||||
|
SDL_Event quit_event{};
|
||||||
|
quit_event.type = SDL_EVENT_QUIT;
|
||||||
|
SDL_PushEvent(&quit_event);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
.get_value_text = {},
|
||||||
|
.on_change = {},
|
||||||
|
},
|
||||||
|
// EIXIR DEL JOC (amb confirmacio).
|
||||||
|
Item{
|
||||||
|
.kind = Kind::ACTION,
|
||||||
|
.label_key = "service_menu.exit",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = [this] {
|
||||||
|
pushConfirmPage("service_menu.confirm_exit", [] {
|
||||||
|
SDL_Event quit_event{};
|
||||||
|
quit_event.type = SDL_EVENT_QUIT;
|
||||||
|
SDL_PushEvent(&quit_event);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
.get_value_text = {},
|
||||||
|
.on_change = {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ServiceMenu::pushConfirmPage(const std::string& title_key, std::function<void()> on_yes) {
|
||||||
|
auto yes_callback = std::move(on_yes);
|
||||||
|
Page page;
|
||||||
|
page.title_key = title_key;
|
||||||
|
page.cursor = 0; // per defecte sobre NO (segur)
|
||||||
|
page.items = {
|
||||||
|
Item{
|
||||||
|
.kind = Kind::ACTION,
|
||||||
|
.label_key = "service_menu.confirm_no",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = [this] { popPage(); },
|
||||||
|
.get_value_text = {},
|
||||||
|
.on_change = {},
|
||||||
|
},
|
||||||
|
Item{
|
||||||
|
.kind = Kind::ACTION,
|
||||||
|
.label_key = "service_menu.confirm_yes",
|
||||||
|
.label_text = {},
|
||||||
|
.selectable = true,
|
||||||
|
.on_activate = std::move(yes_callback),
|
||||||
|
.get_value_text = {},
|
||||||
|
.on_change = {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
pushPage(std::move(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ServiceMenu::pushPage(Page page) {
|
||||||
|
stack_.push_back(std::move(page));
|
||||||
|
// El cursor salta a una pagina nova: enganxem el highlight per a
|
||||||
|
// evitar que vagi lliscant des de la posicio anterior.
|
||||||
|
highlight_snap_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ServiceMenu::popPage() {
|
||||||
|
if (stack_.size() <= 1) {
|
||||||
|
// Estem a la pagina arrel: BACKSPACE tanca el menu.
|
||||||
|
closing_ = true;
|
||||||
|
playAcceptSound();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stack_.pop_back();
|
||||||
|
highlight_snap_ = true;
|
||||||
|
playAcceptSound();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ServiceMenu::moveCursor(int direction) {
|
||||||
|
if (stack_.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Page& page = stack_.back();
|
||||||
|
const std::size_t N = page.items.size();
|
||||||
|
if (N == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Cerca el seguent item seleccionable amb wrap-around.
|
||||||
|
std::size_t idx = page.cursor;
|
||||||
|
for (std::size_t step = 0; step < N; ++step) {
|
||||||
|
idx = (idx + static_cast<std::size_t>(direction + static_cast<int>(N))) % N;
|
||||||
|
if (page.items[idx].selectable) {
|
||||||
|
if (idx != page.cursor) {
|
||||||
|
page.cursor = idx;
|
||||||
|
playSelectSound();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ServiceMenu::activateCurrent() {
|
||||||
|
// ENTER = canvi de valor cap endavant (equivalent a RIGHT). Per a
|
||||||
|
// SUBMENU/ACTION entra/activa; per a TOGGLE/CYCLE/INT_RANGE incrementa.
|
||||||
|
changeValue(+1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ServiceMenu::changeValue(int direction) {
|
||||||
|
if (stack_.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Page& page = stack_.back();
|
||||||
|
if (page.cursor >= page.items.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Item& item = page.items[page.cursor];
|
||||||
|
if (!item.selectable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (item.kind) {
|
||||||
|
case Kind::TOGGLE:
|
||||||
|
case Kind::CYCLE:
|
||||||
|
case Kind::INT_RANGE:
|
||||||
|
if (item.on_change) {
|
||||||
|
item.on_change(direction);
|
||||||
|
playAcceptSound();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Kind::SUBMENU:
|
||||||
|
case Kind::ACTION:
|
||||||
|
// Nomes +1 entra/activa: LEFT no fa res (BACKSPACE per a sortir).
|
||||||
|
if (direction > 0 && item.on_activate) {
|
||||||
|
item.on_activate();
|
||||||
|
playAcceptSound();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Kind::LABEL:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ServiceMenu::handleEvent(const SDL_Event& event) -> bool {
|
||||||
|
if (!open_ || stack_.empty() || event.type != SDL_EVENT_KEY_DOWN) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (event.key.scancode) {
|
||||||
|
case SDL_SCANCODE_UP:
|
||||||
|
moveCursor(-1);
|
||||||
|
return true;
|
||||||
|
case SDL_SCANCODE_DOWN:
|
||||||
|
moveCursor(+1);
|
||||||
|
return true;
|
||||||
|
case SDL_SCANCODE_RETURN:
|
||||||
|
case SDL_SCANCODE_KP_ENTER:
|
||||||
|
activateCurrent();
|
||||||
|
return true;
|
||||||
|
case SDL_SCANCODE_RIGHT:
|
||||||
|
changeValue(+1);
|
||||||
|
return true;
|
||||||
|
case SDL_SCANCODE_LEFT:
|
||||||
|
changeValue(-1);
|
||||||
|
return true;
|
||||||
|
case SDL_SCANCODE_BACKSPACE:
|
||||||
|
popPage();
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ServiceMenu::computeTargetHeight() const -> float {
|
||||||
|
if (stack_.empty()) {
|
||||||
|
return 0.0F;
|
||||||
|
}
|
||||||
|
using namespace Defaults::ServiceMenu;
|
||||||
|
const Page& page = stack_.back();
|
||||||
|
int h = GAP_Y; // padding superior
|
||||||
|
h += TITLE_HEIGHT; // titol
|
||||||
|
if (page.subtitle_provider) {
|
||||||
|
h += GAP_Y / 2 + SUBTITLE_HEIGHT; // subtitol amb mig gap
|
||||||
|
}
|
||||||
|
h += GAP_Y; // gap abans del separador
|
||||||
|
h += SEPARATOR_HEIGHT + GAP_Y; // separador + gap
|
||||||
|
const auto N = static_cast<int>(page.items.size());
|
||||||
|
if (N > 0) {
|
||||||
|
h += (N * ITEM_HEIGHT) + ((N - 1) * ITEM_GAP_Y) + GAP_Y;
|
||||||
|
}
|
||||||
|
return static_cast<float>(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ServiceMenu::computeTargetWidth() const -> float {
|
||||||
|
using namespace Defaults::ServiceMenu;
|
||||||
|
if (stack_.empty()) {
|
||||||
|
return static_cast<float>(BOX_WIDTH_MIN);
|
||||||
|
}
|
||||||
|
const Page& page = stack_.back();
|
||||||
|
// Comencem amb l'ample del titol.
|
||||||
|
float content_w = Graphics::VectorText::getTextWidth(
|
||||||
|
Locale::get().text(page.title_key),
|
||||||
|
TITLE_SCALE,
|
||||||
|
TEXT_SPACING);
|
||||||
|
if (page.subtitle_provider) {
|
||||||
|
content_w = std::max(content_w, Graphics::VectorText::getTextWidth(page.subtitle_provider(), SUBTITLE_SCALE, TEXT_SPACING));
|
||||||
|
}
|
||||||
|
for (const Item& item : page.items) {
|
||||||
|
const std::string LABEL = resolveLabel(item);
|
||||||
|
if (LABEL.empty() && item.get_value_text) {
|
||||||
|
content_w = std::max(content_w, Graphics::VectorText::getTextWidth(item.get_value_text(), ITEM_SCALE, TEXT_SPACING));
|
||||||
|
} else if (item.get_value_text) {
|
||||||
|
const float LABEL_W = Graphics::VectorText::getTextWidth(LABEL, ITEM_SCALE, TEXT_SPACING);
|
||||||
|
const float VALUE_W = Graphics::VectorText::getTextWidth(
|
||||||
|
item.get_value_text(),
|
||||||
|
ITEM_SCALE,
|
||||||
|
TEXT_SPACING);
|
||||||
|
content_w = std::max(content_w,
|
||||||
|
LABEL_W + static_cast<float>(MIN_LABEL_VALUE_GAP) + VALUE_W);
|
||||||
|
} else {
|
||||||
|
content_w = std::max(content_w,
|
||||||
|
Graphics::VectorText::getTextWidth(LABEL, ITEM_SCALE, TEXT_SPACING));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Padding total: highlight pad als dos costats + inset del text.
|
||||||
|
const float REQUIRED = content_w +
|
||||||
|
(2.0F * static_cast<float>(HIGHLIGHT_PAD_X)) +
|
||||||
|
(2.0F * static_cast<float>(TEXT_INSET_X));
|
||||||
|
return std::max(static_cast<float>(BOX_WIDTH_MIN), REQUIRED);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ServiceMenu::computeItemTopY(float box_y, std::size_t index, bool has_subtitle) -> float {
|
||||||
|
using namespace Defaults::ServiceMenu;
|
||||||
|
float items_y0 = box_y +
|
||||||
|
static_cast<float>(GAP_Y) +
|
||||||
|
static_cast<float>(TITLE_HEIGHT);
|
||||||
|
if (has_subtitle) {
|
||||||
|
items_y0 += static_cast<float>(GAP_Y / 2) + static_cast<float>(SUBTITLE_HEIGHT);
|
||||||
|
}
|
||||||
|
items_y0 += static_cast<float>(GAP_Y) +
|
||||||
|
static_cast<float>(SEPARATOR_HEIGHT) +
|
||||||
|
static_cast<float>(GAP_Y);
|
||||||
|
return items_y0 + (static_cast<float>(index) * static_cast<float>(ITEM_HEIGHT + ITEM_GAP_Y));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ServiceMenu::update(float delta_time) {
|
||||||
|
if (!open_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
using namespace Defaults::ServiceMenu;
|
||||||
|
|
||||||
|
if (closing_) {
|
||||||
|
open_anim_ -= CLOSE_SPEED * delta_time;
|
||||||
|
if (open_anim_ <= 0.0F) {
|
||||||
|
open_anim_ = 0.0F;
|
||||||
|
animated_h_ = 0.0F;
|
||||||
|
open_ = false;
|
||||||
|
closing_ = false;
|
||||||
|
stack_.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
open_anim_ = std::min(1.0F, open_anim_ + (OPEN_SPEED * delta_time));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smoothing exponencial cap a l'alçada i ample objectius de la pagina.
|
||||||
|
const float TARGET_H_BOX = closing_ ? 0.0F : computeTargetHeight();
|
||||||
|
const float TARGET_W_BOX = closing_ ? animated_w_ : computeTargetWidth();
|
||||||
|
const float ALPHA_H = 1.0F - std::exp(-HEIGHT_RATE * delta_time);
|
||||||
|
const float ALPHA_W = 1.0F - std::exp(-WIDTH_RATE * delta_time);
|
||||||
|
animated_h_ += (TARGET_H_BOX - animated_h_) * ALPHA_H;
|
||||||
|
animated_w_ += (TARGET_W_BOX - animated_w_) * ALPHA_W;
|
||||||
|
|
||||||
|
// Highlight: lerp cap a la Y de l'item del cursor. Snapping nomes en
|
||||||
|
// obrir o canviar de pagina; en moure el cursor amb UP/DOWN, el rect
|
||||||
|
// llisca suaument cap a la nova posicio.
|
||||||
|
if (stack_.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Page& page = stack_.back();
|
||||||
|
if (page.items.empty()) {
|
||||||
|
highlight_snap_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float BOX_H_TARGET = computeTargetHeight();
|
||||||
|
const float BOX_Y_TARGET = (CANVAS_H - BOX_H_TARGET) * 0.5F;
|
||||||
|
const bool HAS_SUBTITLE = static_cast<bool>(page.subtitle_provider);
|
||||||
|
const float ITEM_TOP = computeItemTopY(BOX_Y_TARGET, page.cursor, HAS_SUBTITLE);
|
||||||
|
const float TARGET_Y = ITEM_TOP - static_cast<float>(HIGHLIGHT_PAD_Y);
|
||||||
|
const float TARGET_H = static_cast<float>(ITEM_HEIGHT) + (2.0F * static_cast<float>(HIGHLIGHT_PAD_Y));
|
||||||
|
if (highlight_snap_) {
|
||||||
|
highlight_y_ = TARGET_Y;
|
||||||
|
highlight_h_ = TARGET_H;
|
||||||
|
highlight_snap_ = false;
|
||||||
|
} else {
|
||||||
|
const float HL_ALPHA = 1.0F - std::exp(-HIGHLIGHT_RATE * delta_time);
|
||||||
|
highlight_y_ += (TARGET_Y - highlight_y_) * HL_ALPHA;
|
||||||
|
highlight_h_ += (TARGET_H - highlight_h_) * HL_ALPHA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Dibuixa un rect (BG sombrejat + 4 ticks L als cantons), simulant
|
||||||
|
// un visor sci-fi al voltant de l'item sel·leccionat.
|
||||||
|
void drawHighlightRect(Rendering::Renderer* renderer, float x, float y, float w, float h) {
|
||||||
|
using namespace Defaults::ServiceMenu;
|
||||||
|
if (w <= 0.0F || h <= 0.0F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Wash de fons translucid.
|
||||||
|
fillRect(renderer, x, y, w, h, HIGHLIGHT_FILL);
|
||||||
|
|
||||||
|
const auto T = static_cast<float>(HIGHLIGHT_THICKNESS);
|
||||||
|
const auto L = static_cast<float>(HIGHLIGHT_TICK_LEN);
|
||||||
|
|
||||||
|
// Top-left
|
||||||
|
fillRect(renderer, x, y, L, T, HIGHLIGHT_OUTLINE);
|
||||||
|
fillRect(renderer, x, y, T, L, HIGHLIGHT_OUTLINE);
|
||||||
|
// Top-right
|
||||||
|
fillRect(renderer, x + w - L, y, L, T, HIGHLIGHT_OUTLINE);
|
||||||
|
fillRect(renderer, x + w - T, y, T, L, HIGHLIGHT_OUTLINE);
|
||||||
|
// Bottom-left
|
||||||
|
fillRect(renderer, x, y + h - T, L, T, HIGHLIGHT_OUTLINE);
|
||||||
|
fillRect(renderer, x, y + h - L, T, L, HIGHLIGHT_OUTLINE);
|
||||||
|
// Bottom-right
|
||||||
|
fillRect(renderer, x + w - L, y + h - T, L, T, HIGHLIGHT_OUTLINE);
|
||||||
|
fillRect(renderer, x + w - T, y + h - L, T, L, HIGHLIGHT_OUTLINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Brackets als 4 cantons de la caixa (sci-fi HUD). Substitueix la vora
|
||||||
|
// completa per un marc obert.
|
||||||
|
void drawCornerBrackets(Rendering::Renderer* renderer, float x, float y, float w, float h) {
|
||||||
|
using namespace Defaults::ServiceMenu;
|
||||||
|
const auto T = static_cast<float>(CORNER_THICKNESS);
|
||||||
|
const auto AH = static_cast<float>(CORNER_ARM_H);
|
||||||
|
const auto AV = static_cast<float>(CORNER_ARM_V);
|
||||||
|
|
||||||
|
// Top-left
|
||||||
|
fillRect(renderer, x, y, AH, T, CORNER_COLOR);
|
||||||
|
fillRect(renderer, x, y, T, AV, CORNER_COLOR);
|
||||||
|
// Top-right
|
||||||
|
fillRect(renderer, x + w - AH, y, AH, T, CORNER_COLOR);
|
||||||
|
fillRect(renderer, x + w - T, y, T, AV, CORNER_COLOR);
|
||||||
|
// Bottom-left
|
||||||
|
fillRect(renderer, x, y + h - T, AH, T, CORNER_COLOR);
|
||||||
|
fillRect(renderer, x, y + h - AV, T, AV, CORNER_COLOR);
|
||||||
|
// Bottom-right
|
||||||
|
fillRect(renderer, x + w - AH, y + h - T, AH, T, CORNER_COLOR);
|
||||||
|
fillRect(renderer, x + w - T, y + h - AV, T, AV, CORNER_COLOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ServiceMenu::draw() const {
|
||||||
|
if (!open_ || stack_.empty() || renderer_ == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
using namespace Defaults::ServiceMenu;
|
||||||
|
|
||||||
|
// Alçada final: smoothing × easing. easeOutQuad afegeix la sensacio
|
||||||
|
// de "snap" al final de l'obertura i l'inici del tancament.
|
||||||
|
const float EASED = easeOutQuad(open_anim_);
|
||||||
|
const float BOX_H = animated_h_ * EASED;
|
||||||
|
if (BOX_H < 1.0F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float BOX_W = animated_w_;
|
||||||
|
const float BOX_X = (CANVAS_W - BOX_W) * 0.5F;
|
||||||
|
const float BOX_Y = (CANVAS_H - BOX_H) * 0.5F;
|
||||||
|
const float CENTER_X = BOX_X + (BOX_W * 0.5F);
|
||||||
|
|
||||||
|
// Fons semi-transparent.
|
||||||
|
fillRect(renderer_, BOX_X, BOX_Y, BOX_W, BOX_H, BG_COLOR);
|
||||||
|
|
||||||
|
// Brackets als cantons (substitueixen la vora completa).
|
||||||
|
drawCornerBrackets(renderer_, BOX_X, BOX_Y, BOX_W, BOX_H);
|
||||||
|
|
||||||
|
// Clip interior per a tallar text que sortiria del cuadre durant
|
||||||
|
// l'animacio open/close. Marge generos perquè no es mengi els brackets.
|
||||||
|
const int CLIP_X = static_cast<int>(BOX_X + static_cast<float>(CORNER_THICKNESS));
|
||||||
|
const int CLIP_Y = static_cast<int>(BOX_Y + static_cast<float>(CORNER_THICKNESS));
|
||||||
|
const int CLIP_W = static_cast<int>(BOX_W - (2.0F * static_cast<float>(CORNER_THICKNESS)));
|
||||||
|
const int CLIP_H = std::max(0, static_cast<int>(BOX_H - (2.0F * static_cast<float>(CORNER_THICKNESS))));
|
||||||
|
renderer_->pushClip(CLIP_X, CLIP_Y, CLIP_W, CLIP_H);
|
||||||
|
|
||||||
|
const Page& page = stack_.back();
|
||||||
|
const bool HAS_SUBTITLE = static_cast<bool>(page.subtitle_provider);
|
||||||
|
|
||||||
|
// Titol centrat al cim de la caixa.
|
||||||
|
const std::string TITLE = Locale::get().text(page.title_key);
|
||||||
|
const float TITLE_CY = BOX_Y + static_cast<float>(GAP_Y) + (static_cast<float>(TITLE_HEIGHT) * 0.5F);
|
||||||
|
text_.renderCentered(TITLE,
|
||||||
|
Vec2{.x = CENTER_X, .y = TITLE_CY},
|
||||||
|
TITLE_SCALE,
|
||||||
|
TEXT_SPACING,
|
||||||
|
1.0F,
|
||||||
|
TITLE_COLOR);
|
||||||
|
|
||||||
|
// Subtitol opcional: sota el titol, mes petit i apagat.
|
||||||
|
if (HAS_SUBTITLE) {
|
||||||
|
const float SUBTITLE_CY = BOX_Y + static_cast<float>(GAP_Y) +
|
||||||
|
static_cast<float>(TITLE_HEIGHT) +
|
||||||
|
(static_cast<float>(GAP_Y) / 4.0F) +
|
||||||
|
(static_cast<float>(SUBTITLE_HEIGHT) * 0.5F);
|
||||||
|
text_.renderCentered(page.subtitle_provider(),
|
||||||
|
Vec2{.x = CENTER_X, .y = SUBTITLE_CY},
|
||||||
|
SUBTITLE_SCALE,
|
||||||
|
TEXT_SPACING,
|
||||||
|
1.0F,
|
||||||
|
SUBTITLE_COLOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separador horitzontal sota el titol (o subtitol si n'hi ha).
|
||||||
|
float sep_y = BOX_Y + static_cast<float>(GAP_Y) + static_cast<float>(TITLE_HEIGHT);
|
||||||
|
if (HAS_SUBTITLE) {
|
||||||
|
sep_y += static_cast<float>(GAP_Y / 2) + static_cast<float>(SUBTITLE_HEIGHT);
|
||||||
|
}
|
||||||
|
sep_y += static_cast<float>(GAP_Y) * 0.5F;
|
||||||
|
const float SEP_Y = sep_y;
|
||||||
|
fillRect(renderer_,
|
||||||
|
BOX_X + static_cast<float>(GAP_Y),
|
||||||
|
SEP_Y,
|
||||||
|
BOX_W - (2.0F * static_cast<float>(GAP_Y)),
|
||||||
|
static_cast<float>(SEPARATOR_HEIGHT),
|
||||||
|
SEPARATOR_COLOR);
|
||||||
|
|
||||||
|
// Highlight rect: nomes si la pagina te items i el rect te alçada.
|
||||||
|
if (!page.items.empty() && highlight_h_ > 0.0F) {
|
||||||
|
const float HL_X = BOX_X + static_cast<float>(HIGHLIGHT_PAD_X);
|
||||||
|
const float HL_W = BOX_W - (2.0F * static_cast<float>(HIGHLIGHT_PAD_X));
|
||||||
|
drawHighlightRect(renderer_, HL_X, highlight_y_, HL_W, highlight_h_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llista d'items.
|
||||||
|
// - Items amb valor (TOGGLE/CYCLE/INT_RANGE): label esquerra + valor dreta dins del highlight.
|
||||||
|
// - Items sense valor (SUBMENU/ACTION/LABEL): label centrat.
|
||||||
|
const float HL_LEFT = BOX_X + static_cast<float>(HIGHLIGHT_PAD_X);
|
||||||
|
const float HL_RIGHT = BOX_X + BOX_W - static_cast<float>(HIGHLIGHT_PAD_X);
|
||||||
|
const float TEXT_TOP_OFFSET = Graphics::VectorText::getTextHeight(ITEM_SCALE) * 0.5F;
|
||||||
|
for (std::size_t i = 0; i < page.items.size(); ++i) {
|
||||||
|
const Item& item = page.items[i];
|
||||||
|
const SDL_Color COL = (i == page.cursor) ? CURSOR_COLOR : LABEL_COLOR;
|
||||||
|
// resolveLabel prioritza label_text (literal) sobre label_key (locale).
|
||||||
|
const std::string LABEL = resolveLabel(item);
|
||||||
|
const float ITEM_TOP = computeItemTopY(BOX_Y, i, HAS_SUBTITLE);
|
||||||
|
const float ITEM_CY = ITEM_TOP + (static_cast<float>(ITEM_HEIGHT) * 0.5F);
|
||||||
|
|
||||||
|
if (LABEL.empty() && item.get_value_text) {
|
||||||
|
// Item nomes-valor (sense label): el text del valor es
|
||||||
|
// renderitza centrat com a label decoratiu. Util per a items
|
||||||
|
// d'informacio com la versio/hash a SISTEMA.
|
||||||
|
text_.renderCentered(item.get_value_text(),
|
||||||
|
Vec2{.x = CENTER_X, .y = ITEM_CY},
|
||||||
|
ITEM_SCALE,
|
||||||
|
TEXT_SPACING,
|
||||||
|
1.0F,
|
||||||
|
COL);
|
||||||
|
} else if (item.get_value_text) {
|
||||||
|
// Layout dues columnes: label esquerra, valor dreta.
|
||||||
|
const std::string VALUE = item.get_value_text();
|
||||||
|
const float TEXT_TOP_Y = ITEM_CY - TEXT_TOP_OFFSET;
|
||||||
|
const float VALUE_W = Graphics::VectorText::getTextWidth(VALUE, ITEM_SCALE, TEXT_SPACING);
|
||||||
|
text_.render(LABEL,
|
||||||
|
Vec2{.x = HL_LEFT + static_cast<float>(TEXT_INSET_X), .y = TEXT_TOP_Y},
|
||||||
|
ITEM_SCALE,
|
||||||
|
TEXT_SPACING,
|
||||||
|
1.0F,
|
||||||
|
COL);
|
||||||
|
text_.render(VALUE,
|
||||||
|
Vec2{.x = HL_RIGHT - static_cast<float>(TEXT_INSET_X) - VALUE_W, .y = TEXT_TOP_Y},
|
||||||
|
ITEM_SCALE,
|
||||||
|
TEXT_SPACING,
|
||||||
|
1.0F,
|
||||||
|
COL);
|
||||||
|
} else {
|
||||||
|
// Layout simple: label centrat.
|
||||||
|
text_.renderCentered(LABEL,
|
||||||
|
Vec2{.x = CENTER_X, .y = ITEM_CY},
|
||||||
|
ITEM_SCALE,
|
||||||
|
TEXT_SPACING,
|
||||||
|
1.0F,
|
||||||
|
COL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer_->popClip();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace System
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
// service_menu.hpp - Menu de servei (singleton)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Overlay de configuracio global accessible amb F12 des de qualsevol escena
|
||||||
|
// (LOGO, TITLE, GAME). Captura tots els KEY_DOWN excepte F1-F12 i ESC, que
|
||||||
|
// continuen arribant a GlobalEvents. Mentre esta obert, GameScene::update()
|
||||||
|
// fa early return per pausar el joc; LOGO i TITLE continuen renderitzant-se
|
||||||
|
// sota el menu.
|
||||||
|
//
|
||||||
|
// Arquitectura inspirada en aee_arcade service_menu.{hpp,cpp}: pila de
|
||||||
|
// pagines amb cursor, animacio open/close amb easing easeOutQuad i clipping
|
||||||
|
// del contingut mentre la caixa creix/decreix.
|
||||||
|
//
|
||||||
|
// API singleton equivalent a Notifier: init() al startup amb un renderer,
|
||||||
|
// get() retorna el punter, destroy() al teardown.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/graphics/vector_text.hpp"
|
||||||
|
#include "core/rendering/render_context.hpp"
|
||||||
|
|
||||||
|
class SDLManager;
|
||||||
|
|
||||||
|
namespace System {
|
||||||
|
|
||||||
|
class DebugOverlay;
|
||||||
|
|
||||||
|
class ServiceMenu {
|
||||||
|
public:
|
||||||
|
// Tipus d'item de menu. En aquesta iteracio nomes s'usen SUBMENU i
|
||||||
|
// LABEL; la resta queden reservats per a iteracions futures (toggles
|
||||||
|
// de vsync/zoom, picker d'idioma, restart, exit...).
|
||||||
|
enum class Kind : std::uint8_t {
|
||||||
|
LABEL, // No interactiu, nomes es dibuixa
|
||||||
|
TOGGLE, // bool flip — reservat
|
||||||
|
CYCLE, // index amb modul — reservat
|
||||||
|
INT_RANGE, // step ± — reservat
|
||||||
|
SUBMENU, // pushPage en activar — usat
|
||||||
|
ACTION // call al lambda en activar — reservat
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Item {
|
||||||
|
Kind kind = Kind::LABEL;
|
||||||
|
std::string label_key; // Clau de locale (s'ignora si label_text no esta buit)
|
||||||
|
std::string label_text; // Text literal (no locale). Util per a labels que no necessiten traduccio (resolucions, etc.)
|
||||||
|
bool selectable = true;
|
||||||
|
// SUBMENU / ACTION: callback en ENTER / RIGHT.
|
||||||
|
std::function<void()> on_activate;
|
||||||
|
// TOGGLE / CYCLE / INT_RANGE: text del valor actual (renderitzat a la dreta).
|
||||||
|
std::function<std::string()> get_value_text;
|
||||||
|
// TOGGLE / CYCLE / INT_RANGE: callback amb +1 (RIGHT/ENTER) o -1 (LEFT).
|
||||||
|
std::function<void(int)> on_change;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Page {
|
||||||
|
std::string title_key;
|
||||||
|
// Subtitol opcional, renderitzat sota el titol amb tipografia mes
|
||||||
|
// petita i color apagat. Es una funcio perque pot ser dinamic
|
||||||
|
// (versio+hash, etc.). Si esta buit, no es renderitza.
|
||||||
|
std::function<std::string()> subtitle_provider;
|
||||||
|
std::vector<Item> items;
|
||||||
|
std::size_t cursor = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inicialitza el singleton amb el renderer global, l'SDLManager (video
|
||||||
|
// toggles: fullscreen, vsync, AA, postfx, zoom) i el DebugOverlay
|
||||||
|
// (toggle del HUD de debug a OPCIONS). Tots propietat del Director.
|
||||||
|
static void init(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay);
|
||||||
|
static void destroy();
|
||||||
|
[[nodiscard]] static auto get() -> ServiceMenu*;
|
||||||
|
|
||||||
|
// F12: alterna obrir/tancar amb animacio.
|
||||||
|
void toggle();
|
||||||
|
[[nodiscard]] auto isOpen() const -> bool;
|
||||||
|
|
||||||
|
void update(float delta_time);
|
||||||
|
void draw() const;
|
||||||
|
|
||||||
|
// Processa el KEY_DOWN. Retorna true si l'ha consumit (UP/DOWN/ENTER/
|
||||||
|
// RIGHT/BACKSPACE/LEFT mentre esta obert). false en qualsevol altre cas.
|
||||||
|
auto handleEvent(const SDL_Event& event) -> bool;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ServiceMenu(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay);
|
||||||
|
|
||||||
|
void buildRootPage();
|
||||||
|
[[nodiscard]] auto buildVideoPage() -> Page;
|
||||||
|
[[nodiscard]] auto buildResolutionPage() const -> Page;
|
||||||
|
[[nodiscard]] static auto buildAudioPage() -> Page;
|
||||||
|
[[nodiscard]] auto buildOptionsPage() const -> Page;
|
||||||
|
[[nodiscard]] auto buildSystemPage() -> Page;
|
||||||
|
// Pagina de confirmacio "ESTAS SEGUR? NO/SI". on_yes s'executa si
|
||||||
|
// l'usuari selecciona SI; el cursor per defecte apunta a NO.
|
||||||
|
void pushConfirmPage(const std::string& title_key, std::function<void()> on_yes);
|
||||||
|
void pushPage(Page page);
|
||||||
|
void popPage();
|
||||||
|
void moveCursor(int direction);
|
||||||
|
void activateCurrent();
|
||||||
|
// RIGHT (direction=+1) / LEFT (direction=-1). Per a TOGGLE/CYCLE/INT_RANGE
|
||||||
|
// crida on_change. Per a SUBMENU/ACTION nomes +1 (entra/activa).
|
||||||
|
void changeValue(int direction);
|
||||||
|
|
||||||
|
// Alçada objectiu de la caixa per a la pagina superior (sense animacio).
|
||||||
|
[[nodiscard]] auto computeTargetHeight() const -> float;
|
||||||
|
|
||||||
|
// Ample objectiu de la caixa per a la pagina superior (sense animacio).
|
||||||
|
// Pren com a base BOX_WIDTH_MIN i s'eixampla si algun text no hi cap.
|
||||||
|
[[nodiscard]] auto computeTargetWidth() const -> float;
|
||||||
|
|
||||||
|
// Y (top) de l'item index dins una caixa col·locada a box_y. Si la
|
||||||
|
// pagina te subtitol, els items es desplacen cap avall.
|
||||||
|
[[nodiscard]] static auto computeItemTopY(float box_y, std::size_t index, bool has_subtitle) -> float;
|
||||||
|
|
||||||
|
Rendering::Renderer* renderer_;
|
||||||
|
SDLManager* sdl_;
|
||||||
|
DebugOverlay* debug_overlay_;
|
||||||
|
Graphics::VectorText text_;
|
||||||
|
|
||||||
|
std::vector<Page> stack_;
|
||||||
|
bool open_ = false;
|
||||||
|
bool closing_ = false;
|
||||||
|
float open_anim_ = 0.0F; // 0..1 raw (sense easing)
|
||||||
|
float animated_h_ = 0.0F; // Alçada animada amb smoothing exponencial
|
||||||
|
float animated_w_ = 0.0F; // Ample animat (eixampla segons contingut)
|
||||||
|
|
||||||
|
// Estat del highlight (rectangle del cursor). Es lerpa cap a l'item
|
||||||
|
// actiu amb ease-out exponencial; quan el cursor "salta" (open o
|
||||||
|
// push/pop de pagina), s'enganxa directament al nou objectiu.
|
||||||
|
float highlight_y_ = 0.0F;
|
||||||
|
float highlight_h_ = 0.0F;
|
||||||
|
bool highlight_snap_ = true;
|
||||||
|
|
||||||
|
static std::unique_ptr<ServiceMenu> instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace System
|
||||||
@@ -20,6 +20,7 @@ namespace ConfigYaml {
|
|||||||
Config::PlayerBindings& player1 = engine_config.player1;
|
Config::PlayerBindings& player1 = engine_config.player1;
|
||||||
Config::PlayerBindings& player2 = engine_config.player2;
|
Config::PlayerBindings& player2 = engine_config.player2;
|
||||||
bool& console = engine_config.console;
|
bool& console = engine_config.console;
|
||||||
|
std::string& locale = engine_config.locale;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// ========== FUNCIONS AUXILIARS PER CONVERSIÓ DE CONTROLES ==========
|
// ========== FUNCIONS AUXILIARS PER CONVERSIÓ DE CONTROLES ==========
|
||||||
@@ -208,6 +209,9 @@ namespace ConfigYaml {
|
|||||||
rendering.render_width = Defaults::Rendering::RENDER_WIDTH_DEFAULT;
|
rendering.render_width = Defaults::Rendering::RENDER_WIDTH_DEFAULT;
|
||||||
rendering.render_height = Defaults::Rendering::RENDER_HEIGHT_DEFAULT;
|
rendering.render_height = Defaults::Rendering::RENDER_HEIGHT_DEFAULT;
|
||||||
|
|
||||||
|
// Idioma
|
||||||
|
locale = "ca";
|
||||||
|
|
||||||
// Version
|
// Version
|
||||||
version = std::string(Project::VERSION);
|
version = std::string(Project::VERSION);
|
||||||
}
|
}
|
||||||
@@ -446,6 +450,16 @@ namespace ConfigYaml {
|
|||||||
loadPlayer1ControlsFromYaml(yaml);
|
loadPlayer1ControlsFromYaml(yaml);
|
||||||
loadPlayer2ControlsFromYaml(yaml);
|
loadPlayer2ControlsFromYaml(yaml);
|
||||||
|
|
||||||
|
// Idioma (opcional; valors admesos: "ca" | "en")
|
||||||
|
if (yaml.contains("locale")) {
|
||||||
|
try {
|
||||||
|
auto val = yaml["locale"].get_value<std::string>();
|
||||||
|
locale = (val == "ca" || val == "en") ? val : "ca";
|
||||||
|
} catch (...) {
|
||||||
|
locale = "ca";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (console) {
|
if (console) {
|
||||||
std::cout << "Config carregada correctament desde: " << config_file_path
|
std::cout << "Config carregada correctament desde: " << config_file_path
|
||||||
<< '\n';
|
<< '\n';
|
||||||
@@ -532,6 +546,9 @@ namespace ConfigYaml {
|
|||||||
file << " render_height: " << rendering.render_height
|
file << " render_height: " << rendering.render_height
|
||||||
<< " # Parell amb render_width (720, 900, 1080, 1440, 2160)\n\n";
|
<< " # Parell amb render_width (720, 900, 1080, 1440, 2160)\n\n";
|
||||||
|
|
||||||
|
file << "# IDIOMA\n";
|
||||||
|
file << "locale: " << locale << " # ca | en\n\n";
|
||||||
|
|
||||||
// Guardar controls de jugadors
|
// Guardar controls de jugadors
|
||||||
savePlayer1ControlsToYaml(file);
|
savePlayer1ControlsToYaml(file);
|
||||||
savePlayer2ControlsToYaml(file);
|
savePlayer2ControlsToYaml(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
@@ -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
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,6 +189,17 @@ namespace Effects {
|
|||||||
.y = fw.head.y - (DIR_Y * fw.current_length),
|
.y = fw.head.y - (DIR_Y * fw.current_length),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (fw.glow) {
|
||||||
|
Rendering::lineaGlow(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,
|
||||||
|
fw.glow_color);
|
||||||
|
} else {
|
||||||
Rendering::linea(renderer_,
|
Rendering::linea(renderer_,
|
||||||
static_cast<int>(fw.head.x),
|
static_cast<int>(fw.head.x),
|
||||||
static_cast<int>(fw.head.y),
|
static_cast<int>(fw.head.y),
|
||||||
@@ -195,6 +210,7 @@ namespace Effects {
|
|||||||
fw.color);
|
fw.color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void FireworkManager::reset() {
|
void FireworkManager::reset() {
|
||||||
for (auto& fw : pool_) {
|
for (auto& fw : pool_) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ 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
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ void FloatingScoreManager::crear(int points, const Vec2& position) {
|
|||||||
pf->position = position;
|
pf->position = position;
|
||||||
pf->velocity = {.x = Defaults::FloatingScore::VELOCITY_X,
|
pf->velocity = {.x = Defaults::FloatingScore::VELOCITY_X,
|
||||||
.y = Defaults::FloatingScore::VELOCITY_Y};
|
.y = Defaults::FloatingScore::VELOCITY_Y};
|
||||||
pf->temps_vida = 0.0F;
|
pf->elapsed_time = 0.0F;
|
||||||
pf->temps_max = Defaults::FloatingScore::LIFETIME;
|
pf->max_lifetime = Defaults::FloatingScore::LIFETIME;
|
||||||
pf->brightness = 1.0F;
|
pf->brightness = 1.0F;
|
||||||
pf->active = true;
|
pf->active = true;
|
||||||
}
|
}
|
||||||
@@ -44,14 +44,14 @@ void FloatingScoreManager::update(float delta_time) {
|
|||||||
pf.position.y += pf.velocity.y * delta_time;
|
pf.position.y += pf.velocity.y * delta_time;
|
||||||
|
|
||||||
// 2. Actualitzar time de vida
|
// 2. Actualitzar time de vida
|
||||||
pf.temps_vida += delta_time;
|
pf.elapsed_time += delta_time;
|
||||||
|
|
||||||
// 3. Calcular brightness (fade lineal)
|
// 3. Calcular brightness (fade lineal)
|
||||||
float progress = pf.temps_vida / pf.temps_max; // 0.0 → 1.0
|
float progress = pf.elapsed_time / pf.max_lifetime; // 0.0 → 1.0
|
||||||
pf.brightness = 1.0F - progress; // 1.0 → 0.0
|
pf.brightness = 1.0F - progress; // 1.0 → 0.0
|
||||||
|
|
||||||
// 4. Desactivar cuando acaba el time
|
// 4. Desactivar cuando acaba el time
|
||||||
if (pf.temps_vida >= pf.temps_max) {
|
if (pf.elapsed_time >= pf.max_lifetime) {
|
||||||
pf.active = false;
|
pf.active = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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).
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class Bullet : public Entities::Entity {
|
|||||||
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;
|
||||||
@@ -30,19 +30,20 @@ class Bullet : public Entities::Entity {
|
|||||||
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.
|
||||||
|
[[nodiscard]] auto getPrevPosition() const -> const Vec2& { return prev_position_; }
|
||||||
void desactivar();
|
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
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
#include "core/input/input.hpp"
|
#include "core/input/input.hpp"
|
||||||
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/system/scene_context.hpp"
|
#include "core/system/scene_context.hpp"
|
||||||
|
#include "core/system/service_menu.hpp"
|
||||||
#include "game/stage_system/stage_loader.hpp"
|
#include "game/stage_system/stage_loader.hpp"
|
||||||
#include "game/systems/collision_system.hpp"
|
#include "game/systems/collision_system.hpp"
|
||||||
#include "game/systems/continue_system.hpp"
|
#include "game/systems/continue_system.hpp"
|
||||||
@@ -30,6 +32,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 +40,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 +63,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 +78,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 +132,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
|
||||||
@@ -180,6 +183,13 @@ void GameScene::handleEvent(const SDL_Event& event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::update(float delta_time) {
|
void GameScene::update(float delta_time) {
|
||||||
|
// Pausa global: mentre el menu de servei esta obert, congelem la lògica
|
||||||
|
// de joc. El draw() segueix executant-se per a mantenir l'escena visible
|
||||||
|
// sota el menu.
|
||||||
|
if (const auto* menu = System::ServiceMenu::get(); menu != nullptr && menu->isOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Orquestador delgado: cada paso vive en su propia función para
|
// Orquestador delgado: cada paso vive en su propia función para
|
||||||
// mantener update() legible y reducir complejidad cognitiva.
|
// mantener update() legible y reducir complejidad cognitiva.
|
||||||
stepPhysics(delta_time);
|
stepPhysics(delta_time);
|
||||||
@@ -213,24 +223,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 +276,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 +327,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 +353,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 +384,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 +449,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 +462,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 +471,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 +490,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 +499,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 +516,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 +525,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 +589,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 +609,7 @@ void GameScene::drawContinueState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::drawGameOverState() {
|
void GameScene::drawGameOverState() {
|
||||||
|
starfield_parallax_.draw();
|
||||||
border_.draw();
|
border_.draw();
|
||||||
drawEnemies();
|
drawEnemies();
|
||||||
drawBullets();
|
drawBullets();
|
||||||
@@ -576,15 +617,15 @@ void GameScene::drawGameOverState() {
|
|||||||
firework_manager_.draw();
|
firework_manager_.draw();
|
||||||
floating_score_manager_.draw();
|
floating_score_manager_.draw();
|
||||||
|
|
||||||
const std::string GAME_OVER_TEXT = "GAME OVER";
|
const std::string GAME_OVER_TEXT = Locale::get().text("game_screen.game_over");
|
||||||
constexpr float SCALE = Defaults::Game::GameOverScreen::TEXT_SCALE;
|
constexpr float SCALE = Defaults::Game::GameOverScreen::TEXT_SCALE;
|
||||||
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 +655,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 +672,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 +696,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 +710,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();
|
||||||
@@ -673,7 +719,7 @@ void GameScene::drawLevelCompletedState() {
|
|||||||
debris_manager_.draw();
|
debris_manager_.draw();
|
||||||
firework_manager_.draw();
|
firework_manager_.draw();
|
||||||
floating_score_manager_.draw();
|
floating_score_manager_.draw();
|
||||||
drawStageMessage(StageSystem::Constants::MISSATGE_LEVEL_COMPLETED);
|
drawStageMessage(Locale::get().text("stage.completed"));
|
||||||
drawScoreboard();
|
drawScoreboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,30 +732,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 +778,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 +810,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)
|
||||||
@@ -771,7 +823,7 @@ auto GameScene::buildScoreboard() const -> std::string {
|
|||||||
|
|
||||||
// Format: "123456 03 LEVEL 01 654321 02"
|
// Format: "123456 03 LEVEL 01 654321 02"
|
||||||
// Nota: dos espais entre seccions, mantenir ambdós slots siempre visibles
|
// Nota: dos espais entre seccions, mantenir ambdós slots siempre visibles
|
||||||
return score_p1 + " " + vides_p1 + " LEVEL " + stage_str + " " + score_p2 + " " + vides_p2;
|
return score_p1 + " " + vides_p1 + " " + Locale::get().text("hud.level") + stage_str + " " + score_p2 + " " + vides_p2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// [NEW] Stage system helper methods
|
// [NEW] Stage system helper methods
|
||||||
@@ -836,9 +888,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 +899,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 +913,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 +936,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -902,14 +955,14 @@ void GameScene::drawContinue() {
|
|||||||
constexpr float SPACING = 4.0F;
|
constexpr float SPACING = 4.0F;
|
||||||
|
|
||||||
// "CONTINUE" text (using constants)
|
// "CONTINUE" text (using constants)
|
||||||
const std::string CONTINUE_TEXT = "CONTINUE";
|
const std::string CONTINUE_TEXT = Locale::get().text("game_screen.continue");
|
||||||
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,26 +971,29 @@ 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) {
|
||||||
const std::string CONTINUES_TEXT = "CONTINUES LEFT: " + std::to_string(Defaults::Game::MAX_CONTINUES - continues_used_);
|
const std::string CONTINUES_TEXT = localeSubstitute(
|
||||||
|
Locale::get().text("game_screen.continues_left"),
|
||||||
|
"{n}",
|
||||||
|
std::to_string(Defaults::Game::MAX_CONTINUES - continues_used_));
|
||||||
float escala_info = Defaults::Game::ContinueScreen::INFO_TEXT_SCALE;
|
float escala_info = Defaults::Game::ContinueScreen::INFO_TEXT_SCALE;
|
||||||
float y_ratio_info = Defaults::Game::ContinueScreen::INFO_TEXT_Y_RATIO;
|
float y_ratio_info = Defaults::Game::ContinueScreen::INFO_TEXT_Y_RATIO;
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,46 +42,46 @@ class LogoScene final : public Scene {
|
|||||||
|
|
||||||
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();
|
||||||
|
|||||||
+341
-409
File diff suppressed because it is too large
Load Diff
@@ -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, // Fade-in del starfield (3.0s)
|
STARFIELD_FADE_IN,
|
||||||
STARFIELD, // Pantalla con camp de estrelles (4.0s)
|
STARFIELD,
|
||||||
MAIN, // Pantalla de título con text (indefinit, hasta START)
|
MAIN,
|
||||||
PLAYER_JOIN_PHASE, // Fase de unió de jugadors: fade-out música + text parpellejant (2.5s)
|
PLAYER_JOIN_PHASE,
|
||||||
BLACK_SCREEN // Pantalla negra de transición (2.0s)
|
BLACK_SCREEN,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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 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 DURATION_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;
|
static auto checkSkipButtonPressed() -> bool;
|
||||||
auto checkStartGameButtonPressed() -> bool;
|
auto checkStartGameButtonPressed() -> bool;
|
||||||
void initTitle(); // Carrega i posiciona las lletres del título
|
void initTitle();
|
||||||
void inicialitzarJailgames(); // Carrega i posiciona el logo JAILGAMES pequeño
|
void inicialitzarJailgames();
|
||||||
void dibuixarPeuTitol(float spacing) const; // Logo JAILGAMES + línia de copyright
|
void dibuixarPeuTitol(float spacing) const;
|
||||||
|
|
||||||
// Sub-pasos de update() (extreure cada state per reduir complexitat).
|
|
||||||
void updateStarfieldFadeInState(float delta_time);
|
void updateStarfieldFadeInState(float delta_time);
|
||||||
void updateStarfieldState(float delta_time);
|
void updateStarfieldState(float delta_time);
|
||||||
void updateMainState(float delta_time);
|
void updateMainState(float delta_time);
|
||||||
void updatePlayerJoinPhaseState(float delta_time);
|
void updatePlayerJoinPhaseState(float delta_time);
|
||||||
void updateBlackScreenState(float delta_time);
|
void updateBlackScreenState(float delta_time);
|
||||||
// Handlers de input globals (independents de l'state actual).
|
|
||||||
void handleSkipInput();
|
void handleSkipInput();
|
||||||
void handleStartInput();
|
void handleStartInput();
|
||||||
// Helper compartit: dispara l'animación de salida per las naves del player que
|
void triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active, const char* log_prefix);
|
||||||
// acaba de fer un join "en aquest frame" (jugadorX_actiu == true && !prev).
|
|
||||||
void triggerExitForJoinedPlayers(bool p1_was_active, bool p2_was_active,
|
|
||||||
const char* log_prefix);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
};
|
|
||||||
@@ -134,9 +134,9 @@ auto SpawnController::selectRandomType() const -> EnemyType {
|
|||||||
return EnemyType::PENTAGON;
|
return EnemyType::PENTAGON;
|
||||||
}
|
}
|
||||||
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado) {
|
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado) {
|
||||||
return EnemyType::QUADRAT;
|
return EnemyType::SQUARE;
|
||||||
}
|
}
|
||||||
return EnemyType::MOLINILLO;
|
return EnemyType::PINWHEEL;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SpawnController::spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos) {
|
void SpawnController::spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos) {
|
||||||
@@ -160,7 +160,7 @@ void SpawnController::applyMultipliers(Enemy& enemy) const {
|
|||||||
float base_rot = enemy.getBaseRotation();
|
float base_rot = enemy.getBaseRotation();
|
||||||
enemy.setRotation(base_rot * config_->multiplicadors.rotation);
|
enemy.setRotation(base_rot * config_->multiplicadors.rotation);
|
||||||
|
|
||||||
// Apply tracking strength (only affects QUADRAT)
|
// Apply tracking strength (only affects SQUARE)
|
||||||
enemy.setTrackingStrength(config_->multiplicadors.tracking_strength);
|
enemy.setTrackingStrength(config_->multiplicadors.tracking_strength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -36,7 +35,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
|
||||||
@@ -78,24 +77,4 @@ namespace StageSystem {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Constants per messages de transición
|
|
||||||
namespace Constants {
|
|
||||||
// Pool de messages per start de level (selecció aleatòria)
|
|
||||||
inline constexpr std::array<const char*, 12> MISSATGES_LEVEL_START = {
|
|
||||||
"ORNI ALERT!",
|
|
||||||
"INCOMING ORNIS!",
|
|
||||||
"ROLLING THREAT!",
|
|
||||||
"ENEMY WAVE!",
|
|
||||||
"WAVE OF ORNIS DETECTED!",
|
|
||||||
"NEXT SWARM APPROACHING!",
|
|
||||||
"BRACE FOR THE NEXT WAVE!",
|
|
||||||
"ANOTHER ATTACK INCOMING!",
|
|
||||||
"SENSORS DETECT HOSTILE ORNIS...",
|
|
||||||
"UNIDENTIFIED ROLLING OBJECTS INBOUND!",
|
|
||||||
"ENEMY FORCES MOBILIZING!",
|
|
||||||
"PREPARE FOR IMPACT!"};
|
|
||||||
|
|
||||||
constexpr const char* MISSATGE_LEVEL_COMPLETED = "GOOD JOB COMMANDER!";
|
|
||||||
} // namespace Constants
|
|
||||||
|
|
||||||
} // namespace StageSystem
|
} // namespace StageSystem
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
#include "core/defaults.hpp"
|
#include "core/defaults.hpp"
|
||||||
|
#include "core/locale/locale.hpp"
|
||||||
#include "stage_config.hpp"
|
#include "stage_config.hpp"
|
||||||
|
|
||||||
namespace StageSystem {
|
namespace StageSystem {
|
||||||
|
|
||||||
StageManager::StageManager(const StageSystemConfig* config)
|
StageManager::StageManager(const StageSystemConfig* config)
|
||||||
: config_(config)
|
: config_(config) {
|
||||||
{
|
|
||||||
if (config_ == nullptr) {
|
if (config_ == nullptr) {
|
||||||
std::cerr << "[StageManager] Error: config es null" << '\n';
|
std::cerr << "[StageManager] Error: config es null" << '\n';
|
||||||
}
|
}
|
||||||
@@ -81,8 +81,9 @@ void StageManager::changeState(EstatStage nou_estat) {
|
|||||||
|
|
||||||
// Select random message when entering LEVEL_START
|
// Select random message when entering LEVEL_START
|
||||||
if (nou_estat == EstatStage::LEVEL_START) {
|
if (nou_estat == EstatStage::LEVEL_START) {
|
||||||
size_t index = static_cast<size_t>(std::rand()) % Constants::MISSATGES_LEVEL_START.size();
|
const std::size_t POOL = Locale::get().count("stage.start");
|
||||||
missatge_level_start_actual_ = Constants::MISSATGES_LEVEL_START[index];
|
const std::size_t INDEX = (POOL == 0) ? 0 : static_cast<std::size_t>(std::rand()) % POOL;
|
||||||
|
missatge_level_start_actual_ = Locale::get().text("stage.start." + std::to_string(INDEX));
|
||||||
|
|
||||||
// [NOU] Iniciar música al entrar en LEVEL_START (después de INIT_HUD)
|
// [NOU] Iniciar música al entrar en LEVEL_START (después de INIT_HUD)
|
||||||
// Solo si no está sonant ya (per evitar reset en loops posteriors)
|
// Solo si no está sonant ya (per evitar reset en loops posteriors)
|
||||||
@@ -126,7 +127,6 @@ void StageManager::processLevelStart(float delta_time) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void StageManager::processPlaying(float delta_time, bool pause_spawn) {
|
void StageManager::processPlaying(float delta_time, bool pause_spawn) {
|
||||||
|
|
||||||
// Update spawn controller (pauses when pause_spawn = true)
|
// Update spawn controller (pauses when pause_spawn = true)
|
||||||
// Note: The actual enemy array update happens in GameScene::update()
|
// Note: The actual enemy array update happens in GameScene::update()
|
||||||
// This is just for internal timekeeping
|
// This is just for internal timekeeping
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
touched_enemy = &enemy;
|
||||||
|
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);
|
ctx.on_player_hit(i);
|
||||||
break; // Solo una colisión por player por frame
|
} 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.ships[player_id].getBody().applyImpulse(BULLET_IMPULSE);
|
||||||
ctx.on_player_hit(player_id);
|
ctx.on_player_hit(player_id);
|
||||||
ctx.lives_per_player[BULLET_OWNER]++;
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ void revivePlayer(Context& ctx, uint8_t player_id) {
|
|||||||
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);
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+219
-190
@@ -1,45 +1,140 @@
|
|||||||
// 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) {
|
|
||||||
|
// Profunditat d'extrusió de la silueta 2D de la nau (en unitats mundials).
|
||||||
|
// 0.0F → emet només la silueta plana. >0 emet volum extrudit.
|
||||||
|
constexpr float SHIP_EXTRUSION_DEPTH = 1.0F;
|
||||||
|
|
||||||
|
// VP lògic per definir forward_dir / direcció del path. Tots els paths
|
||||||
|
// s'allunyen cap a aquest punt; les naus exiting continuen MÉS ENLLÀ
|
||||||
|
// (vegeu SHIP_EXIT_TRAVEL) per no desaparèixer en arribar al VP.
|
||||||
|
constexpr float SHIP_EXIT_Z = 800.0F;
|
||||||
|
constexpr Vec3 VANISHING_POINT{.x = 0.0F, .y = 0.0F, .z = SHIP_EXIT_Z};
|
||||||
|
|
||||||
|
// Profunditat addicional darrere del VP cap a la qual les naus exiting
|
||||||
|
// convergeixen. Així P1 (X<0) i P2 (X>0) mantenen sempre els seus
|
||||||
|
// hemisferis i no es creuen al passar pel VP — totes dues acaben al
|
||||||
|
// centre projectat (640, 360) sense travessar-lo.
|
||||||
|
constexpr float SHIP_EXIT_OVERFLOW = 700.0F;
|
||||||
|
|
||||||
|
// Directors VP → origen de cada nau, normalitzats. P1 ve des de "les 7"
|
||||||
|
// del rellotge (baix-esquerra), P2 des de "les 5" (baix-dreta). Els
|
||||||
|
// components estan calibrats perquè a TARGET_DIST el pixel projectat
|
||||||
|
// caigui aprox sota la "P de PRESS" / "Y de PLAY" del text del títol.
|
||||||
|
constexpr Vec3 PATH_DIR_P1{.x = -0.0887F, .y = -0.0683F, .z = -0.9938F};
|
||||||
|
constexpr Vec3 PATH_DIR_P2{.x = +0.0887F, .y = -0.0683F, .z = -0.9938F};
|
||||||
|
|
||||||
|
// Distàncies des del VP al llarg del path (unitats mundials).
|
||||||
|
// Reduïm TARGET_DIST per acostar el descans al VP (puja en pantalla,
|
||||||
|
// s'allunya de PRESS START); compensem amb SHIP_FLOAT_SCALE més gran.
|
||||||
|
constexpr float TARGET_DIST = 480.0F; // Descans a Z≈323 → pixel ≈ (558, 423)
|
||||||
|
constexpr float ENTRY_DIST = 770.0F; // Inicial a Z≈35 → fora pantalla baix-esq.
|
||||||
|
|
||||||
|
// Pitch addicional sobre el look-at pur per fer que el dors de la nau
|
||||||
|
// s'incline cap a la càmera (~-14° afegits). Amb forward quasi paral·lel
|
||||||
|
// a +Z, el pitch look-at és ~-94°; afegint això queda al voltant de -108°,
|
||||||
|
// que és l'angle visualment validat com a "bo" per l'usuari.
|
||||||
|
constexpr float PITCH_LIFT_RAD = -0.25F;
|
||||||
|
|
||||||
|
// Look-at: calcula pitch+yaw que duen (0,-1,0) local a forward_dir mundial.
|
||||||
|
// Requereix l'ordre de rotació X→Y→Z al applyTransform de wireframe3d.
|
||||||
|
// Si forward és quasi vertical (sin(pitch) ≈ 0), retorna yaw=0 (qualsevol).
|
||||||
|
auto computePitchYawForLookAt(const Vec3& forward_dir) -> Vec2 {
|
||||||
|
const float DY = std::clamp(forward_dir.y, -1.0F, 1.0F);
|
||||||
|
const float PITCH_LOOKAT = -std::acos(-DY); // ∈ [-π, 0]
|
||||||
|
const float SIN_PITCH = std::sin(PITCH_LOOKAT);
|
||||||
|
float yaw = 0.0F;
|
||||||
|
if (std::abs(SIN_PITCH) >= 1.0E-5F) {
|
||||||
|
const float SY = -forward_dir.x / SIN_PITCH;
|
||||||
|
const float CY = -forward_dir.z / SIN_PITCH;
|
||||||
|
yaw = std::atan2(SY, CY);
|
||||||
|
}
|
||||||
|
return Vec2{.x = PITCH_LOOKAT + PITCH_LIFT_RAD, .y = yaw};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto safeNormalize(const Vec3& v, const Vec3& fallback) -> Vec3 {
|
||||||
|
return v.lengthSquared() > 0.0F ? v.normalized() : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto entryForward(const TitleShip& ship) -> Vec3 {
|
||||||
|
return safeNormalize(ship.target_position - ship.initial_position,
|
||||||
|
Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto floatingForward(const Vec3& target) -> Vec3 {
|
||||||
|
return safeNormalize(VANISHING_POINT - target,
|
||||||
|
Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto exitForward(const Vec3& current) -> Vec3 {
|
||||||
|
return safeNormalize(VANISHING_POINT - current,
|
||||||
|
Vec3{.x = 0.0F, .y = 0.0F, .z = 1.0F});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mida visual i animació.
|
||||||
|
constexpr float SHIP_FLOAT_SCALE = 2.0F;
|
||||||
|
constexpr float SHIP_ENTRY_SCALE = 2.0F; // Mida mundial idèntica; la perspectiva fa la resta
|
||||||
|
// 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::init() {
|
void ShipAnimator::init() {
|
||||||
// Carregar formes de naves con perspectiva pre-calculada
|
auto shape_p1 = Graphics::ShapeLoader::load("ship.shp");
|
||||||
auto forma_p1 = Graphics::ShapeLoader::load("ship_perspective.shp"); // Perspectiva izquierda
|
auto shape_p2 = Graphics::ShapeLoader::load("ship2.shp");
|
||||||
auto forma_p2 = Graphics::ShapeLoader::load("ship2_perspective.shp"); // Perspectiva derecha
|
|
||||||
|
|
||||||
// Configurar ship P1
|
|
||||||
ships_[0].player_id = 1;
|
ships_[0].player_id = 1;
|
||||||
ships_[0].shape = forma_p1;
|
if (shape_p1 && shape_p1->isValid()) {
|
||||||
|
ships_[0].mesh = Graphics::extrudeShape2D(*shape_p1, SHIP_EXTRUSION_DEPTH);
|
||||||
|
}
|
||||||
configureShipP1(ships_[0]);
|
configureShipP1(ships_[0]);
|
||||||
|
|
||||||
// Configurar ship P2
|
|
||||||
ships_[1].player_id = 2;
|
ships_[1].player_id = 2;
|
||||||
ships_[1].shape = forma_p2;
|
if (shape_p2 && shape_p2->isValid()) {
|
||||||
|
ships_[1].mesh = Graphics::extrudeShape2D(*shape_p2, SHIP_EXTRUSION_DEPTH);
|
||||||
|
}
|
||||||
configureShipP2(ships_[1]);
|
configureShipP2(ships_[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShipAnimator::update(float delta_time) {
|
void ShipAnimator::update(float delta_time) {
|
||||||
// Dispatcher segons state de cada ship
|
|
||||||
for (auto& ship : ships_) {
|
for (auto& ship : ships_) {
|
||||||
if (!ship.visible) {
|
if (!ship.visible) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (ship.state) {
|
switch (ship.state) {
|
||||||
case ShipState::ENTERING:
|
case ShipState::ENTERING:
|
||||||
updateEntering(ship, delta_time);
|
updateEntering(ship, delta_time);
|
||||||
@@ -47,101 +142,79 @@ void ShipAnimator::update(float delta_time) {
|
|||||||
case ShipState::FLOATING:
|
case ShipState::FLOATING:
|
||||||
updateFloating(ship, delta_time);
|
updateFloating(ship, delta_time);
|
||||||
break;
|
break;
|
||||||
case ShipState::EXITING:
|
case ShipState::EXITING: {
|
||||||
updateExiting(ship, delta_time);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ShipAnimator::draw() const {
|
void ShipAnimator::draw() const {
|
||||||
for (const auto& ship : ships_) {
|
if (camera_ == nullptr || renderer_ == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < ships_.size(); ++i) {
|
||||||
|
const auto& ship = ships_[i];
|
||||||
if (!ship.visible) {
|
if (!ship.visible) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const Vec2 EULER = computePitchYawForLookAt(ship.forward_dir);
|
||||||
// Renderizar ship (perspectiva ya incorporada a la shape)
|
const Graphics::Transform3D TRANSFORM{
|
||||||
Rendering::renderShape(
|
.position = ship.current_position,
|
||||||
renderer_,
|
.rotation_euler = Vec3{.x = EULER.x, .y = EULER.y, .z = 0.0F},
|
||||||
ship.shape,
|
.scale = ship.current_scale,
|
||||||
ship.current_position,
|
};
|
||||||
0.0F, // angle (rotación 2D no utilitzada)
|
const SDL_Color SHIP_COLOR = (i == 0)
|
||||||
ship.current_scale,
|
? Defaults::Title::Colors::SHIP_P1
|
||||||
1.0F, // progress (siempre visible)
|
: Defaults::Title::Colors::SHIP_P2;
|
||||||
1.0F // brightness (brightness màxima)
|
Graphics::drawWireframe(renderer_, *camera_, ship.mesh, TRANSFORM, 1.0F, SHIP_COLOR);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShipAnimator::startEntryAnimation() {
|
void ShipAnimator::startEntryAnimation() {
|
||||||
using namespace Defaults::Title::Ships;
|
for (auto& ship : ships_) {
|
||||||
|
ship.state = ShipState::ENTERING;
|
||||||
// Configurar ship P1 para l'animación de entrada
|
ship.state_time = 0.0F;
|
||||||
ships_[0].state = ShipState::ENTERING;
|
ship.current_position = ship.initial_position;
|
||||||
ships_[0].state_time = 0.0F;
|
ship.current_scale = ship.initial_scale;
|
||||||
ships_[0].initial_position = computeOffscreenPosition(CLOCK_8_ANGLE);
|
ship.forward_dir = entryForward(ship);
|
||||||
ships_[0].current_position = ships_[0].initial_position;
|
}
|
||||||
ships_[0].current_scale = ships_[0].initial_scale;
|
|
||||||
|
|
||||||
// Configurar ship P2 para l'animación de entrada
|
|
||||||
ships_[1].state = ShipState::ENTERING;
|
|
||||||
ships_[1].state_time = 0.0F;
|
|
||||||
ships_[1].initial_position = computeOffscreenPosition(CLOCK_4_ANGLE);
|
|
||||||
ships_[1].current_position = ships_[1].initial_position;
|
|
||||||
ships_[1].current_scale = ships_[1].initial_scale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShipAnimator::triggerExitAnimation() {
|
void ShipAnimator::triggerExitAnimation() {
|
||||||
// Configurar ambdues naves para l'animación de salida
|
|
||||||
for (auto& ship : ships_) {
|
for (auto& ship : ships_) {
|
||||||
// Canviar state a EXITING
|
|
||||||
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ó
|
void ShipAnimator::triggerExitAnimationForPlayer(int player_id) {
|
||||||
// (current_scale pot ser diferent si está en ENTERING)
|
for (auto& ship : ships_) {
|
||||||
|
if (ship.player_id == player_id) {
|
||||||
|
ship.state = ShipState::EXITING;
|
||||||
|
ship.state_time = 0.0F;
|
||||||
|
ship.initial_position = ship.current_position;
|
||||||
|
ship.forward_dir = exitForward(ship.current_position);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShipAnimator::skipToFloatingState() {
|
void ShipAnimator::skipToFloatingState() {
|
||||||
// Posar ambdues naves directament en state FLOATING
|
|
||||||
for (auto& ship : ships_) {
|
for (auto& ship : ships_) {
|
||||||
ship.state = ShipState::FLOATING;
|
ship.state = ShipState::FLOATING;
|
||||||
ship.state_time = 0.0F;
|
ship.state_time = 0.0F;
|
||||||
ship.oscillation_phase = 0.0F;
|
ship.oscillation_phase = 0.0F;
|
||||||
|
|
||||||
// Posar en posición objetivo (sin animación)
|
|
||||||
ship.current_position = ship.target_position;
|
ship.current_position = ship.target_position;
|
||||||
ship.current_scale = ship.target_scale;
|
ship.current_scale = ship.target_scale;
|
||||||
|
ship.forward_dir = floatingForward(ship.target_position);
|
||||||
// NO establir visibilitat aquí - ya ho hace el caller
|
|
||||||
// (evita fer visibles ambdues naves cuando solo una ha premut START)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto ShipAnimator::isVisible() const -> bool {
|
|
||||||
// Retorna true si almenys una ship es visible
|
|
||||||
return std::ranges::any_of(ships_, [](const TitleShip& ship) { return ship.visible; });
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShipAnimator::triggerExitAnimationForPlayer(int player_id) {
|
|
||||||
// Trobar la ship del player especificat
|
|
||||||
for (auto& ship : ships_) {
|
|
||||||
if (ship.player_id == player_id) {
|
|
||||||
// Canviar state a EXITING solo per esta ship
|
|
||||||
ship.state = ShipState::EXITING;
|
|
||||||
ship.state_time = 0.0F;
|
|
||||||
|
|
||||||
// Preservar posición actual (pot estar a mig camí si START es prem durante ENTERING)
|
|
||||||
ship.initial_position = ship.current_position;
|
|
||||||
|
|
||||||
// La scale objetivo es preserva para calcular la interpolació
|
|
||||||
// (current_scale pot ser diferent si está en ENTERING)
|
|
||||||
break; // Solo una ship per player
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,177 +224,133 @@ void ShipAnimator::setVisible(bool visible) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ShipAnimator::isAnimationComplete() const -> bool {
|
auto ShipAnimator::isVisible() const -> bool {
|
||||||
// Comprovar si todas las naves són invisibles (han completat l'animación de salida)
|
return std::ranges::any_of(ships_,
|
||||||
return std::ranges::all_of(ships_, [](const TitleShip& ship) { return !ship.visible; });
|
[](const TitleShip& s) { return s.visible; });
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ShipAnimator::isAnimationComplete() const -> bool {
|
||||||
|
return std::ranges::all_of(ships_,
|
||||||
|
[](const TitleShip& s) { return !s.visible; });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Métodos de animación (stubs)
|
|
||||||
void ShipAnimator::updateEntering(TitleShip& ship, float delta_time) {
|
void ShipAnimator::updateEntering(TitleShip& ship, float delta_time) {
|
||||||
using namespace Defaults::Title::Ships;
|
|
||||||
|
|
||||||
ship.state_time += delta_time;
|
ship.state_time += delta_time;
|
||||||
|
|
||||||
// Esperar al delay antes de començar l'animación
|
|
||||||
if (ship.state_time < ship.entry_delay) {
|
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_position = ship.initial_position;
|
||||||
ship.current_scale = ship.initial_scale;
|
ship.current_scale = ship.initial_scale;
|
||||||
return;
|
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);
|
||||||
|
|
||||||
// Cálculo del progrés (restant el delay)
|
// Acumula la fase d'oscil·lació també durant ENTERING; sense això,
|
||||||
float elapsed = ship.state_time - ship.entry_delay;
|
// al passar a FLOATING la posició salta d'amplitud_y de cop perquè
|
||||||
float progress = std::min(1.0F, elapsed / ENTRY_DURATION);
|
// 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;
|
||||||
|
|
||||||
// Aplicar easing (ease_out_quad per arribada suau)
|
const float INTERP_X = Easing::lerp(ship.initial_position.x, ship.target_position.x, EASED);
|
||||||
float eased_progress = Easing::easeOutQuad(progress);
|
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);
|
||||||
|
|
||||||
// Lerp posición (inicial → objetivo)
|
const float TWO_PI = 2.0F * Defaults::Math::PI;
|
||||||
ship.current_position.x = Easing::lerp(ship.initial_position.x, ship.target_position.x, eased_progress);
|
const float OFFSET_X = ship.amplitude_x *
|
||||||
ship.current_position.y = Easing::lerp(ship.initial_position.y, ship.target_position.y, eased_progress);
|
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);
|
||||||
|
|
||||||
// Lerp scale (grande → normal)
|
ship.current_position.x = INTERP_X + OFFSET_X;
|
||||||
ship.current_scale = Easing::lerp(ship.initial_scale, ship.target_scale, eased_progress);
|
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);
|
||||||
|
|
||||||
// Transicionar a FLOATING cuando completi
|
if (ELAPSED >= ENTRY_DURATION) {
|
||||||
if (elapsed >= ENTRY_DURATION) {
|
|
||||||
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ó
|
// 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) {
|
void ShipAnimator::updateFloating(TitleShip& ship, float delta_time) {
|
||||||
using namespace Defaults::Title::Ships;
|
|
||||||
|
|
||||||
// Actualitzar time i fase de oscil·lació
|
|
||||||
ship.state_time += delta_time;
|
ship.state_time += delta_time;
|
||||||
ship.oscillation_phase += delta_time;
|
ship.oscillation_phase += delta_time;
|
||||||
|
|
||||||
// Oscil·lació sinusoïdal X/Y (parámetros específics per ship)
|
const float TWO_PI = 2.0F * Defaults::Math::PI;
|
||||||
float offset_x = ship.amplitude_x * std::sin(2.0F * Defaults::Math::PI * ship.frequency_x * ship.oscillation_phase);
|
const float OFFSET_X = ship.amplitude_x *
|
||||||
float offset_y = ship.amplitude_y * std::sin((2.0F * Defaults::Math::PI * ship.frequency_y * ship.oscillation_phase) + FLOAT_PHASE_OFFSET);
|
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);
|
||||||
|
|
||||||
// Aplicar oscil·lació a la posición objetivo
|
ship.current_position.x = ship.target_position.x + OFFSET_X;
|
||||||
ship.current_position.x = ship.target_position.x + offset_x;
|
ship.current_position.y = ship.target_position.y + OFFSET_Y;
|
||||||
ship.current_position.y = ship.target_position.y + offset_y;
|
ship.current_position.z = ship.target_position.z;
|
||||||
|
|
||||||
// Escala constant (sin "breathing" per ara)
|
|
||||||
ship.current_scale = ship.target_scale;
|
ship.current_scale = ship.target_scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShipAnimator::updateExiting(TitleShip& ship, float delta_time) {
|
void ShipAnimator::updateExiting(TitleShip& ship, float delta_time) {
|
||||||
using namespace Defaults::Title::Ships;
|
|
||||||
|
|
||||||
ship.state_time += 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);
|
||||||
|
|
||||||
// Calcular progrés (0.0 → 1.0)
|
// Destí: punt fix a (VP.x, VP.y, VP.z + OVERFLOW). Cada nau s'apropa
|
||||||
float progress = std::min(1.0F, ship.state_time / EXIT_DURATION);
|
// 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
|
||||||
|
|
||||||
// Aplicar easing (ease_in_quad per aceleración hacia el point de fuga)
|
if (PROGRESS >= 1.0F) {
|
||||||
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;
|
ship.visible = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuración
|
|
||||||
void ShipAnimator::configureShipP1(TitleShip& ship) {
|
void ShipAnimator::configureShipP1(TitleShip& ship) {
|
||||||
using namespace Defaults::Title::Ships;
|
|
||||||
|
|
||||||
// Estat inicial: FLOATING (per test estàtic)
|
|
||||||
ship.state = ShipState::FLOATING;
|
ship.state = ShipState::FLOATING;
|
||||||
ship.state_time = 0.0F;
|
ship.state_time = 0.0F;
|
||||||
|
// Target i initial sobre el path VP → "les 7" del rellotge (P1).
|
||||||
// Posicions (clock 8, bottom-left)
|
ship.target_position = VANISHING_POINT + (PATH_DIR_P1 * TARGET_DIST);
|
||||||
ship.target_position = {.x = p1TargetX(), .y = p1TargetY()};
|
ship.initial_position = VANISHING_POINT + (PATH_DIR_P1 * ENTRY_DIST);
|
||||||
|
ship.current_position = ship.initial_position;
|
||||||
// Calcular posición inicial (fuera de pantalla)
|
ship.target_scale = SHIP_FLOAT_SCALE;
|
||||||
ship.initial_position = computeOffscreenPosition(CLOCK_8_ANGLE);
|
ship.current_scale = SHIP_FLOAT_SCALE;
|
||||||
ship.current_position = ship.initial_position; // Començar fuera de pantalla
|
ship.initial_scale = SHIP_ENTRY_SCALE;
|
||||||
|
|
||||||
// Escales
|
|
||||||
ship.target_scale = FLOATING_SCALE;
|
|
||||||
ship.current_scale = FLOATING_SCALE;
|
|
||||||
ship.initial_scale = ENTRY_SCALE_START;
|
|
||||||
|
|
||||||
// Flotació
|
|
||||||
ship.oscillation_phase = 0.0F;
|
ship.oscillation_phase = 0.0F;
|
||||||
|
|
||||||
// Parámetros de entrada
|
|
||||||
ship.entry_delay = P1_ENTRY_DELAY;
|
ship.entry_delay = P1_ENTRY_DELAY;
|
||||||
|
|
||||||
// Parámetros de oscil·lació específics P1
|
|
||||||
ship.amplitude_x = FLOAT_AMPLITUDE_X;
|
ship.amplitude_x = FLOAT_AMPLITUDE_X;
|
||||||
ship.amplitude_y = FLOAT_AMPLITUDE_Y;
|
ship.amplitude_y = FLOAT_AMPLITUDE_Y;
|
||||||
ship.frequency_x = FLOAT_FREQUENCY_X_BASE * P1_FREQUENCY_MULTIPLIER;
|
ship.frequency_x = FLOAT_FREQUENCY_X_BASE * P1_FREQUENCY_MULTIPLIER;
|
||||||
ship.frequency_y = FLOAT_FREQUENCY_Y_BASE * P1_FREQUENCY_MULTIPLIER;
|
ship.frequency_y = FLOAT_FREQUENCY_Y_BASE * P1_FREQUENCY_MULTIPLIER;
|
||||||
|
ship.forward_dir = entryForward(ship);
|
||||||
// Visibilitat
|
|
||||||
ship.visible = true;
|
ship.visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShipAnimator::configureShipP2(TitleShip& ship) {
|
void ShipAnimator::configureShipP2(TitleShip& ship) {
|
||||||
using namespace Defaults::Title::Ships;
|
|
||||||
|
|
||||||
// Estat inicial: FLOATING (per test estàtic)
|
|
||||||
ship.state = ShipState::FLOATING;
|
ship.state = ShipState::FLOATING;
|
||||||
ship.state_time = 0.0F;
|
ship.state_time = 0.0F;
|
||||||
|
// Target i initial sobre el path VP → "les 5" del rellotge (P2).
|
||||||
// Posicions (clock 4, bottom-right)
|
ship.target_position = VANISHING_POINT + (PATH_DIR_P2 * TARGET_DIST);
|
||||||
ship.target_position = {.x = p2TargetX(), .y = p2TargetY()};
|
ship.initial_position = VANISHING_POINT + (PATH_DIR_P2 * ENTRY_DIST);
|
||||||
|
ship.current_position = ship.initial_position;
|
||||||
// Calcular posición inicial (fuera de pantalla)
|
ship.target_scale = SHIP_FLOAT_SCALE;
|
||||||
ship.initial_position = computeOffscreenPosition(CLOCK_4_ANGLE);
|
ship.current_scale = SHIP_FLOAT_SCALE;
|
||||||
ship.current_position = ship.initial_position; // Començar fuera de pantalla
|
ship.initial_scale = SHIP_ENTRY_SCALE;
|
||||||
|
|
||||||
// Escales
|
|
||||||
ship.target_scale = FLOATING_SCALE;
|
|
||||||
ship.current_scale = FLOATING_SCALE;
|
|
||||||
ship.initial_scale = ENTRY_SCALE_START;
|
|
||||||
|
|
||||||
// Flotació
|
|
||||||
ship.oscillation_phase = 0.0F;
|
ship.oscillation_phase = 0.0F;
|
||||||
|
|
||||||
// Parámetros de entrada
|
|
||||||
ship.entry_delay = P2_ENTRY_DELAY;
|
ship.entry_delay = P2_ENTRY_DELAY;
|
||||||
|
|
||||||
// Parámetros de oscil·lació específics P2
|
|
||||||
ship.amplitude_x = FLOAT_AMPLITUDE_X;
|
ship.amplitude_x = FLOAT_AMPLITUDE_X;
|
||||||
ship.amplitude_y = FLOAT_AMPLITUDE_Y;
|
ship.amplitude_y = FLOAT_AMPLITUDE_Y;
|
||||||
ship.frequency_x = FLOAT_FREQUENCY_X_BASE * P2_FREQUENCY_MULTIPLIER;
|
ship.frequency_x = FLOAT_FREQUENCY_X_BASE * P2_FREQUENCY_MULTIPLIER;
|
||||||
ship.frequency_y = FLOAT_FREQUENCY_Y_BASE * P2_FREQUENCY_MULTIPLIER;
|
ship.frequency_y = FLOAT_FREQUENCY_Y_BASE * P2_FREQUENCY_MULTIPLIER;
|
||||||
|
ship.forward_dir = entryForward(ship);
|
||||||
// Visibilitat
|
|
||||||
ship.visible = true;
|
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
|
||||||
|
|||||||
@@ -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, // Entrant desde fuera de pantalla
|
ENTERING,
|
||||||
FLOATING, // Flotante en posición estàtica
|
FLOATING,
|
||||||
EXITING // Volant hacia el point de fuga
|
EXITING,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Dades de una ship individual al título.
|
|
||||||
// Todos los miembros tienen inicializador por defecto: ShipAnimator::ships_
|
|
||||||
// es un std::array<TitleShip, 2> y sin estos defaults los campos primitivos
|
|
||||||
// quedarían indeterminados al instanciar el animador.
|
|
||||||
struct TitleShip {
|
struct TitleShip {
|
||||||
// Identificació
|
int player_id{0};
|
||||||
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:
|
||||||
explicit ShipAnimator(Rendering::Renderer* renderer);
|
ShipAnimator(Rendering::Renderer* renderer, const Graphics::Camera3D* camera);
|
||||||
|
|
||||||
// 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;
|
||||||
|
|
||||||
|
// 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:
|
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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user