Compare commits

...

114 Commits

Author SHA1 Message Date
JailDesigner 817c8fc8a0 merge fix/neteja-warnings: neteja de warnings 2026-05-31 00:20:35 +02:00
JailDesigner 3fe8fa9b32 fix: silencia -Wtautological-compare de stb_vorbis al cmake 2026-05-31 00:20:35 +02:00
JailDesigner 65f710bf7a chore(release): actualitza icones noves de l'aplicació 2026-05-30 23:20:01 +02:00
JailDesigner 72302554ae fix(release): apuja el target de macOS a 13.3 per std::format 2026-05-30 17:15:03 +02:00
JailDesigner 03530d0439 chore(release): actualitza icones de l'aplicació 2026-05-30 15:27:29 +02:00
JailDesigner 705d32e919 Merge branch 'feat/captures-pantalla': captura de pantalla amb F9 (PNG amb shaders) i fix de text 2026-05-30 11:19:58 +02:00
JailDesigner e420db2896 fix(text): fallback de minúscules a majúscules i glif de barra baixa (_) 2026-05-30 11:19:41 +02:00
JailDesigner 785700f819 feat(captures): captura de pantalla amb F9 (PNG amb shaders, a mida de finestra) 2026-05-30 11:06:38 +02:00
JailDesigner 07863577bc Merge branch 'tweak/colors-titol-ambre': PULSA START en ambre i ombra del títol més brillant 2026-05-30 10:32:33 +02:00
JailDesigner 8a341be027 tweak(títol): PULSA START en ambre i ombra del títol una mica més brillant 2026-05-30 10:32:23 +02:00
JailDesigner 93fb914e54 Merge branch 'feat/demo-no-friendly-fire': el pilot IA de la demo no dispara si té el company en la línia de tir 2026-05-30 10:15:50 +02:00
JailDesigner 8d659c44e5 feat(demo): el pilot IA retén el tret si té el company en la línia de tir (evita foc amic) 2026-05-30 10:06:30 +02:00
JailDesigner 5407f66c9e Merge branch 'fix/servicemenu': el menu de servei flota sense pausar la demo ni congelar el contador del títol 2026-05-30 09:50:16 +02:00
JailDesigner dd91b07a14 fix(servicemenu): el menu flota i no pausa la demo ni congela el contador del títol 2026-05-30 09:44:28 +02:00
JailDesigner fc8233ef57 Merge branch 'fix/demo-silencia-sfx-i-fuga-veu': la demo calla només els SFX de joc i ja no es cola la veu de fase al títol 2026-05-30 09:14:26 +02:00
JailDesigner ef2c13b011 fix(demo): silencia només els SFX de joc i evita que la veu de fase es cole al títol 2026-05-30 09:13:41 +02:00
JailDesigner 69e337393a Merge branch 'fix/color-frases-fase': frases de fase en ambre (desacoblades del títol) 2026-05-30 08:41:46 +02:00
JailDesigner 56c3f978d3 tweak(joc): les frases de fase tornen a l'ambre, desacoblades del PULSA START blanc del títol 2026-05-30 08:41:23 +02:00
JailDesigner cb958f33ba Merge branch 'tweak/colors-titol': colors del títol (ORNI ATTACK i JAILGAMES en cian, naus blanques, PRESS START blanc i intermitent) 2026-05-29 22:26:35 +02:00
JailDesigner e3d12e6e27 feat(titol): PRESS START intermitent (lent en aparèixer, ràpid en prémer START) sincronitzat 2026-05-29 22:26:05 +02:00
JailDesigner 47e9d85708 tweak(titol): ORNI ATTACK en cian (ombra color estrelles), naus blanques, JAILGAMES menys brillant i PULSA START una mica més amunt 2026-05-29 22:17:30 +02:00
JailDesigner 82027e4975 docs(titol): corregeix comentari obsolet (cian, no verd) de COPYRIGHT_BRIGHTNESS 2026-05-29 22:00:23 +02:00
JailDesigner ab06cb32c9 tweak(titol): JAILGAMES i copyright en cian pur en lloc de verd 2026-05-29 22:00:07 +02:00
JailDesigner 9e7061d8b7 tweak(titol): PREMEU START en blanc, JAILGAMES en verd estàndard i copyright el mateix verd amb menys brillo 2026-05-29 21:56:33 +02:00
JailDesigner b4b95c883f Merge branch 'feature/marcador': redisseny del marcador (color per jugador, ceros atenuats, vides com a slots/dígits commutables i layout centrat) 2026-05-29 21:42:21 +02:00
JailDesigner a46b93c917 tweak(hud): el mode numèric de vides mostra repuestos (vides-1), coherent amb els slots 2026-05-29 21:39:52 +02:00
JailDesigner 8d18c50aaa tweak(hud): mode de vides commutable a Defaults (slots o dígits); per defecte dígits per veure'l 2026-05-29 21:36:51 +02:00
JailDesigner b412435862 tweak(hud): NIVELL encès i el número amb els zeros de farciment atenuats com els punts 2026-05-29 21:27:11 +02:00
JailDesigner 5b90a9a767 tweak(hud): jugador inactiu = marcador apagat (tot atenuat, no en blanc) 2026-05-29 21:11:53 +02:00
JailDesigner 5ba562178b tweak(hud): el bloc d'un jugador inactiu es deixa apagat (sense dibuixar, reservant l'ample) 2026-05-29 21:06:29 +02:00
JailDesigner 55b37ba594 tweak(hud): alinea verticalment els slots de vides amb la línia del marcador (centre del bbox, no el declarat) 2026-05-29 21:00:21 +02:00
JailDesigner 20825c8138 tweak(hud): puja una mica l'alçada dels slots de vides (factor d'ajust sobre el glif) 2026-05-29 20:56:37 +02:00
JailDesigner 9235e684e8 tweak(hud): redueix els slots de vides a l'alçada real del glif i els pinta sense glow 2026-05-29 20:52:17 +02:00
JailDesigner 0350063fb7 tweak(hud): torna el tracking de les xifres a l'original (spacing 0.0) 2026-05-29 20:52:17 +02:00
JailDesigner 56065995fd tweak(hud): fila del marcador centrada amb posicions fixes (sense justificar a les vores) 2026-05-29 20:44:37 +02:00
JailDesigner 17e9206d26 tweak(hud): vides com a slots fixos (NUM_SLOTS = MAX_VIDES-1) que s'encenen/atenuen 2026-05-29 20:43:26 +02:00
JailDesigner 462e91d967 tweak(hud): restaura el tracking de les xifres del marcador (spacing 2.0) 2026-05-29 20:41:32 +02:00
JailDesigner 3bc87ad652 tweak(hud): l'últim dígit de la puntuació sempre encès (puntuació 0 no apaga el marcador) 2026-05-29 20:41:05 +02:00
JailDesigner a7233e13df tweak(hud): MAX_VIDES com a font única de vides i recalibra el groc atenuat de P2 2026-05-29 20:40:40 +02:00
JailDesigner 0abd661905 tweak(hud): vides com a icones de la nau en miniatura en lloc d'un número 2026-05-29 20:14:32 +02:00
JailDesigner a808226481 tweak(hud): zeros de farciment de la puntuació atenuats i etiqueta NIVELL en verd atenuat 2026-05-29 20:11:37 +02:00
JailDesigner 317e2a3fd9 tweak(hud): marcador en tres blocs ancorats (P1 esquerra, P2 dreta, nivell centrat) amb color per jugador 2026-05-29 20:09:28 +02:00
JailDesigner e4f8f586d6 tweak(hud): constants de l'esquema de color per jugador i de les icones de vides 2026-05-29 20:06:01 +02:00
JailDesigner 6f29731679 Merge branch 'tweaks/varis': nau ferida en roig i retorn al logo en acabar la partida 2026-05-29 19:55:11 +02:00
JailDesigner d7a9bd4ab2 tweak(game over): en acabar la partida es torna al logo en lloc del títol 2026-05-29 19:13:07 +02:00
JailDesigner ab5489a080 tweak(nau): la nau ferida parpelleja en roig pur en lloc de daurat 2026-05-29 19:12:53 +02:00
JailDesigner f4567a2e82 Merge branch 'fix/attract-dive-i-debris': logo silenciós en explotar i dive de càmera continu sota la cortinilla 2026-05-29 19:07:37 +02:00
JailDesigner 4b298ffc1c fix(attract): el logo no sona en explotar i la càmera del dive no frena amb la cortinilla 2026-05-29 19:06:40 +02:00
JailDesigner 0f986cbf80 Merge branch 'docs/arquitectura': document d'arquitectura per a nous companys 2026-05-29 11:56:23 +02:00
JailDesigner 582bd0ee30 docs: detalla el pipeline de shaders i la física al document d'arquitectura 2026-05-29 11:56:14 +02:00
JailDesigner 2e4030c2f2 docs: document d'arquitectura del projecte per a nous companys 2026-05-29 11:54:39 +02:00
JailDesigner a9b662840b Merge branch 'feature/attract-polish': polish de l'attract mode (logo silenciós, demo sense SFX, fons ja muntat, rètol de demo i transició dive+cortinilla títol→demo) 2026-05-29 10:15:04 +02:00
JailDesigner 30bbb37bff fix(demo): el dive movia només la posició de la càmera i s'invertia el forward; ara mou posició i target alhora 2026-05-29 10:13:30 +02:00
JailDesigner 2f6d6c405f feat(demo): transició títol→demo amb dive de càmera + cortinilla negra (substitueix el fundido) 2026-05-29 10:03:17 +02:00
JailDesigner 068f42782b feat(demo): transició per fosa a/desde negre en el salt títol→demo 2026-05-29 09:21:02 +02:00
JailDesigner 472c543c7b feat(demo): el marcador mostra el rètol de demo en lloc de puntuacions 2026-05-29 09:16:55 +02:00
JailDesigner 4e67a67ace feat(demo): la graella del fons apareix ja muntada en entrar a la demo 2026-05-29 09:12:30 +02:00
JailDesigner 1e63d3ae9d feat(demo): silenciar els efectes de so durant la demo (música intacta) 2026-05-29 09:09:37 +02:00
JailDesigner b363efd1f0 feat(demo): logo silenciós dins el cicle d'atracció (no sons ni reinici de música si ja sona) 2026-05-29 08:54:56 +02:00
JailDesigner 0abbaa09f8 Merge branch 'feature/attract-mode': attract mode (demo jugant-se sola) amb IA, 1P/2P, vides infinites i música contínua 2026-05-28 13:25:40 +02:00
JailDesigner 455b7a6893 feat(demo): demos a 1 i 2 jugadors, esquiva de bales enemigues i vides infinites 2026-05-28 13:14:19 +02:00
JailDesigner 92f76d091d fix(col·lisions): les bales d'enemic deixen de comptar com a foc amic (out-of-bounds a lives_per_player) i maten d'un toc 2026-05-28 13:13:36 +02:00
JailDesigner c1956e0028 feat(demo): attract mode amb pilot IA, escenaris curats i música contínua del títol 2026-05-28 12:01:12 +02:00
JailDesigner 491992a4d7 bump version a 0.8.1 2026-05-26 19:40:08 +02:00
JailDesigner e5b727216c Merge branch 'refactor/move-gamecontrollerdb-to-root': gamecontrollerdb fora de data/ (al costat del binari) + logs uniformes 2026-05-26 19:39:11 +02:00
JailDesigner f03e337b9a refactor(input): gamecontrollerdb.txt a l'arrel + target controllerdb + logs estil [Input] 2026-05-26 19:38:31 +02:00
JailDesigner 99e99e7e08 Merge branch 'refactor/remove-dead-oscillator-code': neteja del ColorOscillator (ara via shader) 2026-05-26 19:23:45 +02:00
JailDesigner b93761eb1e refactor(render): eliminar restes del ColorOscillator (setLineColor/getLineColor/global mutable) i deixar DEFAULT_LINE_COLOR constexpr 2026-05-26 19:23:29 +02:00
JailDesigner 4f5421191d Merge branch 'feat/hud-palette': HUD amb colors per funció + diferenciació P1/P2 2026-05-26 19:18:07 +02:00
JailDesigner 71ed9dc24f feat(hud): paleta per segments (P1 blanc, vides ambre, nivell verd, P2 rosa) 2026-05-26 19:17:22 +02:00
JailDesigner 1a0cc504c4 Merge branch 'refactor/rename-explosion-sounds': sons d'explosió i bullet_zap amb noms descriptius + enemy_hit per a debris_partial 2026-05-26 19:06:00 +02:00
JailDesigner 86775d4642 refactor(audio): renombrar hit.wav a bullet_zap.wav (desintegració de bala, no HURT d'enemic) 2026-05-26 19:05:43 +02:00
JailDesigner b936f410ce feat(audio): so enemy_hit per a debris_partial (impacte parcial a enemic amb HP>1) 2026-05-26 19:03:19 +02:00
JailDesigner ddcd2076a1 refactor(audio): renombrar explosion/explosion2 a enemy_explosion/player_explosion 2026-05-26 18:57:26 +02:00
JailDesigner 9345facaed Merge branch 'feat/orb-counterattack': orb taronja rosat dispara bullet_double cap al jugador en cada hit 2026-05-26 18:54:27 +02:00
JailDesigner 885caa6bc3 feat(orb): contra-atac amb bullet_double dirigida al jugador en rebre impacte 2026-05-26 18:53:34 +02:00
JailDesigner a77bbe4420 Merge branch 'feat/reorganize-shapes': renombre big_pentagon→orb i reorganització de data/shapes per categoria 2026-05-26 18:27:11 +02:00
JailDesigner 61a4886e62 refactor(shapes): reorganitzar data/shapes en subcarpetes per categoria (enemy/bullet/ship/effect) 2026-05-26 18:25:15 +02:00
JailDesigner 164f58c883 refactor(enemies): renombrar big_pentagon a orb i enemy_big_orb a enemy_orb 2026-05-26 18:09:29 +02:00
JailDesigner fbfacb825b Merge branch 'refactor/revert-stl-loops': bucles for explícits en lloc de std::ranges::* on aplica 2026-05-26 13:50:46 +02:00
JailDesigner 5e4d2cf993 refactor(physics): tornar std::ranges::find a bucle for explícit 2026-05-26 13:49:16 +02:00
JailDesigner 97d3749269 refactor: tornar std::ranges::{any,all,find}_of a bucles for explícits 2026-05-26 13:45:54 +02:00
JailDesigner 0dcecf9a3c tune(lint): desactivar readability-use-anyofallof per coherència amb cppcheck 2026-05-26 13:41:06 +02:00
JailDesigner c75e6406cd Merge branch 'feat/wave-based-stages': sistema d'onades declaratives per fase 2026-05-26 13:37:24 +02:00
JailDesigner 0254b44369 tune(stages): netejar comentaris obsolets a processPlaying 2026-05-26 13:36:48 +02:00
JailDesigner ff11567471 feat(stages): sistema d'onades declaratives amb condicions de transició 2026-05-26 13:32:43 +02:00
JailDesigner 06e383fe2c Merge branch 'feat/enemy-health-system': sistema d'HP declaratiu, big_pentagon i ajustos visuals 2026-05-25 22:47:54 +02:00
JailDesigner dc5b31087a Merge branch 'feat/debris-bullet-impulse': la bala impacta al cos O als trossos 2026-05-25 22:47:54 +02:00
JailDesigner 9e745dc3fc tune(enemy): trossos parcials i firework petit en color wounded 2026-05-25 22:47:31 +02:00
JailDesigner 14b10c663e tune(enemy): big_pentagon orb circular, firework petit per hit, sense wounded chain 2026-05-25 22:28:36 +02:00
JailDesigner f64c72f9a6 feat(enemy): sistema d'HP declaratiu i nou enemic big_pentagon 2026-05-25 21:46:48 +02:00
JailDesigner 610eaf257e refactor(debris): la bala impacta al cos O als trossos, mai a tots dos 2026-05-25 21:26:32 +02:00
JailDesigner b511740d93 Merge branch 'feat/enemy-ai-shoot': els enemics poden disparar bales declaratives des del YAML 2026-05-25 20:23:30 +02:00
JailDesigner b0643b6f62 Merge branch 'feat/enemy-ai-wander-chase-flee': afegir WANDER/CHASE/FLEE i target multi-ship 2026-05-25 20:23:25 +02:00
JailDesigner 7e8d79222c Merge branch 'feat/enemy-ai-movement-migration': moviment dels enemics a un sistema d'IA declaratiu 2026-05-25 20:23:02 +02:00
JailDesigner 14295ce859 feat(enemy): els enemics poden disparar bales via tick d'IA 2026-05-25 20:05:01 +02:00
JailDesigner 5ad433e63a feat(enemy): afegir behaviors WANDER/CHASE/FLEE i target multi-ship 2026-05-25 18:08:11 +02:00
JailDesigner 61e40e88f4 feat(enemy): migrar el moviment dels enemics a un sistema d'IA declaratiu 2026-05-25 17:45:30 +02:00
JailDesigner 410955de3c Merge branch 'feat/entity-event-system': sistema d'events declaratius per a enemics 2026-05-25 13:44:06 +02:00
JailDesigner 9c0502eefb feat(enemy): sistema d'events declaratius via YAML 2026-05-25 13:34:48 +02:00
JailDesigner 9b3da3a6e7 Merge branch 'feat/enemy-star': afegir tipus STAR i 3 nous shapes 2026-05-25 12:42:06 +02:00
JailDesigner bc41169176 feat(enemy): afegir tipus STAR (estrella de 5 puntes) i 3 nous shapes
- Nou enemic STAR amb shape star_5.shp, escala 0.7 i color groc pur.
  Reusa el comportament zigzag del Pentagon i carrega via EnemyRegistry.
- DistribucioEnemics estesa amb camp 'star' opcional (default 0) per
  mantenir compat amb stages antics.
- Stage 1 reconfigurat a 25/25/25/25 per mostrar els 4 tipus.
- Afegits també shapes bullet_long.shp i bullet_double.shp (encara no
  utilitzats; preparats per futures variants de bala).
2026-05-25 12:36:26 +02:00
JailDesigner b3a1afce06 Merge branch 'feat/entities-yaml-enemy-shared': paràmetres compartits dels enemics a cada YAML 2026-05-25 11:59:28 +02:00
JailDesigner 4b6dc8a47a feat(entities): migrar paràmetres compartits dels enemics a cada YAML 2026-05-25 11:54:40 +02:00
JailDesigner 3dadd5fc1a Merge branch 'feat/entities-yaml-bullet': migració de la bala a YAML 2026-05-25 11:47:36 +02:00
JailDesigner bea844d51e feat(entities): migrar bullet a data/entities/bullet/bullet.yaml 2026-05-25 11:42:43 +02:00
JailDesigner 5fb6c68df4 Merge branch 'feat/entities-shape-scale': collision_radius derivat del shape + scale al YAML 2026-05-25 11:33:52 +02:00
JailDesigner 866a057704 feat(entities): derivar collision_radius del shape + scale/collision_factor al YAML 2026-05-25 11:29:43 +02:00
JailDesigner da8eab330d Merge branch 'feat/entities-yaml-enemies': configuració dels enemics en YAML 2026-05-25 10:15:34 +02:00
JailDesigner 39bda0775e feat(entities): migrar la configuració dels 3 enemics a data/entities/<type>/*.yaml 2026-05-25 10:01:12 +02:00
JailDesigner ed4d3a3915 Merge branch 'feat/entities-yaml-player': configuració del player en YAML 2026-05-25 09:39:39 +02:00
JailDesigner 6447932212 feat(entities): migrar la configuració del player a data/entities/player/player.yaml 2026-05-25 08:32:49 +02:00
JailDesigner 9f278772bb Merge branch 'feat/pack-resources-align': alinear pack_resources amb projectes germans 2026-05-25 07:55:49 +02:00
JailDesigner 2d073b6055 feat(pack): alinear sortida i build amb projectes germans 2026-05-25 07:55:30 +02:00
132 changed files with 8762 additions and 2148 deletions
+3
View File
@@ -9,6 +9,9 @@ Checks:
- -bugprone-easily-swappable-parameters - -bugprone-easily-swappable-parameters
- -bugprone-narrowing-conversions - -bugprone-narrowing-conversions
- -modernize-avoid-c-arrays - -modernize-avoid-c-arrays
# No forçar reemplaç de bucles "normals" per std::any_of/std::all_of.
# Equivalent a `--suppress=useStlAlgorithm` que ja tenim a cppcheck.
- -readability-use-anyofallof
# performance-noexcept-move-constructor crashea clang-tidy (LLVM 19.1) # performance-noexcept-move-constructor crashea clang-tidy (LLVM 19.1)
# con recursión infinita en ExceptionSpecAnalyzer::analyzeRecord cuando # con recursión infinita en ExceptionSpecAnalyzer::analyzeRecord cuando
# analiza ciertas instanciaciones de std::set. No es un falso positivo # analiza ciertas instanciaciones de std::set. No es un falso positivo
+6 -3
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(orni VERSION 0.8.0 LANGUAGES CXX) project(orni VERSION 0.8.1 LANGUAGES CXX)
# Info del projecte (font de veritat per a project.h) # Info del projecte (font de veritat per a project.h)
set(PROJECT_LONG_NAME "Orni Attack") set(PROJECT_LONG_NAME "Orni Attack")
@@ -69,7 +69,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
if(EXTERNAL_SOURCES) if(EXTERNAL_SOURCES)
set_source_files_properties( set_source_files_properties(
${EXTERNAL_SOURCES} ${EXTERNAL_SOURCES}
PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations" PROPERTIES COMPILE_OPTIONS "-Wno-missing-field-initializers;-Wno-deprecated-declarations;-Wno-tautological-compare"
) )
endif() endif()
@@ -110,7 +110,10 @@ add_executable(pack_resources EXCLUDE_FROM_ALL
tools/pack_resources/pack_resources.cpp tools/pack_resources/pack_resources.cpp
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
) )
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source") target_include_directories(pack_resources PRIVATE
"${CMAKE_SOURCE_DIR}/source"
"${CMAKE_BINARY_DIR}"
)
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack --- # --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
+838
View File
@@ -0,0 +1,838 @@
# Arquitectura de Orni Attack
> Documento de orientación para alguien que llega nuevo al proyecto. Cada
> afirmación está anclada a código real (fichero/clase/función con su ruta).
> Cuando algo no se ha podido verificar o no existe, se indica explícitamente.
> El objetivo no es vender una arquitectura ideal, sino describir lo que **este**
> proyecto hace, incluso donde es poco convencional.
## Índice
1. [Visión general](#1-visión-general)
2. [Punto de entrada y el Director](#2-punto-de-entrada-y-el-director)
3. [Bucle principal](#3-bucle-principal)
4. [Sistema de escenas](#4-sistema-de-escenas)
5. [Renderizado: de la lógica al píxel](#5-renderizado-de-la-lógica-al-píxel)
6. [Entrada](#6-entrada)
7. [Audio](#7-audio)
8. [Recursos](#8-recursos)
9. [Comunicación entre módulos](#9-comunicación-entre-módulos)
10. [Lógica del juego](#10-lógica-del-juego)
11. [IA del modo demo (attract)](#11-ia-del-modo-demo-attract)
12. [Efectos visuales](#12-efectos-visuales)
13. [Configuración, constantes y convenciones](#13-configuración-constantes-y-convenciones)
14. [Guía de navegación](#14-guía-de-navegación)
---
## 1. Visión general
Orni Attack es un arcade vectorial (estética CRT de líneas con bloom) construido
sobre **SDL3**, usando la **GPU API de SDL3** (`SDL_gpu`) para el render — **no**
`SDL_Renderer`. El código está partido en dos grandes mundos:
- **`source/core/`** — el "motor": ventana, GPU, audio, input, recursos, i18n,
overlays de sistema. No conoce nada del juego concreto. Por ejemplo,
[audio.hpp](source/core/audio/audio.hpp) recibe un struct de configuración y no
lee YAML, e [input.hpp](source/core/input/input.hpp) no incluye nada de `game/`.
- **`source/game/`** — la lógica concreta de Orni Attack: escenas, entidades
(naves, enemigos, balas), sistemas (colisiones, IA), stages/oleadas y efectos.
El punto de indirección entre ambos mundos para el render es
[render_context.hpp](source/core/rendering/render_context.hpp): el juego habla con
un `Rendering::Renderer*` opaco que es un alias de `GPU::GpuFrameRenderer`. Esto
permite cambiar de backend sin tocar las firmas del juego.
```mermaid
graph TD
subgraph entry["Punto de entrada"]
MAIN["main.cpp<br/>SDL_MAIN_USE_CALLBACKS"]
end
MAIN -->|posee| DIR["Director<br/>(es el programa)"]
subgraph core["source/core (motor)"]
SDLM["SDLManager<br/>ventana + GPU"]
GE["GlobalEvents<br/>F1-F7/F12/ESC/hotplug"]
INPUT["Input (singleton)"]
AUDIO["Audio (singleton)"]
RES["Resource::Loader / Pack"]
LOC["Locale (i18n)"]
OVL["Notifier · ServiceMenu<br/>DebugOverlay · DefineInputs"]
end
subgraph game["source/game (juego)"]
SCN["Scenes<br/>Logo · Title · Game"]
ENT["Entities<br/>Ship · Enemy · Bullet"]
SYS["Systems<br/>Collision · EnemyAi · DemoPilot"]
STG["StageManager / WaveRunner"]
FX["Effects<br/>debris · firework · score · trail"]
end
DIR --> SDLM
DIR --> GE
DIR --> OVL
DIR --> SCN
SCN --> ENT
SCN --> SYS
SCN --> STG
SCN --> FX
GE --> INPUT
SCN -.usa.-> AUDIO
SCN -.usa.-> RES
OVL -.usa.-> LOC
```
**Patrón dominante de comunicación:** singletons globales (`Input::get()`,
`Audio::get()`, `Locale::get()`, `Notifier`, `ServiceMenu`) más paso por
referencia de un `Rendering::Renderer*` y un `SceneContext&`. **No hay** un bus de
eventos genérico ni un ECS — las entidades viven en `std::array` de tamaño fijo
dentro de `GameScene` y los sistemas operan sobre un struct `Context` de punteros
(ver [§10](#10-lógica-del-juego)).
**Rasgo de diseño destacable:** gran parte de la lógica es *data-driven*. Los
enemigos, balas y el jugador se describen en **YAML declarativo**
(`data/entities/*/*.yaml`: physics/ai/animation/events), los stages en
`data/stages/stages.yaml` (oleadas), y las figuras vectoriales en ficheros `.shp`.
---
## 2. Punto de entrada y el Director
El `main` real está en [main.cpp](source/main.cpp) y usa el modo de callbacks de
SDL3 (`#define SDL_MAIN_USE_CALLBACKS 1`). En lugar de un bucle `while` clásico,
SDL llama a cuatro funciones, y todas son pura fontanería que delega en un
`Director`:
```cpp
// main.cpp
auto SDL_AppInit(void** appstate, int argc, char* argv[]) -> SDL_AppResult {
System::Relaunch::setArgv(argc, argv);
auto director = std::make_unique<Director>(argc, argv);
*appstate = director.release(); // SDL guarda el puntero
return SDL_APP_CONTINUE;
}
auto SDL_AppEvent(void* s, SDL_Event* e) { return ((Director*)s)->handleEvent(*e); }
auto SDL_AppIterate(void* s) { return ((Director*)s)->iterate(); }
void SDL_AppQuit(void* s, ...) { /* reabsorbe y destruye el Director */ }
```
La filosofía está escrita en el propio comentario de cabecera de
[director.hpp](source/core/system/director.hpp):
> *El Director és EL programa: posseeix la configuració, els subsistemes i
> l'estat.*
Como con `SDL_MAIN_USE_CALLBACKS` no hay un `scope` que envuelva todo el bucle,
el estado que antes vivía en un `run()` ahora es **miembro** del Director:
`sdl_` (SDLManager), `context_` (SceneContext), `debug_overlay_` y
`current_scene_` (todos `std::unique_ptr`, ver
[director.hpp:45-48](source/core/system/director.hpp#L45-L48)).
### Orden de arranque (constructor)
El constructor [Director::Director](source/core/system/director.cpp#L46) ejecuta el
bootstrap completo, en este orden:
1. `ConfigYaml::init()` — valores por defecto de configuración.
2. Parseo de argumentos (`--console`, `--reset-config`) en
[checkProgramArguments](source/core/system/director.cpp#L241).
3. `Utils::initializePathSystem()` + sistema de recursos
([§8](#8-recursos)): en *release* el `resources.pack` es obligatorio; en *dev*
hay fallback a `data/`.
4. Crea la carpeta de sistema (`~/.config/jailgames/<NAME>` en Linux) y carga/crea
`config.yaml` ([createSystemFolder](source/core/system/director.cpp#L260)).
5. Carga el `locale` ([§7](#7-audio) usa lo mismo: i18n).
6. `Input::init()` con el `gamecontrollerdb.txt` (autoasigna mandos a P1/P2 la
primera vez).
7. Crea `SDLManager` (ventana + GPU), oculta el cursor, inicializa `Audio`.
8. **Precarga bloqueante** de todos los recursos (música, sonidos, shapes) para
evitar tirones de I/O en las transiciones
([director.cpp:187-195](source/core/system/director.cpp#L187-L195)).
9. Crea el `SceneContext` y fija la escena inicial: `TITLE` en `_DEBUG`, `LOGO`
en el resto ([director.cpp:200-205](source/core/system/director.cpp#L200-L205)).
10. Inicializa los overlays de sistema: `DebugOverlay`, `Notifier`, `ServiceMenu`,
`DefineInputs`.
El destructor [Director::~Director](source/core/system/director.cpp#L218) guarda
la config y destruye los subsistemas **en orden inverso** a la construcción (el
`Notifier` referencia el renderer, así que debe morir antes que `sdl_`).
---
## 3. Bucle principal
Cada frame, SDL llama a `SDL_AppIterate`, que delega en
[Director::iterate()](source/core/system/director.cpp#L383). Su estructura es:
```mermaid
sequenceDiagram
participant SDL
participant Dir as Director::iterate()
participant Scene
participant SDLM as SDLManager
participant GPU as GpuFrameRenderer
SDL->>Dir: iterate()
Note over Dir: si wants_quit_ → SDL_APP_SUCCESS
Dir->>Dir: si !scene o scene.isFinished() → advanceScene()
Dir->>Dir: delta_time = (now - last) capeado a 50 ms
Dir->>Dir: Input::update()
Dir->>Scene: update(dt)
Dir->>Dir: overlays.update(dt) + Audio::update()
Dir->>SDLM: clear() (= GPU.beginFrame)
alt swapchain no disponible
SDLM-->>Dir: false → saltar draw+present
end
Dir->>SDLM: updateRenderingContext()
Dir->>Scene: draw()
Dir->>Dir: overlays.draw() (capas)
Dir->>SDLM: present() (= GPU.endFrame → bloom + postfx)
```
Puntos concretos a tener en cuenta:
- **Pivot de escena**: si no hay escena o la actual reporta `isFinished()`, se
llama a [advanceScene()](source/core/system/director.cpp#L338), que destruye la
actual y construye la siguiente con
[buildScene()](source/core/system/director.cpp#L323) según
`context_->nextScene()`.
- **Delta time**: se mide con `SDL_GetTicks()` y se **capea a 50 ms** para evitar
saltos grandes tras un stall ([director.cpp:397-400](source/core/system/director.cpp#L397-L400)).
- **Orden de update**: `Input::update()``current_scene_->update(dt)`
`debug_overlay_``Notifier``ServiceMenu``DefineInputs``Audio::update()`.
- **Render por capas** (de abajo arriba, entre `clear` y `present`):
escena → `debug_overlay_``Notifier` (toasts) → `ServiceMenu``DefineInputs`
(modal de rebinding). Si el overlay de rebinding está activo, el menú de servicio
no se pinta ([director.cpp:432-439](source/core/system/director.cpp#L432-L439)).
- **Salto de frame**: si `sdl_->clear()` devuelve `false` (swapchain no disponible,
p. ej. ventana minimizada), se omiten `draw` y `present` ese frame.
El bucle de eventos vive aparte, en
[Director::handleEvent()](source/core/system/director.cpp#L354), que enruta cada
`SDL_Event` por la cadena: **ventana → GlobalEvents → F11 (debug overlay) →
escena** (ver [§9](#9-comunicación-entre-módulos)).
---
## 4. Sistema de escenas
La interfaz base es [scene.hpp](source/core/system/scene.hpp). Como dice su
cabecera, *el frame loop vive en el Director, no en cada escena*. Cada escena
implementa cuatro métodos puros:
```cpp
virtual void handleEvent(const SDL_Event&) = 0; // eventos no-globales
virtual void update(float delta_time) = 0; // lógica
virtual void draw() = 0; // pintado (entre clear y present)
virtual auto isFinished() const -> bool = 0; // ¿transición pendiente?
```
Una escena pide transición vía `context_.setNextScene(...)`; en el siguiente frame
`isFinished()` devuelve `true` y el Director la destruye para construir la
siguiente.
### SceneContext
[scene_context.hpp](source/core/system/scene_context.hpp) es el "buzón" de
transición que el Director posee y va pasando a cada escena por referencia. Tiene:
- `SceneType` (enum): `LOGO`, `TITLE`, `GAME`, `EXIT`.
- `Option` (p. ej. `JUMP_TO_TITLE_MAIN`) consumible con `consumeOption()`.
- `MatchConfig` (jugadores activos, modo NORMAL/DEMO) para pasar a `GAME`.
- El **índice del escenario de demo** (`demoScenarioIndex()` / `advanceDemoScenario()`),
que persiste entre escenas para que cada entrada al attract mode muestre el
siguiente escenario curado (ver [§11](#11-ia-del-modo-demo-attract)).
Existe además una variable global `SceneManager::actual` que el Director mantiene
sincronizada con la escena en curso (compatibilidad hacia atrás).
### Las tres escenas (FSM jerárquica)
```mermaid
stateDiagram-v2
[*] --> LOGO
LOGO --> TITLE
TITLE --> GAME : START (1P/2P)
TITLE --> GAME : idle timeout (DEMO)
GAME --> TITLE : game over / fin demo (input)
GAME --> LOGO : fin demo (timeout/muerte)
TITLE --> [*] : EXIT
```
Cada escena tiene además su **propia** máquina de estados interna:
- **[LogoScene](source/game/scenes/logo_scene.hpp)** — `AnimationState`:
`PRE_ANIMATION → ANIMATION → POST_ANIMATION → EXPLOSION → POST_EXPLOSION`. Anima
el logo JAILGAMES y lo hace explotar en fragmentos (debris).
- **[TitleScene](source/game/scenes/title_scene.hpp)** — `TitleState`:
`STARFIELD_FADE_IN → STARFIELD → MAIN → PLAYER_JOIN_PHASE → BLACK_SCREEN →
DEMO_DIVE → DEMO_CURTAIN`. Naves 3D flotantes (vía
[ShipAnimator](source/game/title/ship_animator.hpp)), selección 1P/2P, y un
`idle_timer_` en el estado `MAIN` que dispara el attract mode por inactividad.
- **[GameScene](source/game/scenes/game_scene.hpp)** — es el núcleo del juego y se
detalla en [§10](#10-lógica-del-juego).
---
## 5. Renderizado: de la lógica al píxel
Este es el subsistema más denso. La idea central: **toda la geometría son líneas**
(la estética es vectorial). El juego acumula líneas en CPU durante `draw()`, y al
final del frame se envían a la GPU en un único batch, se rasterizan a una textura
*offscreen*, y un par de pases de post-procesado (bloom + flicker/fondo) componen
la imagen final sobre la swapchain.
### 5.1 Capas del subsistema
| Fichero | Rol |
|---|---|
| [sdl_manager.hpp/.cpp](source/core/rendering/sdl_manager.hpp) | Crea la ventana SDL, posee el `GpuFrameRenderer`, gestiona zoom/fullscreen/letterbox. Expone `clear()` / `present()` / `getRenderer()`. |
| [gpu/gpu_frame_renderer.hpp/.cpp](source/core/rendering/gpu/gpu_frame_renderer.hpp) | Orquestador del frame GPU: `beginFrame``pushLine`/`pushRect``endFrame` (`flushBatch` + `bloomPass` + `compositePass`). |
| [gpu/gpu_device](source/core/rendering/gpu/gpu_device.hpp) | Wrapper del `SDL_GPUDevice` (claim de ventana, formato de swapchain). |
| [gpu/gpu_line_pipeline](source/core/rendering/gpu/gpu_line_pipeline.hpp) | Pipeline de líneas: dibuja cada línea como un quad (2 triángulos) con antialias geométrico. |
| [gpu/gpu_bloom_pipeline](source/core/rendering/gpu/gpu_bloom_pipeline.hpp) | Blur gaussiano separable (pase H + pase V) sobre dos texturas ping-pong. |
| [gpu/gpu_postfx_pipeline](source/core/rendering/gpu/gpu_postfx_pipeline.hpp) | Composite final: mezcla escena + bloom + flicker + fondo pulsante. |
| [line_renderer.hpp/.cpp](source/core/rendering/line_renderer.hpp) | API que usa el juego: `Rendering::linea(...)` y `lineaGlow(...)`. |
| [shape_renderer.hpp/.cpp](source/core/rendering/shape_renderer.hpp) | `renderShape(...)`: dibuja una `Shape` aplicando transformación y, opcionalmente, glow multipase. |
### 5.2 Una `Shape` y cómo se carga
Una "shape" es una figura vectorial: un conjunto de **polilíneas** y **líneas**
([shape.hpp](source/core/graphics/shape.hpp)). Los ficheros viven en `data/shapes/`
con extensión `.shp` y un formato de texto tipo clave:valor. Ejemplo real
([data/shapes/ship/arrow.shp](data/shapes/ship/arrow.shp)):
```
name: arrow
scale: 1.0
center: 0, 0
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
```
> Nota: el formato real usa directivas `name:`, `scale:`, `center:`,
> `polyline:` y `line:` (Y negativo = arriba). No es la sintaxis
> `POLYLINE: (x,y)` que podría suponerse de otros motores.
La carga la centraliza [shape_loader.hpp](source/core/graphics/shape_loader.hpp)
(`Graphics::ShapeLoader::load(filename)`), con caché de `std::shared_ptr<Shape>`.
Todas las shapes se precargan en el boot del Director.
### 5.3 El flujo de un frame de render
```mermaid
graph TD
A["Scene::draw()<br/>(acumula en CPU)"] --> B["Rendering::linea / renderShape"]
B --> C["GpuFrameRenderer::pushLine()<br/>extruye quad → vertices_ / indices_"]
C -.repetido N veces.-> C
A --> D["SDLManager::present()<br/>= GpuFrameRenderer::endFrame()"]
D --> E["flushBatch()<br/>sube VBO/IBO, dibuja sobre OFFSCREEN"]
E --> F["bloomPass()<br/>H: high-pass+blur → bloom_a<br/>V: blur → bloom_b"]
F --> G["compositePass()<br/>offscreen + bloom_b + flicker + fondo<br/>→ swapchain (letterbox)"]
G --> H["SubmitGPUCommandBuffer + present"]
```
Paso a paso, con anclas reales:
1. **Emisión (juego).** Durante `current_scene_->draw()`, el juego llama a
[Rendering::linea()](source/core/rendering/line_renderer.hpp#L33) (y
`renderShape`, `VectorText`, `Playfield`, etc.). Las coordenadas son **lógicas
(1280×720)**. El color por defecto si `alpha==0` es el verde fósforo CRT
`DEFAULT_LINE_COLOR = {100,255,100,255}`.
2. **Acumulación (CPU).** `linea()` pre-multiplica el brillo y llama a
[GpuFrameRenderer::pushLine()](source/core/rendering/gpu/gpu_frame_renderer.hpp#L88),
que **extruye** la línea en un quad (4 vértices, 6 índices) y lo acumula en
`vertices_` / `indices_`. Si el antialias está activo, añade ~0.5 px de padding y
marca `edge_dist` para el fade del fragment shader.
3. **Flush (GPU).** En `endFrame()`, `flushBatch()` sube el batch a un VBO/IBO,
abre un render pass sobre el `offscreen_texture_` (R8G8B8A8, tamaño físico
configurable, independiente del lógico) y dibuja con el `line_pipeline_`. El
vertex shader transforma píxeles lógicos → NDC; el fragment shader aplica
`smoothstep` sobre `edge_dist` para el suavizado.
4. **Bloom.** `bloomPass()` hace un blur separable: pase H (high-pass por
luminancia + blur horizontal → `bloom_texture_a_`) y pase V (blur vertical →
`bloom_texture_b_`). Parámetros en `PostFxParams`
([gpu_frame_renderer.hpp:33-51](source/core/rendering/gpu/gpu_frame_renderer.hpp#L33-L51)).
5. **Composite.** `compositePass()` dibuja un triángulo *fullscreen* sobre la
swapchain, muestreando offscreen + bloom, aplicando flicker temporal y un fondo
verde pulsante. Aquí se aplica el **letterbox** vía el viewport físico
(`setViewport`).
El interruptor maestro de post-proceso es **F6** (`setPostFxEnabled`): cuando está
OFF, la escena offscreen sale tal cual (passthrough), útil para A/B testing.
### 5.4 Texto, 3D y elementos de escena
- **[VectorText](source/core/graphics/vector_text.hpp)** — renderiza texto donde
cada carácter es una `Shape` precargada.
- **[Camera3D](source/core/graphics/camera3d.hpp)** + **[Wireframe3D](source/core/graphics/wireframe3d.hpp)**
— proyección perspectiva en CPU de mallas 3D (vértices + aristas) a líneas 2D.
Lo usan el starfield 3D y las naves del título.
- **[Starfield](source/core/graphics/starfield.hpp)** (campo de estrellas 3D que
vienen hacia la cámara) y **[StarfieldParallax](source/core/graphics/starfield_parallax.hpp)**
(capas 2D de fondo con parallax).
- **[Playfield](source/core/graphics/playfield.hpp)** — rejilla de fondo con
animación de construcción y *ripples* (ondas) que reaccionan a la nave y a las
explosiones.
- **[Border](source/core/graphics/border.hpp)** — marco de 4 lados que se desplaza
al recibir impactos.
- **[Curtain](source/core/graphics/curtain.hpp)** — cortinilla negra para
transiciones; se pinta siempre la última.
### 5.5 Shaders: fuentes, compilación y selección
Las fuentes GLSL viven en [shaders/](shaders/): `line.vert.glsl`, `line.frag.glsl`,
`postfx.vert.glsl`, `postfx.frag.glsl`, `bloom.frag.glsl`. **No se cargan de disco en
runtime**: se embeben como arrays/strings en el binario.
**Pipeline de compilación (SPIR-V, Linux/Windows).** Lo orquesta
[CMakeLists.txt:139-187](CMakeLists.txt#L139). La lógica clave:
- Para cada `.glsl` hay un header destino en
[gpu/spv/](source/core/rendering/gpu/spv/) (p. ej. `line_vert_spv.h`).
- CMake busca `glslc` (`find_program(GLSLC_EXE ...)`). Hay **tres caminos**:
1. `glslc` presente → un `add_custom_command` regenera los headers SPV cuando
cambian los `.glsl`, vía el target `shaders` del que depende el ejecutable.
2. `glslc` ausente pero **los headers ya están commiteados** → se usan tal cual
(los `.spv.h` están versionados en el repo).
3. `glslc` ausente **y** faltan headers → `FATAL_ERROR` pidiendo instalar
`shaderc`/`vulkan-sdk`.
- La conversión binario→header la hace el script
[tools/shaders/compile_spirv.cmake](tools/shaders/compile_spirv.cmake): invoca
`glslc -O -fshader-stage=<vert|frag>` para producir el `.spv`, lee el binario como
hex (`file(READ ... HEX)`) y escribe un header con
`static const uint8_t LINE_VERT_SPV[] = { 0x.., ... };` y su `_SIZE`. Es
multiplataforma puro CMake (no necesita `bash` ni `xxd`).
**MSL (macOS).** Los headers Metal en [gpu/msl/](source/core/rendering/gpu/msl/)
(`line_vert.msl.h`, etc.) están **escritos a mano** (no los genera CMake), como
strings literales C++.
**Selección SPV vs MSL: es _compile-time_, no runtime.** La hace
[shader_factory.hpp](source/core/rendering/gpu/shader_factory.hpp) con `#ifdef __APPLE__`:
en Apple expone `createShaderMSL(...)` (`SDL_GPU_SHADERFORMAT_MSL`), y en el resto
`createShaderSPIRV(...)` (`SDL_GPU_SHADERFORMAT_SPIRV`). Cada pipeline llama al helper
disponible con el header embebido correspondiente. (Es decir: no es `GpuDevice` quien
elige el backend de shader, sino el preprocesador al compilar.)
---
## 6. Entrada
El subsistema de input ([core/input/](source/core/input/)) es un **singleton**
(`Input::init()` / `Input::get()` / `Input::destroy()`) que unifica teclado,
gamepads y ratón.
- **Acciones**: enum `InputAction` (`LEFT`, `RIGHT`, `THRUST`, `SHOOT`, `START`,
`MENU`, ...) en [input_types.hpp](source/core/input/input_types.hpp).
- **Bindings por jugador**: hay bindings separados de teclado y de gamepad para P1
y P2, que se cargan de la config con `applyPlayer1Bindings()` /
`applyPlayer2Bindings()` (llamados desde el constructor del Director).
- **Captura por frame**: `Input::update()` lee `SDL_GetKeyboardState()` y los ejes
y botones del gamepad, y hace *edge-detection* para distinguir `just_pressed` de
`is_held`. La consulta es `checkAction(...)` / `checkActionPlayer1/2(...)`.
- **Hotplug**: `Input::handleEvent()` procesa `SDL_EVENT_GAMEPAD_ADDED/REMOVED`
(`addGamepad` / `removeGamepad`) y notifica con un toast vía `Notifier`.
- **Ratón**: [mouse.hpp](source/core/input/mouse.hpp) auto-oculta el cursor.
- **Rebinding en runtime**: [define_inputs.hpp](source/core/input/define_inputs.hpp)
es un modal singleton que captura una secuencia de acciones, persiste en config y
reaplica bindings sin reiniciar.
El enrutado de input ocurre en dos sitios: los eventos **globales** pasan por
`GlobalEvents::handle()` (que primero deja a `Input` procesar el hotplug), y la
lógica de juego consulta directamente `Input::get()->checkAction...` durante
`update()` (p. ej. [Ship::processInput](source/game/entities/ship.hpp)).
---
## 7. Audio
[core/audio/](source/core/audio/) es otro singleton (`Audio::init/get/destroy`)
con un motor de bajo nivel propio:
- **[Audio](source/core/audio/audio.hpp)** — capa lógica: `playMusic()`,
`playSound()`, volúmenes por grupo (`GAME`, `INTERFACE`), `playSoundWithEcho/Reverb`.
- **[jail_audio.hpp](source/core/audio/jail_audio.hpp)** (`Ja::Engine`) — motor
sobre SDL3 audio: streaming de **OGG** (vía `stb_vorbis`) para música, **WAV**
descomprimido para efectos, mezcla en N canales.
- **[audio_adapter.hpp](source/core/audio/audio_adapter.hpp)** —
`AudioResource::getMusic/getSound`: caché *lazy* que carga bytes vía
`Resource::Helper` y los decodifica una sola vez.
- **[audio_effects.hpp](source/core/audio/audio_effects.hpp)** — DSP de echo y
reverb; presets en `data/config/sounds.yaml`
([sound_effects_config.hpp](source/core/audio/sound_effects_config.hpp)).
El Director precarga toda la música y todos los sonidos en el boot, y llama a
`Audio::update()` una vez por frame.
---
## 8. Recursos
[core/resources/](source/core/resources/) abstrae de dónde salen los bytes:
- **[resource_pack](source/core/resources/resource_pack.hpp)** (`Resource::Pack`)
— lee un fichero empaquetado con cabecera *magic* `"ORNI"` y entradas con CRC32
para validación de integridad.
- **[resource_loader](source/core/resources/resource_loader.hpp)**
(`Resource::Loader`, singleton Meyers) — `loadResource()`, `resourceExists()`,
`listResources(prefix)`, `validatePack()`.
- **[resource_helper](source/core/resources/resource_helper.hpp)** — wrappers de
conveniencia (`initializeResourceSystem`, `listResources`, `loadFile`).
**Estrategia dual** (decidida en el constructor del Director,
[director.cpp:64-93](source/core/system/director.cpp#L64-L93)):
- **Release** (`RELEASE_BUILD`): `resources.pack` es **obligatorio** y se valida su
integridad; si falla, el juego aborta. No hay fallback (ver memoria de proyecto
*"No fallback a SDL_Renderer"* — aquí es la política equivalente para recursos).
- **Dev**: intenta el pack; si no está, hace **fallback al directorio `data/`** del
filesystem, escaneándolo según prefijo (`music/`, `sounds/`, `shapes/`).
El formato de datos de juego:
- **Entidades** (`data/entities/<nombre>/<nombre>.yaml`) — YAML declarativo con
`shape`, `physics`, `ai`, `animation`, `wounded`, `spawn`, `colors`, `score`,
`events`. Ejemplo: [data/entities/square/square.yaml](data/entities/square/square.yaml).
- **Stages** (`data/stages/stages.yaml`) — oleadas (`waves`) con `spawn`,
`spawn_interval`, `next` y multiplicadores de dificultad por stage.
- **Shapes** (`data/shapes/**/*.shp`) — figuras vectoriales (ver [§5.2](#52-una-shape-y-cómo-se-carga)).
El parser YAML usado es [fkyaml](source/external/fkyaml_node.hpp) (cabecera única),
envuelto por [config_yaml](source/game/config_yaml.hpp).
---
## 9. Comunicación entre módulos
No hay un sistema de mensajería desacoplado. La comunicación es:
1. **Eventos SDL → cadena del Director.** Por cada `SDL_Event`,
[Director::handleEvent](source/core/system/director.cpp#L354) intenta, en orden:
`SDLManager::handleWindowEvent``GlobalEvents::handle` → F11 (debug overlay) →
`current_scene_->handleEvent`.
2. **GlobalEvents** ([global_events.cpp](source/core/system/global_events.cpp)) es
el orquestador de la entrada global. Su `handle()` hace, en orden:
`Input::get()->handleEvent` (hotplug) → `consumeIfDefineActive` (si el modal de
rebinding está activo, **engulle todo**) → `SDL_EVENT_QUIT` → ratón → botón MENU
del mando → reenvío al `ServiceMenu` si está abierto → teclas de función:
| Tecla | Acción |
|---|---|
| F1 / F2 | reducir / aumentar tamaño de ventana |
| F3 | fullscreen |
| F4 | VSync |
| F5 | antialias geométrico |
| F6 | post-procesado (bloom/flicker/fondo) |
| F7 | idioma ca ↔ en (hot-swap de `Locale`) |
| F11 | debug overlay (gestionado en el Director, no en GlobalEvents) |
| F12 | menú de servicio |
| ESC | doble pulsación para salir (la 1ª muestra un toast de confirmación) |
3. **Singletons compartidos.** `Input`, `Audio`, `Locale`, `Notifier`,
`ServiceMenu`, `DefineInputs` se acceden globalmente vía `::get()`. Muchos
comprueban `nullptr` para degradar con elegancia (p. ej. el hotplug notifica
solo si `Notifier::get() != nullptr`).
4. **Paso por referencia.** Las escenas reciben `SDLManager&` y `SceneContext&`; el
render se propaga como `Rendering::Renderer*`. Los sistemas de juego reciben un
struct `Context` con punteros a los pools (ver [§10](#10-lógica-del-juego)).
**Overlays de sistema** (todos singletons, todos por encima de la escena):
- **[Notifier](source/core/system/notifier.hpp)** — toasts deslizantes centrados
(`notifyInfo/Warn/Exit`), con máquina de animación HIDDEN/ENTERING/HOLDING/EXITING.
- **[ServiceMenu](source/core/system/service_menu.hpp)** — menú de configuración
(F12) con pila de páginas (vídeo, audio, controles, sistema...).
- **[DebugOverlay](source/core/system/debug_overlay.hpp)** — HUD de FPS/VSync (F11).
- **[Relaunch](source/core/system/relaunch.hpp)** — reinicio en caliente vía
`execv` (lo solicita el ServiceMenu, lo ejecuta `SDL_AppQuit`).
**Lo que NO existe** (verificado): no hay event bus genérico, ni cola de mensajes
desacoplada, ni un FSM genérico reutilizable fuera de las máquinas de estado
concretas de cada escena/sistema, ni un ECS.
---
## 10. Lógica del juego
Toda la partida vive en [GameScene](source/game/scenes/game_scene.hpp). Es la clase
más grande del juego y actúa como orquestador. Posee:
- El mundo físico [Physics::PhysicsWorld](source/core/physics/physics_world.hpp)
(integración cinemática + colisiones físicas).
- Pools de tamaño **fijo**: `std::array<Ship, 2>`,
`std::array<Enemy, MAX_ORNIS>` (15), `std::array<Bullet, MAX_BULLETS_TOTAL>` (6:
P1=[0,1,2], P2=[3,4,5]).
- Estado de partida: vidas, score y *death timers* por jugador, máquina de
game over (`GameOverState`: `NONE/CONTINUE/GAME_OVER`), continues usados.
- El stage system, los efectos visuales, y los `DemoPilot` (uno por nave).
### 10.1 Orquestación por frame
[GameScene::update()](source/game/scenes/game_scene.cpp) es un orquestador delgado;
cada paso es una función privada (descompuesto para reducir complejidad cognitiva):
```cpp
void GameScene::update(float dt) {
if (ServiceMenu abierto) return; // pausa global (draw sí sigue)
stepPhysics(dt);
if (mode == DEMO) { if (stepDemo(dt)) return; }
else if (game_over_state_ == NONE) { stepShootingInput(); stepMidGameJoin(); }
if (stepContinueScreen(dt)) return;
if (stepGameOver(dt)) return;
stepDeathSequence(dt);
stepStageStateMachine(dt);
}
```
El corazón del gameplay es
[stepStageStateMachine](source/game/scenes/game_scene.hpp#L166), que despacha según
el estado del stage; en `PLAYING`,
[runStagePlaying](source/game/scenes/game_scene.hpp#L169) ejecuta: WaveRunner
(spawns) → IA de cada enemigo → control de naves
([updateShipsControl](source/game/scenes/game_scene.cpp), que en demo usa
`applyMovement` con el control del pilot y fuera de demo usa `processInput`) →
detección de colisiones ([runCollisionDetections](source/game/scenes/game_scene.hpp#L176)).
`draw()` despacha de forma análoga según `GameOverState` y el estado del stage, y
siempre pinta la cortinilla al final.
### 10.2 Entidades
Las tres heredan de `Entities::Entity` ([entity.hpp](source/core/entities/entity.hpp)):
- **[Ship](source/game/entities/ship.hpp)** — nave del jugador. `processInput()`
(humano) y `applyMovement()` (usado por la IA demo). Estados: activa,
invulnerable (parpadeo tras spawn), herida (`hurt`). Al morir genera debris con
la inercia heredada.
- **[Enemy](source/game/entities/enemy.hpp)** — 5 tipos (`EnemyType`: `PENTAGON`,
`SQUARE`, `PINWHEEL`, `STAR`, `ORB`). Toda su config (físicas, IA, animación,
eventos) viene del **YAML** vía [EnemyRegistry](source/game/entities/enemy_registry.hpp).
Tiene salud (la mayoría HP=1; `ORB` HP=10) y estado *wounded* (parpadeo).
- **[Bullet](source/game/entities/bullet.hpp)** — con `owner_id` (0=P1, 1=P2,
≥16=enemigo) y `prev_position` para colisión *swept* (la bala que cruza un enemigo
entre dos frames). Config en [BulletRegistry](source/game/entities/bullet_registry.hpp).
### 10.3 IA de enemigos: declarativa
Los enemigos **no** tienen comportamiento hardcoded. El YAML describe:
- Una **primitiva de movimiento** (`MovementType` en
[enemy_ai.hpp](source/game/entities/enemy_ai.hpp)): `ZIGZAG`, `TRACKING`,
`RECTILINEAR_PROXIMITY`, `WANDER`, `CHASE`, `FLEE`.
- **Acciones de tick** periódicas (p. ej. `SHOOT`).
- **Eventos** (`on_hit`, `on_no_health`, `on_hurt_end`, `on_destroy`) con acciones
(`APPLY_IMPULSE`, `DECREASE_HEALTH`, `CREATE_DEBRIS`, `ADD_SCORE`, `FLASH`,
`FIRE_BULLET`, `DESTROY`, ...).
Dos sistemas los ejecutan:
- **[EnemyAiSystem](source/game/systems/enemy_ai_system.hpp)** — `move()` aplica la
primitiva de movimiento; `tick()` añade las acciones periódicas. Helper
`findNearestShipPosition()` para las primitivas que buscan al jugador.
- **[EnemyEventDispatcher](source/game/systems/enemy_event_dispatcher.hpp)** —
ejecuta las acciones declarativas cuando se dispara un evento.
### 10.4 Colisiones
[CollisionSystem](source/game/systems/collision_system.hpp) recibe un struct
`Context` (punteros a ships/enemies/bullets, managers de efectos, timers, scores,
vidas y un callback `on_player_hit`) que GameScene construye en
[buildCollisionContext](source/game/scenes/game_scene.hpp#L174). Detecta:
bala↔enemigo, nave↔enemigo, bala↔jugador (fuego amigo / autodisparo), bala
enemiga↔nave, y balas fuera del área. Reglas observadas: el primer impacto deja al
enemigo *wounded*; el segundo lo destruye y suma score. La nave entra en `hurt` al
primer toque y muere al segundo durante ese estado.
### 10.5 Stages y oleadas
- **[StageManager](source/game/stage_system/stage_manager.hpp)** — FSM del stage
(`EstatStage`): `INIT_HUD` (anima el HUD, 3 s) → `LEVEL_START` ("ENEMY INCOMING",
3 s, arranca `game.ogg`) → `PLAYING``LEVEL_COMPLETED` ("GOOD JOB COMMANDER!",
3 s) → siguiente stage. `initDemo(stage_id)` arranca directamente en `PLAYING`
para el attract mode.
- **[WaveRunner](source/game/stage_system/wave_runner.hpp)** — emite los enemigos de
cada oleada según `spawn_interval` y avanza cuando se cumple `next` (`all_dead`,
`timeout`, o ambos).
- **[StageConfig](source/game/stage_system/stage_config.hpp)** /
[StageLoader](source/game/stage_system/stage_loader.hpp) — modelo y carga del
YAML de stages.
### 10.6 Dos capas de colisión: física vs gameplay
Conviene no confundirlas, porque conviven:
**1. Física** — [PhysicsWorld](source/core/physics/physics_world.hpp) /
[physics_world.cpp](source/core/physics/physics_world.cpp). Es un mundo 2D
minimalista de arcade. Cada frame, `update(dt)` hace tres pasos:
1. **Integración** semi-implícita de Euler con damping exponencial
(`v += (F·invMass)·dt; v *= exp(-damping·dt); x += v·dt`) sobre cada
[RigidBody](source/core/physics/rigid_body.hpp) no estático. Un cuerpo con
`mass=0` (`inverse_mass=0`) es estático (masa infinita).
2. **Rebote contra los bordes** del `PLAYAREA` (`resolveBoundsCollisions`): reposiciona
el cuerpo dentro del rect y refleja la componente normal de la velocidad por su
`restitution`. Antes de reflejar, invoca un `BoundsHitCallback` opcional con la
velocidad de impacto entrante (lo usa GameScene para los efectos de borde).
3. **Colisiones cuerpo-cuerpo** (`resolveBodyCollisions`): broadphase trivial
**O(n²)** (suficiente para ~23 cuerpos), círculo-círculo, con corrección posicional
de penetración + **impulso elástico** `j = -(1+e)(v_rel·n) / (1/mₐ + 1/m_b)`
(referencia Box2D / Chris Hecker, en `resolveBodyPair`). Los cuerpos con `radius=0`
(las balas, cinemáticas puras) **no** participan aquí.
Los `RigidBody` los poseen las entidades; el mundo solo guarda punteros no-owning
(`addBody`/`removeBody`).
**2. Gameplay** — [collision_system.cpp](source/game/systems/collision_system.cpp)
(ver [§10.4](#104-colisiones)), que decide *qué pasa* (daño, score, muerte). Usa los
helpers de [collision.hpp](source/core/physics/collision.hpp): `checkCollision`
(círculo-círculo discreto, distancia al cuadrado sin `sqrt`) y `checkCollisionSwept`
(segment-círculo, para que una bala rápida no atraviese un enemigo entre frames —
*anti-tunneling*). Estos checks usan el `collision_radius` de la **entidad**
(con amplificador opcional de hitbox), no el `radius` del body.
En resumen: la **física** mueve y rebota los cuerpos; el **gameplay** detecta los
contactos relevantes para las reglas. Una bala no rebota físicamente (radius 0) pero sí
provoca daño vía el check *swept*.
---
## 11. IA del modo demo (attract)
El attract mode es una partida que se juega sola para atraer al jugador. Se activa
desde [TitleScene](source/game/scenes/title_scene.hpp) cuando el `idle_timer_` en el
estado `MAIN` supera el umbral de inactividad, y desde
[GameScene](source/game/scenes/game_scene.hpp) cuando `match_config_.mode == DEMO`.
La IA vive en [DemoPilot](source/game/systems/demo_pilot.hpp) /
[demo_pilot.cpp](source/game/systems/demo_pilot.cpp). Su diseño es explícito en la
cabecera: busca **parecer humano, no ser óptimo**. Características clave:
- **Solo lectura**: `DemoPilot::compute(ship, enemies, bullets, play_area, dt)`
devuelve un `Control{left,right,thrust,shoot}`. No lee `Input` ni muta entidades;
GameScene aplica el resultado vía `Ship::applyMovement` + `fireBullet`.
- **Escenarios curados**: hay 4 (`SCENARIOS` en
[demo_pilot.hpp:36-42](source/game/systems/demo_pilot.hpp#L36-L42)): stages
`{5,8,6,10}` con 1 o 2 naves IA. El `SceneContext` recuerda el índice y rota al
siguiente en cada entrada al demo.
**Lógica de decisión por prioridad** (verificado en `demo_pilot.cpp`, con sus
constantes):
1. **Esquiva de bala** — si una bala enemiga entrante está dentro de
`DODGE_SCAN_RADIUS = 190 px` y viene hacia la nave (`DODGE_HEADING_MIN = 0.25`),
maniobra perpendicular a la bala con sesgo al centro (`WALL_BIAS = 0.6`); no
dispara mientras esquiva.
2. **Sin enemigos** — deriva tranquila (giro lento).
3. **Peligro cercano** — si el objetivo está a menos de `DANGER_RADIUS = 95 px`, se
aleja con sesgo al centro.
4. **Combate** — apuntado con *lead* (`LEAD_TIME = 0.30 s`) más un error humano
(`AIM_JITTER_MAX = 0.10 rad`); dispara si el error es menor que
`FIRE_TOLERANCE = 0.18 rad` y el cooldown (`FIRE_COOLDOWN = 0.32 s`) lo permite;
se acerca si está más lejos que `APPROACH_RADIUS = 250 px`.
Temporización "humana": reevalúa el objetivo cada `RETARGET_INTERVAL = 0.15 s` y usa
una zona muerta de rotación (`ROTATE_DEADZONE = 0.05 rad`) para no oscilar. La demo
se rompe con cualquier input (vuelve a TITLE) o por timeout/muerte (vuelve a LOGO),
gestionado en [stepDemo](source/game/scenes/game_scene.hpp#L157).
---
## 12. Efectos visuales
Viven en [game/effects/](source/game/effects/) y son managers con pools:
- **[DebrisManager](source/game/effects/debris_manager.hpp)** — rompe una shape en
fragmentos que vuelan radialmente, heredando inercia del cuerpo y, opcionalmente,
el impulso de la bala que causó la muerte. Notifica al `Border` (bump) y al
`Playfield` (ripple). Lo usan muerte de nave/enemigo, balas fuera de área y las
explosiones del logo.
- **[FireworkManager](source/game/effects/firework_manager.hpp)** — bursts de fuegos
artificiales.
- **[FloatingScoreManager](source/game/effects/floating_score_manager.hpp)** —
números de puntuación flotantes ("+150").
- **[TrailManager](source/game/effects/trail_manager.hpp)** — estela tras las naves.
---
## 13. Configuración, constantes y convenciones
**Configuración:**
- **[EngineConfig](source/core/config/engine_config.hpp)** — struct POD con
ventana, rendering, audio, bindings de jugadores, locale, console. Es la config
persistente (`config.yaml`), gestionada por
[config_yaml](source/game/config_yaml.hpp) (`ConfigYaml::engine_config`,
`loadFromFile`/`saveToFile`).
- **[PostFxConfig](source/core/config/postfx_config.hpp)** — carga los `PostFxParams`
(bloom/flicker/fondo) desde YAML.
- **[GameConfig::MatchConfig](source/core/system/game_config.hpp)** — config no
persistente de la partida (jugadores activos, modo NORMAL/DEMO).
**Constantes y tipos:**
- **[core/types.hpp](source/core/types.hpp)** — `Vec2` / `Vec3` (agregados con
operadores y helpers como `length()`, `normalized()`, `dot()`, `cross()`).
- **[core/defaults/](source/core/defaults/)** — un fichero por dominio
(`window.hpp`, `rendering.hpp`, `audio.hpp`, `entities.hpp`, `notifier.hpp`...)
con todas las constantes por defecto. `game/constants.hpp` reexporta varias como
alias (`MAX_ORNIS`, `MAX_BULLETS`, `PI`) y añade helpers de área de juego.
**Convenciones de código** (de `.clang-tidy`, confirmadas en memoria de proyecto):
- Métodos en `camelBack`, tipos en `CamelCase`, constantes en `UPPER_CASE`.
- Comentarios mayormente en **catalán** (algunos en castellano); el código y los
identificadores mezclan catalán/castellano/inglés.
- Patrón recurrente: **singletons** con `init/get/destroy` y comprobación de
`nullptr` para degradación elegante.
- Patrón recurrente: descomposición de funciones grandes (`update`/`draw`) en
sub-pasos privados (`stepX`/`runX`/`drawXState`) para mantener baja la complejidad
cognitiva.
- Análisis estático (cppcheck/clang-tidy) corre vía git hooks
([.githooks/](.githooks/)); la política es **arreglar la causa**, no suprimir el
diagnóstico.
---
## 14. Guía de navegación
| Si quieres tocar… | Mira… |
|---|---|
| El arranque, orden de init, o el bucle de frame | [director.cpp](source/core/system/director.cpp) (`Director::iterate` / `handleEvent`) |
| Las callbacks de SDL | [main.cpp](source/main.cpp) |
| Añadir/cambiar una escena o una transición | [scene.hpp](source/core/system/scene.hpp), [scene_context.hpp](source/core/system/scene_context.hpp), `Director::buildScene` |
| Cómo se dibuja una línea / el frame de render | [line_renderer.cpp](source/core/rendering/line_renderer.cpp) → [gpu_frame_renderer.cpp](source/core/rendering/gpu/gpu_frame_renderer.cpp) |
| Bloom / flicker / fondo (post-proceso) | [gpu_postfx_pipeline](source/core/rendering/gpu/gpu_postfx_pipeline.hpp), [gpu_bloom_pipeline](source/core/rendering/gpu/gpu_bloom_pipeline.hpp), shaders en [shaders/](shaders/) |
| Crear/editar una figura vectorial | `data/shapes/**/*.shp` + [shape_loader.hpp](source/core/graphics/shape_loader.hpp) |
| El texto en pantalla | [vector_text.hpp](source/core/graphics/vector_text.hpp) |
| Eventos globales (teclas F, ESC, hotplug) | [global_events.cpp](source/core/system/global_events.cpp) |
| Controles, bindings, rebinding | [input.cpp](source/core/input/input.cpp), [define_inputs.cpp](source/core/input/define_inputs.cpp) |
| Reproducir música/efectos | [audio.hpp](source/core/audio/audio.hpp), [audio_adapter.hpp](source/core/audio/audio_adapter.hpp) |
| Cómo se cargan los recursos / el pack | [resource_loader.cpp](source/core/resources/resource_loader.cpp), [resource_pack.cpp](source/core/resources/resource_pack.cpp) |
| Reglas de la partida, vidas, game over | [game_scene.cpp](source/game/scenes/game_scene.cpp) |
| Comportamiento de un enemigo | su YAML en `data/entities/<tipo>/` + [enemy_ai_system.cpp](source/game/systems/enemy_ai_system.cpp) |
| Definir oleadas / dificultad de un nivel | [data/stages/stages.yaml](data/stages/stages.yaml) + [stage_manager.cpp](source/game/stage_system/stage_manager.cpp) |
| Colisiones | [collision_system.cpp](source/game/systems/collision_system.cpp) |
| La IA del modo demo | [demo_pilot.cpp](source/game/systems/demo_pilot.cpp) |
| Explosiones / partículas | [debris_manager.cpp](source/game/effects/debris_manager.cpp) |
| El menú de servicio (F12) | [service_menu.cpp](source/core/system/service_menu.cpp) |
| Textos traducibles | `data/locale/*.yaml` + [locale.cpp](source/core/locale/locale.cpp) |
| Constantes por defecto | [core/defaults/](source/core/defaults/) |
---
### Notas de honestidad sobre la cobertura
- Todas las secciones se verificaron leyendo directamente los ficheros y firmas
citados, incluyendo el **pipeline de compilación de shaders**
([§5.5](#55-shaders-fuentes-compilación-y-selección): `CMakeLists.txt` +
`tools/shaders/compile_spirv.cmake` + `shader_factory.hpp`) y el interior de la
**física** ([§10.6](#106-dos-capas-de-colisión-física-vs-gameplay):
`physics_world.cpp` + `collision.hpp` + `rigid_body.hpp`).
- Lo que **no** se ha trazado a fondo y queda como lectura directa del código si hace
falta: los detalles finos de animación de cada overlay (curvas de easing del
`Notifier`/`ServiceMenu`) y la coreografía interna completa de `LogoScene` y
`TitleScene` (más allá de sus estados). Son descriptivos, no estructurales.
</content>
</invoke>
+29 -2
View File
@@ -84,9 +84,18 @@ else
endif endif
.PHONY: all debug release _windows-release _macos-release _linux-release \ .PHONY: all debug release _windows-release _macos-release _linux-release \
run run-debug clean rebuild show-version pack \ run run-debug clean rebuild show-version pack controllerdb \
format format-check tidy tidy-fix cppcheck hooks-install help format format-check tidy tidy-fix cppcheck hooks-install help
# Còpia del gamecontrollerdb.txt (si existeix) al directori de build, perquè
# director.cpp el resolgui via resource_base = directori de l'executable.
# Silenciós si el fitxer no existeix (l'usuari encara no ha fet `make controllerdb`).
ifeq ($(OS),Windows_NT)
CP_CONTROLLERDB = @powershell -Command "if (Test-Path 'gamecontrollerdb.txt') { Copy-Item 'gamecontrollerdb.txt' -Destination '$(BUILDDIR)' -Force }"
else
CP_CONTROLLERDB = @if [ -f gamecontrollerdb.txt ]; then cp gamecontrollerdb.txt $(BUILDDIR)/; fi
endif
# ============================================================================== # ==============================================================================
# COMPILACIÓ # COMPILACIÓ
# ============================================================================== # ==============================================================================
@@ -98,10 +107,12 @@ endif
all: all:
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS) @cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
@cmake --build $(BUILDDIR) -j$(JOBS) @cmake --build $(BUILDDIR) -j$(JOBS)
$(CP_CONTROLLERDB)
debug: debug:
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS) @cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
@cmake --build $(BUILDDIR) -j$(JOBS) @cmake --build $(BUILDDIR) -j$(JOBS)
$(CP_CONTROLLERDB)
run: all run: all
@./$(BUILDDIR)/$(PROJECT) @./$(BUILDDIR)/$(PROJECT)
@@ -138,6 +149,7 @@ _linux-release:
# Còpia de fitxers # Còpia de fitxers
cp $(BUILDDIR)/resources.pack "$(RELEASE_FOLDER)" cp $(BUILDDIR)/resources.pack "$(RELEASE_FOLDER)"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
cp README.md "$(RELEASE_FOLDER)" cp README.md "$(RELEASE_FOLDER)"
@[ -f LICENSE ] && cp LICENSE "$(RELEASE_FOLDER)" || true @[ -f LICENSE ] && cp LICENSE "$(RELEASE_FOLDER)" || true
cp "$(TARGET_FILE)" "$(RELEASE_FILE)" cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
@@ -166,6 +178,7 @@ _windows-release:
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}" @powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
@powershell -Command "Copy-Item -Path '$(BUILDDIR)/resources.pack' -Destination '$(RELEASE_FOLDER)'" @powershell -Command "Copy-Item -Path '$(BUILDDIR)/resources.pack' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)' }" @powershell -Command "if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)' }"
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'" @powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "if (Test-Path 'release\windows\dll') { Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)' }" @powershell -Command "if (Test-Path 'release\windows\dll') { Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)' }"
@@ -189,7 +202,7 @@ _macos-release:
# Compila la versió Apple Silicon # Compila la versió Apple Silicon
@cmake -S . -B $(BUILDDIR)/arm $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release \ @cmake -S . -B $(BUILDDIR)/arm $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 \ -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=13.3 \
-DMACOS_BUNDLE=ON $(CMAKE_DEFS) -DMACOS_BUNDLE=ON $(CMAKE_DEFS)
@cmake --build $(BUILDDIR)/arm -j$(JOBS) @cmake --build $(BUILDDIR)/arm -j$(JOBS)
@@ -206,6 +219,7 @@ _macos-release:
# Còpia de recursos i metadades del bundle # Còpia de recursos i metadades del bundle
cp $(BUILDDIR)/arm/resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources" cp $(BUILDDIR)/arm/resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks" cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
cp release/icons/icon.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources" cp release/icons/icon.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents" cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
@@ -274,6 +288,19 @@ pack:
@cmake --build $(BUILDDIR) --target pack_resources @cmake --build $(BUILDDIR) --target pack_resources
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack @./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
# ==============================================================================
# DESCÀRREGA DE GAMECONTROLLERDB
# ==============================================================================
# Descarrega l'última versió de gamecontrollerdb.txt (mappings de gamepads
# mantinguts per la comunitat) a l'arrel del projecte. SDL el carrega via
# filesystem real (no dins resources.pack) i s'ha de copiar al costat del binari
# en cada build (gestionat per CP_CONTROLLERDB a `all`/`debug` i pels release targets).
controllerdb:
@echo "Descargando gamecontrollerdb.txt..."
curl -fsSL https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt \
-o gamecontrollerdb.txt
@echo "gamecontrollerdb.txt actualizado"
# ============================================================================== # ==============================================================================
# CODE QUALITY (delegats a cmake) # CODE QUALITY (delegats a cmake)
# ============================================================================== # ==============================================================================
+22
View File
@@ -0,0 +1,22 @@
name: bullet
# Shape de la bala. El bounding_radius del .shp dóna el hitbox base (~3 px);
# scale el modula visualment i pel hitbox.
shape:
path: bullet/basic.shp
scale: 1.0
collision_factor: 1.0
# Cinemàtica pura: la bala no col·lisiona físicament al PhysicsWorld
# (body_.radius = 0 al spawn), però sí participa al gameplay via
# checkCollisionSwept. La mass i l'impact_momentum_factor es fan servir
# només per calcular l'impuls que rep l'enemic en impactar.
physics:
mass: 0.5
restitution: 0.0 # irrelevant (no rebota)
linear_damping: 0.0 # movement rectilini uniforme
angular_damping: 0.0
impact_momentum_factor: 3.0 # factor de transferència de moment bala→enemic
colors:
normal: [155, 255, 175] # verd laser
@@ -0,0 +1,21 @@
name: bullet_double
# Variant de bala "anular" (dos cercles concèntrics, aspecte d'aura de plasma).
# Pensada per a contra-atacs d'enemic (ex: orb dispara una bullet_double al
# jugador quan rep un impacte). Mateixa física que la bala bàsica del player;
# canvien la forma (cercle doble) i el color per llegir-se com a tret enemic
# distintiu (groc verdós vs. el verd laser del player o el roig de bullet_long).
shape:
path: bullet/double.shp
scale: 1.5
collision_factor: 1.0
physics:
mass: 0.5
restitution: 0.0
linear_damping: 0.0
angular_damping: 0.0
impact_momentum_factor: 4.0
colors:
normal: [200, 255, 80] # groc verdós (chartreuse) — contra-atac de l'orb
@@ -0,0 +1,19 @@
name: bullet_long
# Variant de bala més llarga, pensada per a bales d'enemic: més visible per al
# jugador i amb prou marge per reaccionar. La velocitat NO viu aquí: es passa
# a Bullet::fire() i la decideix qui dispara (l'AiTickAction).
shape:
path: bullet/long.shp
scale: 1.0
collision_factor: 0.5
physics:
mass: 0.5
restitution: 0.0
linear_damping: 0.0
angular_damping: 0.0
impact_momentum_factor: 3.0
colors:
normal: [255, 100, 100] # roig clar — diferencia visualment del verd laser del player
+86
View File
@@ -0,0 +1,86 @@
name: orb
ai_type: orb # Validat contra el directori; mapeja a EnemyType::ORB.
# Shape circular pròpia (anell exterior + anell interior + 6 radis + nucli),
# pensada per llegir-se com a "reactor / orb" amb més detall que els enemics
# petits.
shape:
path: enemy/orb.shp
scale: 1.0
collision_factor: 1.0
physics:
mass: 50.0 # Molt pesat: una bala el frena un poc però no el "envia a passejar".
speed: 50.0 # Avança decidit cap al ship (no és lent passiu, és amenaça constant).
rotation_delta_min: 0.3
rotation_delta_max: 1.5
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
ai:
# Persecució contínua del ship més proper. chase_strength alt (1.0 = ~1s
# per realinear-se) perquè, encara que una bala l'empentja lateralment,
# ràpidament torna a posar la seua proa cap al jugador.
movement:
type: chase
chase_strength: 1.0
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.5 # Una mica més llarg que els altres (és un boss).
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 54.0 # 1.5× del normal (alineat amb scale 1.5).
colors:
normal: [255, 140, 110] # taronja rosat (coral) — distintiu del boss orb.
wounded: [255, 220, 60]
score: 500 # 5x un enemic normal: aguanta 10x més.
# Estrenant el sistema HP: 10 unitats. Cada bala fa decrease_health + flash
# + create_debris_partial (xip a 0.3x) + create_fireworks_small (espurna).
# Al 10è hit (HP=0), on_no_health encadena destroy directe — sense passar
# per wounded (com Star). 10 HP ja és prou dificultat sense afegir un hit
# extra.
health: 10
events:
on_hit:
- action: fire_bullet # contra-atac: dispara bullet_double dirigida al jugador
bullet: bullet_double
bullet_speed: 200.0
aim_mode: aimed
- action: decrease_health # primer: si arriba a 0 dispara on_no_health
#- action: flash # feedback visual de damage parcial
- action: create_debris_partial # xip a 0.3x mida (sense ser letal)
#- action: create_fireworks_small # espurna a cada hit (12 punts, lent)
- action: apply_impulse # empenta el cos (skip si will_die)
on_no_health:
- action: destroy # mort directa, sense wounded
on_destroy:
- action: add_score
- action: create_debris # explosió completa
- action: create_fireworks
+72
View File
@@ -0,0 +1,72 @@
name: pentagon
ai_type: pentagon # Validat contra el directori; mapeja a EnemyType::PENTAGON.
shape:
path: enemy/pentagon.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics:
mass: 5.0
speed: 35.0 # px/s (esquivador lent)
rotation_delta_min: 0.75 # rad/s — rotació visual mínima
rotation_delta_max: 3.75 # rad/s — rotació visual màxima
restitution: 1.0 # rebot elàstic perfecte contra parets
linear_damping: 0.0 # manté velocitat (sense fricció)
angular_damping: 0.0
behavior:
# Pentagon: zigzag esquivador (canvi de direcció probabilístic per segon).
angle_change_max: 1.0 # rad — magnitud del canvi de direcció
zigzag_prob_per_second: 0.8
animation:
pulse: # respiració d'escala aleatòria
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel: # acceleració/desacceleració de rotació visual
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0 # segons en estat ferit abans d'explotar
blink_hz: 10.0 # parpelleig color normal ↔ wounded
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0 # px mínim respecte al player al spawn
colors:
normal: [0, 255, 255] # Cyan pur "esquivador"
wounded: [255, 220, 60] # Daurat (parpelleig al rebre impacte)
score: 100
events:
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
# decrease_health primer perquè si la mort cau aquí (segon hit durant wounded),
# el dispatcher salta la resta del chain (incloent apply_impulse) sobre el
# cos ja destruït.
on_hit:
- action: decrease_health
- action: apply_impulse
on_no_health:
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+69
View File
@@ -0,0 +1,69 @@
name: pinwheel
ai_type: pinwheel # Validat contra el directori; mapeja a EnemyType::PINWHEEL.
shape:
path: enemy/pinwheel.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics:
mass: 4.0 # Més lleuger — àgil
speed: 50.0 # px/s (el més ràpid)
rotation_delta_min: 3.0 # rad/s — rotació base elevada
rotation_delta_max: 6.0
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
behavior:
# Pinwheel: movement rectilíniauniforme + boost de rotació visual prop de la nau.
rotation_proximity_multiplier: 3.0 # Multiplicador de rotació quan és prop de la nau
proximity_distance: 100.0 # Llindar de distància (px)
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0
colors:
normal: [255, 0, 255] # Magenta pur "agressiu"
wounded: [255, 220, 60]
score: 200
events:
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
on_hit:
- action: decrease_health
- action: apply_impulse
on_no_health:
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+49
View File
@@ -0,0 +1,49 @@
name: player_ship
# Shape de la nau. Resolt per ShapeLoader (busca a "shapes/<path>").
# Nota: el segon jugador rep un override del shape ("ship/wedge.shp") al ctor.
# Quan s'introdueixin variants reals de nau, es crearà un YAML separat
# per cada model.
#
# scale: multiplicador visual i de hitbox sobre la mida nativa del .shp (1.0 = mida del fitxer).
# collision_factor: ajust opcional del hitbox respecte el cercle circumscrit
# automàtic de la shape; tocar només si el feel del hitbox
# no quadra amb la silueta visual (default 1.0).
shape:
path: ship/arrow.shp
scale: 1.0
collision_factor: 1.0
physics:
mass: 10.0
restitution: 0.6
linear_damping: 1.5
angular_damping: 0.0
rotation_speed: 3.14 # rad/s (~180 deg/s, input-driven sense inercia)
acceleration: 400.0 # px/s^2 multiplicat per la massa quan THRUST
max_velocity: 180.0 # px/s (clamp post-integració per preservar feel arcade)
# Factor de transferència del moment lineal de la nau a l'enemic en el
# frame exacte que mor per col·lisió (afegit per damunt del rebot natural).
death_impact_factor: 0.3
invulnerability:
duration: 3.0 # segons d'invulnerabilitat post-respawn
blink_visible: 0.1 # segons visible per cicle de parpelleig
blink_invisible: 0.1 # segons invisible per cicle de parpelleig
hurt:
duration: 15.0 # segons en estat "ferit" abans de tornar a normal
blink_hz: 10.0 # freqüència parpelleig color normal <-> color hurt
# Empenta visual: la nau s'escala lleugerament amb la velocitat.
# Manté la sensació del Pascal original (0..MAX_VEL → 1.0..~1.5).
visual_thrust:
push_divisor: 33.33
scale_divisor: 12.0
colors:
normal: [255, 255, 255] # blanc neutre
hurt: [255, 0, 0] # roig pur (estat ferit)
weapon:
bullet_speed: 700.0 # velocitat escalar de la bullet (px/s)
+70
View File
@@ -0,0 +1,70 @@
name: square
ai_type: square # Validat contra el directori; mapeja a EnemyType::SQUARE.
shape:
path: enemy/square.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics:
mass: 8.0 # Més pesat — "tanc"
speed: 40.0 # px/s (velocitat mitjana)
rotation_delta_min: 0.3 # rad/s — rotació lenta
rotation_delta_max: 1.5
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
ai:
# Square: persecució contínua del ship més proper (steering suau, "tanc lent").
movement:
type: chase
chase_strength: 0.5 # Força/segon de la LERP cap a la direcció ideal (1.0 = ~1s per realinear)
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0
colors:
normal: [255, 0, 0] # Roig pur "tanc"
wounded: [255, 220, 60]
score: 150
events:
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
on_hit:
- action: decrease_health
- action: apply_impulse
on_no_health:
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+77
View File
@@ -0,0 +1,77 @@
name: star
ai_type: star # Validat contra el directori; mapeja a EnemyType::STAR.
shape:
path: enemy/star.shp
scale: 0.7 # Lleugerament més petit que els altres enemics per diferenciar visualment.
collision_factor: 1.0
physics:
mass: 5.0
speed: 35.0 # Mateixos paràmetres que pentagon (esquivador lent).
rotation_delta_min: 0.75
rotation_delta_max: 3.75
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
ai:
# Movement: zigzag esquivador (com Pentagon).
movement:
type: zigzag
angle_change_max: 1.0
zigzag_prob_per_second: 0.8
# Accions periòdiques: cada ~2.5s dispara una bala apuntada al ship més proper.
tick:
- action: shoot
interval: 2.5
aim_mode: aimed # apunta al ship més proper (atan2)
jitter_rad: 0.0 # sense soroll: tret perfecte
bullet: bullet_long # variant més visible per al jugador
bullet_speed: 150.0 # px/s — prou lenta per reaccionar
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0
colors:
normal: [255, 255, 0] # Groc estrella
wounded: [255, 220, 60]
score: 100
events:
# STAR: mor al primer impacte, sense passar per wounded.
# HP=1 (default): decrement → on_no_health → destroy directe (sense wounded).
on_hit:
- action: decrease_health
on_no_health:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+4
View File
@@ -13,6 +13,7 @@ notification:
antialias_off: "AA INACTIU" antialias_off: "AA INACTIU"
postfx_on: "POSTPROCESSAT ACTIU" postfx_on: "POSTPROCESSAT ACTIU"
postfx_off: "POSTPROCESSAT INACTIU" postfx_off: "POSTPROCESSAT INACTIU"
screenshot: "IMATGE {file} GUARDADA A {folder}"
locale_switched: "IDIOMA: {lang}" locale_switched: "IDIOMA: {lang}"
gamepad_connected: "{name} CONNECTAT" gamepad_connected: "{name} CONNECTAT"
gamepad_disconnected: "{name} DESCONNECTAT" gamepad_disconnected: "{name} DESCONNECTAT"
@@ -27,6 +28,9 @@ hud:
title: title:
press_start: "PREMEU START PER JUGAR" press_start: "PREMEU START PER JUGAR"
demo:
banner: "MODE DEMO - PREMEU START"
game_screen: game_screen:
game_over: "FI DEL JOC" game_over: "FI DEL JOC"
continue: "CONTINUAR" continue: "CONTINUAR"
+4
View File
@@ -12,6 +12,7 @@ notification:
antialias_off: "AA OFF" antialias_off: "AA OFF"
postfx_on: "POSTPROCESS ON" postfx_on: "POSTPROCESS ON"
postfx_off: "POSTPROCESS OFF" postfx_off: "POSTPROCESS OFF"
screenshot: "IMAGE {file} SAVED AT {folder}"
locale_switched: "LANGUAGE: {lang}" locale_switched: "LANGUAGE: {lang}"
gamepad_connected: "{name} CONNECTED" gamepad_connected: "{name} CONNECTED"
gamepad_disconnected: "{name} DISCONNECTED" gamepad_disconnected: "{name} DISCONNECTED"
@@ -26,6 +27,9 @@ hud:
title: title:
press_start: "PRESS START TO PLAY" press_start: "PRESS START TO PLAY"
demo:
banner: "DEMO MODE - PRESS START"
game_screen: game_screen:
game_over: "GAME OVER" game_over: "GAME OVER"
continue: "CONTINUE" continue: "CONTINUE"
@@ -1,6 +1,6 @@
# bullet.shp - Projectil (octàgon, radi=3) # bullet/basic.shp - Projectil (octàgon, radi=3)
name: bullet name: basic
scale: 1.0 scale: 1.0
center: 0, 0 center: 0, 0
+17
View File
@@ -0,0 +1,17 @@
# bullet/double.shp - Bala anular (dos cercles concèntrics)
# © 2026 JailDesigner
#
# Dos octàgons concèntrics al centre (0,0):
# - Exterior: radi 4 (lleugerament més gran que la bala estàndard, radi 3)
# - Interior: radi 2 (lleugerament més petit que la bala estàndard)
# Aspecte d'anell / aura de plasma. Bounding radius natiu = 4.
name: double
scale: 1.0
center: 0, 0
# Cercle exterior (octàgon, radi 4)
polyline: 0,-4 2.83,-2.83 4,0 2.83,2.83 0,4 -2.83,2.83 -4,0 -2.83,-2.83 0,-4
# Cercle interior (octàgon, radi 2)
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
+32
View File
@@ -0,0 +1,32 @@
# bullet/long.shp - Bala allargada vertical (dos mig-octàgons + dos costats)
# © 2026 JailDesigner
#
# Càpsula orientada al llarg de l'eix Y: la bala viatja segons el seu angle
# de moviment (angle=0 = Y negatiu), i així s'estira en la direcció de vol.
# Es dibuixen només els segments exteriors per evitar veure la unió interna
# dels dos cercles; el resultat visual són dos "mig-octàgons" separats per
# un petit gap al centre, units pels dos costats verticals.
#
# Geometria:
# Mig-octàgon superior (radi 3) centrat a (0, -3)
# Mig-octàgon inferior (radi 3) centrat a (0, 3)
# Punt extrem superior: (0, -6)
# Punt extrem inferior: (0, 6)
# Bounding radius natiu = 6 (extrem vertical a y=±6).
# collision_factor al YAML compensa el bounding doble (0.5 → hitbox ≈ 3).
name: long
scale: 1.0
center: 0, 0
# Mig-octàgon superior (5 vèrtexs: del cantó dret cap al punt extrem i a l'esquerre)
polyline: 3,-3 2.12,-5.12 0,-6 -2.12,-5.12 -3,-3
# Mig-octàgon inferior
polyline: 3,3 2.12,5.12 0,6 -2.12,5.12 -3,3
# Costat dret (uneix extrem inferior del mig superior amb extrem superior del mig inferior)
polyline: 3,-3 3,3
# Costat esquerre
polyline: -3,-3 -3,3
@@ -1,7 +1,7 @@
# star.shp - Estrella per a starfield # effect/starfield.shp - Estrella per a starfield
# © 2026 JailDesigner # © 2026 JailDesigner
name: star name: starfield
scale: 1.0 scale: 1.0
center: 0, 0 center: 0, 0
@@ -1,4 +1,4 @@
# title_flash.shp - Sparkle 4-puntes amb costats còncaus (Atari-style) # effect/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 # 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. # quadràtica amb control point ±8). 5 punts per arc subdividint la corba.
+32
View File
@@ -0,0 +1,32 @@
# enemy/orb.shp - ORNI enemic gegant (orb circular, doble anell amb radis)
# © 2026 JailDesigner
#
# Forma "reactor / boss circular" — més detall que els enemics petits perquè
# es renderitza a escala 1.5x i ha de llegir-se com a amenaça gran.
# - Anell exterior: dodecàgon (12 vèrtexs) — aparença circular suau, radi 20.
# - Anell interior: hexàgon (6 vèrtexs, rotat 30°) — radi 10.
# - 6 radis curts que connecten l'anell interior amb l'exterior.
# - Petit "+" central com a nucli.
# Bounding radius natiu = 20 (alineat amb la resta d'enemics).
name: orb
scale: 1.0
center: 0, 0
# Anell exterior (dodecàgon, vèrtex apuntant amunt)
polyline: 0,-20 10,-17.32 17.32,-10 20,0 17.32,10 10,17.32 0,20 -10,17.32 -17.32,10 -20,0 -17.32,-10 -10,-17.32 0,-20
# Anell interior (hexàgon, vèrtex apuntant a la dreta — rotat 30° respecte l'exterior)
polyline: 5,-8.66 10,0 5,8.66 -5,8.66 -10,0 -5,-8.66 5,-8.66
# 6 radis: del vèrtex de l'hexàgon interior al vèrtex corresponent del dodecàgon exterior
line: 5,-8.66 10,-17.32
line: 10,0 20,0
line: 5,8.66 10,17.32
line: -5,8.66 -10,17.32
line: -10,0 -20,0
line: -5,-8.66 -10,-17.32
# Nucli central: petit "+" (2 segments creuats, radi 3)
line: -3,0 3,0
line: 0,-3 0,3
@@ -1,6 +1,6 @@
# enemy_pentagon.shp - ORNI enemic (pentàgon doble concentric, radi exterior=20) # enemy/pentagon.shp - ORNI enemic (pentàgon doble concentric, radi exterior=20)
name: enemy_pentagon name: pentagon
scale: 1.0 scale: 1.0
center: 0, 0 center: 0, 0
@@ -1,7 +1,7 @@
# enemy_pinwheel.shp - ORNI enemic (molinillo de 4 triangles) # enemy/pinwheel.shp - ORNI enemic (molinillo de 4 triangles)
# © 2026 JailDesigner # © 2026 JailDesigner
name: enemy_pinwheel name: pinwheel
scale: 1.0 scale: 1.0
center: 0, 0 center: 0, 0
@@ -1,6 +1,6 @@
# enemy_square.shp - ORNI enemic (rombe, radi=20) + ull amb pupil·la al centre # enemy/square.shp - ORNI enemic (rombe, radi=20) + ull amb pupil·la al centre
name: enemy_square name: square
scale: 1.0 scale: 1.0
center: 0, 0 center: 0, 0
+15
View File
@@ -0,0 +1,15 @@
# enemy/star.shp - ORNI enemic (estrella de 5 puntes, només perímetre)
# © 2026 JailDesigner
#
# Pentagrama clàssic: 5 vèrtexs exteriors (radi 20) alternant amb 5 vèrtexs
# interiors (radi 7.64 = 20/φ² ≈ proporció àuria) per donar puntes esveltes.
# Vèrtex apuntant amunt (igual que enemy_pentagon).
#
# Sense línies interiors: una única polyline que recorre el perímetre.
# Bounding radius natiu ≈ 20 (alineat amb pentagon/square/pinwheel).
name: star
scale: 1.0
center: 0, 0
polyline: 0,-20 4.49,-6.18 19.02,-6.18 7.27,2.36 11.76,16.18 0,7.64 -11.76,16.18 -7.27,2.36 -19.02,-6.18 -4.49,-6.18 0,-20
+9
View File
@@ -0,0 +1,9 @@
# char_underscore.shp - Símbolo _ (barra baja)
# Dimensiones: 20×40 (blocky display)
name: char_underscore
scale: 1.0
center: 10, 20
# Línea horizontal abajo (bajo la baseline de las letras)
line: 3,33 17,33
-8
View File
@@ -1,8 +0,0 @@
# ship.shp - Nau del jugador 1
# Triangle amb base còncava (punta de fletxa)
name: ship
scale: 1.0
center: 0, 0
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
+7
View File
@@ -0,0 +1,7 @@
# ship/arrow.shp - Nau del jugador 1 (triangle amb base còncava, punta de fletxa)
name: arrow
scale: 1.0
center: 0, 0
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
@@ -1,7 +1,7 @@
# ship2.shp - Nau del jugador 2 (interceptor amb ales) # ship/interceptor.shp - Interceptor amb ales laterals pronunciades
# © 2026 JailDesigner # © 2026 JailDesigner
name: ship2 name: interceptor
scale: 1.0 scale: 1.0
center: 0, 0 center: 0, 0
@@ -1,7 +1,6 @@
# ship2.shp - Nau del jugador 2 # ship/wedge.shp - Nau del jugador 2 (triangle amb cercle central)
# Triangle amb cercle central (distintiu visual)
name: ship2 name: wedge
scale: 1.0 scale: 1.0
center: 0, 0 center: 0, 0
Binary file not shown.
+158 -143
View File
@@ -1,168 +1,183 @@
# stages.yaml - Configuració de les 10 etapes d'Orni Attack # stages.yaml - Configuració de les fases d'Orni Attack
# © 2026 JailDesigner # © 2026 JailDesigner
#
# Format basat en onades (waves). Cada wave:
# - spawn: list d'enemics a generar, en ordre.
# - spawn_interval: segons entre spawns interns (default 0 = simultanis).
# - next: condició per avançar a la wave següent.
# - "all_dead" / "end" → quan tots els enemics de l'arena han mort.
# - { timeout: T } → quan han passat T segons des de l'inici de la wave.
# - { all_dead: true, timeout: T } → el que arribe abans (amuntegament si vas lent).
#
# Tipus d'enemic: pentagon, square (alias: cuadrado), pinwheel (alias: molinillo), star, orb.
metadata: metadata:
version: "1.0" version: "2.0"
total_stages: 10 total_stages: 10
description: "Progressive difficulty curve from novice to expert" description: "Wave-based progression"
stages: stages:
# STAGE 1: Tutorial - Mix de tots els tipus, velocitat lenta # STAGE 1 Tutorial: contacte amb pentagons i un cuadrado.
# (Test: també hi ha un orb a la primera onada per provar el contra-atac.)
- stage_id: 1 - stage_id: 1
total_enemies: 50 multipliers: { velocity: 0.85, rotation: 0.9, tracking: 0.3 }
spawn_config: waves:
mode: "progressive" - spawn: [pentagon, pentagon, orb]
initial_delay: 0.3 spawn_interval: 0.6
spawn_interval: 0.4 next: all_dead
enemy_distribution: - spawn: [pentagon, pentagon, square]
pentagon: 34 spawn_interval: 0.5
cuadrado: 33 next: all_dead
molinillo: 33 - spawn: [pentagon, pentagon, square, square]
difficulty_multipliers: spawn_interval: 0.4
speed_multiplier: 0.7 next: end
rotation_multiplier: 0.8
tracking_strength: 0.0
# STAGE 2: Introduction to tracking enemies # STAGE 2 — Apareixen molinillos.
- stage_id: 2 - stage_id: 2
total_enemies: 7 multipliers: { velocity: 0.95, rotation: 1.0, tracking: 0.4 }
spawn_config: waves:
mode: "progressive" - spawn: [pentagon, pentagon, pentagon]
initial_delay: 1.5 spawn_interval: 0.5
spawn_interval: 2.5 next: all_dead
enemy_distribution: - spawn: [pinwheel]
pentagon: 70 next: all_dead
cuadrado: 30 - spawn: [pentagon, square, pinwheel]
molinillo: 0 spawn_interval: 0.6
difficulty_multipliers: next: all_dead
speed_multiplier: 0.85 - spawn: [pinwheel, pinwheel, pentagon]
rotation_multiplier: 0.9 spawn_interval: 0.5
tracking_strength: 0.3 next: end
# STAGE 3: All enemy types, normal speed # STAGE 3 — Primer orb (HP=10).
- stage_id: 3 - stage_id: 3
total_enemies: 10 multipliers: { velocity: 1.0, rotation: 1.0, tracking: 0.5 }
spawn_config: waves:
mode: "progressive" - spawn: [pentagon, pentagon, square]
initial_delay: 1.0 spawn_interval: 0.4
spawn_interval: 2.0 next: all_dead
enemy_distribution: - spawn: [orb]
pentagon: 50 next: { all_dead: true, timeout: 12.0 }
cuadrado: 30 - spawn: [pinwheel, pinwheel]
molinillo: 20 spawn_interval: 0.5
difficulty_multipliers: next: all_dead
speed_multiplier: 1.0 - spawn: [pentagon, square, pinwheel, pinwheel]
rotation_multiplier: 1.0 spawn_interval: 0.4
tracking_strength: 0.5 next: end
# STAGE 4: Increased count, faster enemies # STAGE 4 — Pressió creixent: timeouts curts que poden encavalcar onades.
- stage_id: 4 - stage_id: 4
total_enemies: 12 multipliers: { velocity: 1.05, rotation: 1.1, tracking: 0.6 }
spawn_config: waves:
mode: "progressive" - spawn: [pentagon, pentagon, pentagon]
initial_delay: 0.8 spawn_interval: 0.3
spawn_interval: 1.8 next: { all_dead: true, timeout: 5.0 }
enemy_distribution: - spawn: [square, square]
pentagon: 40 spawn_interval: 0.4
cuadrado: 35 next: { all_dead: true, timeout: 6.0 }
molinillo: 25 - spawn: [pinwheel, pinwheel, pinwheel]
difficulty_multipliers: spawn_interval: 0.4
speed_multiplier: 1.1 next: all_dead
rotation_multiplier: 1.15 - spawn: [orb, pentagon, pentagon]
tracking_strength: 0.6 spawn_interval: 0.5
next: end
# STAGE 5: Maximum count reached # STAGE 5 — Apareix la star (zigzag clon del pentagon).
- stage_id: 5 - stage_id: 5
total_enemies: 15 multipliers: { velocity: 1.1, rotation: 1.2, tracking: 0.7 }
spawn_config: waves:
mode: "progressive" - spawn: [star, star]
initial_delay: 0.5 spawn_interval: 0.4
spawn_interval: 1.5 next: all_dead
enemy_distribution: - spawn: [pentagon, square, star]
pentagon: 35 spawn_interval: 0.4
cuadrado: 35 next: { all_dead: true, timeout: 6.0 }
molinillo: 30 - spawn: [pinwheel, pinwheel, star, star]
difficulty_multipliers: spawn_interval: 0.4
speed_multiplier: 1.2 next: all_dead
rotation_multiplier: 1.25 - spawn: [orb, square, square]
tracking_strength: 0.7 spawn_interval: 0.5
next: end
# STAGE 6: Molinillo becomes dominant # STAGE 6 — Densitat alta, mix amb timeouts agressius.
- stage_id: 6 - stage_id: 6
total_enemies: 15 multipliers: { velocity: 1.15, rotation: 1.25, tracking: 0.8 }
spawn_config: waves:
mode: "progressive" - spawn: [pentagon, pinwheel, pentagon, pinwheel]
initial_delay: 0.3 spawn_interval: 0.3
spawn_interval: 1.3 next: { all_dead: true, timeout: 5.0 }
enemy_distribution: - spawn: [square, square, star]
pentagon: 30 spawn_interval: 0.4
cuadrado: 30 next: { all_dead: true, timeout: 5.0 }
molinillo: 40 - spawn: [pinwheel, pinwheel, pinwheel]
difficulty_multipliers: spawn_interval: 0.3
speed_multiplier: 1.3 next: all_dead
rotation_multiplier: 1.4 - spawn: [orb, pinwheel, pinwheel]
tracking_strength: 0.8 spawn_interval: 0.4
next: end
# STAGE 7: High intensity, fast spawns # STAGE 7 — Tiradors i agressivitat.
- stage_id: 7 - stage_id: 7
total_enemies: 15 multipliers: { velocity: 1.25, rotation: 1.35, tracking: 0.9 }
spawn_config: waves:
mode: "progressive" - spawn: [square, square, square]
initial_delay: 0.2 spawn_interval: 0.5
spawn_interval: 1.0 next: { all_dead: true, timeout: 6.0 }
enemy_distribution: - spawn: [pinwheel, pinwheel, pentagon, pentagon]
pentagon: 25 spawn_interval: 0.3
cuadrado: 30 next: { all_dead: true, timeout: 5.0 }
molinillo: 45 - spawn: [star, star, star]
difficulty_multipliers: spawn_interval: 0.4
speed_multiplier: 1.4 next: all_dead
rotation_multiplier: 1.5 - spawn: [orb, pinwheel, pinwheel, square]
tracking_strength: 0.9 spawn_interval: 0.5
next: end
# STAGE 8: Expert level, 50% molinillos # STAGE 8 — Pressió constant.
- stage_id: 8 - stage_id: 8
total_enemies: 15 multipliers: { velocity: 1.35, rotation: 1.45, tracking: 1.0 }
spawn_config: waves:
mode: "progressive" - spawn: [pinwheel, pinwheel, pinwheel]
initial_delay: 0.1 spawn_interval: 0.3
spawn_interval: 0.8 next: { all_dead: true, timeout: 4.0 }
enemy_distribution: - spawn: [square, square, star, star]
pentagon: 20 spawn_interval: 0.3
cuadrado: 30 next: { all_dead: true, timeout: 5.0 }
molinillo: 50 - spawn: [orb]
difficulty_multipliers: next: { all_dead: true, timeout: 8.0 }
speed_multiplier: 1.5 - spawn: [pinwheel, pinwheel, square, star, pentagon]
rotation_multiplier: 1.6 spawn_interval: 0.3
tracking_strength: 1.0 next: end
# STAGE 9: Near-maximum difficulty # STAGE 9 — Quasi-final.
- stage_id: 9 - stage_id: 9
total_enemies: 15 multipliers: { velocity: 1.5, rotation: 1.6, tracking: 1.1 }
spawn_config: waves:
mode: "progressive" - spawn: [pinwheel, pinwheel, star, star]
initial_delay: 0.0 spawn_interval: 0.3
spawn_interval: 0.6 next: { all_dead: true, timeout: 4.0 }
enemy_distribution: - spawn: [orb, square, square]
pentagon: 15 spawn_interval: 0.4
cuadrado: 25 next: { all_dead: true, timeout: 8.0 }
molinillo: 60 - spawn: [pinwheel, pinwheel, pinwheel, pinwheel]
difficulty_multipliers: spawn_interval: 0.3
speed_multiplier: 1.6 next: { all_dead: true, timeout: 5.0 }
rotation_multiplier: 1.7 - spawn: [orb, pinwheel, pinwheel, square, star]
tracking_strength: 1.1 spawn_interval: 0.4
next: end
# STAGE 10: Final challenge, 70% molinillos # STAGE 10 — Repte final.
- stage_id: 10 - stage_id: 10
total_enemies: 15 multipliers: { velocity: 1.7, rotation: 1.8, tracking: 1.2 }
spawn_config: waves:
mode: "progressive" - spawn: [pinwheel, pinwheel, pinwheel, pinwheel]
initial_delay: 0.0 spawn_interval: 0.25
spawn_interval: 0.5 next: { all_dead: true, timeout: 4.0 }
enemy_distribution: - spawn: [orb, square, star]
pentagon: 10 spawn_interval: 0.4
cuadrado: 20 next: { all_dead: true, timeout: 6.0 }
molinillo: 70 - spawn: [pinwheel, pinwheel, star, star, square]
difficulty_multipliers: spawn_interval: 0.3
speed_multiplier: 1.8 next: { all_dead: true, timeout: 5.0 }
rotation_multiplier: 2.0 - spawn: [orb, orb, pinwheel, pinwheel, star]
tracking_strength: 1.2 spawn_interval: 0.4
next: end
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 537 KiB

+1 -1
View File
@@ -29,7 +29,7 @@
<key>CSResourcesFileMapped</key> <key>CSResourcesFileMapped</key>
<true/> <true/>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>10.15</string> <string>13.3</string>
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<true/> <true/>
<key>NSHumanReadableCopyright</key> <key>NSHumanReadableCopyright</key>
+11
View File
@@ -281,6 +281,17 @@ void Audio::enableMusic(bool value) {
setMusicVolume(config_.music_volume); setMusicVolume(config_.music_volume);
} }
// Silencia o restaura un grup de sons concret sense alterar config_ (el volum
// que l'usuari va triar) ni els altres grups. Silenciar posa la ganancia del
// grup a 0; restaurar-la torna al volum efectiu normal (que ja aplica els gates
// master/sound i el volum de l'usuari). A diferència de setSoundVolume, no
// xafa config_.sound_volume, así que el menu de servei segueix mostrant i
// operant el volum real durant la demo.
void Audio::silenceGroup(Group group, bool silenced) {
const float VOL = silenced ? 0.0F : effectiveVolume(config_.sound_volume, sound_enabled_);
engine_->setSoundVolume(VOL, static_cast<int>(group));
}
// Inicialitza SDL Audio y el motor Ja::Engine owned. // Inicialitza SDL Audio y el motor Ja::Engine owned.
void Audio::initSDLAudio() { void Audio::initSDLAudio() {
if (!SDL_Init(SDL_INIT_AUDIO)) { if (!SDL_Init(SDL_INIT_AUDIO)) {
+6
View File
@@ -128,6 +128,12 @@ class Audio {
// --- Configuración de sons --- // --- Configuración de sons ---
void enableSound(bool value); // Estableix l'estat dels sons (reaplica volum) void enableSound(bool value); // Estableix l'estat dels sons (reaplica volum)
void toggleSound() { enableSound(!sound_enabled_); } // Alterna l'estat dels sons (reaplica volum) void toggleSound() { enableSound(!sound_enabled_); } // Alterna l'estat dels sons (reaplica volum)
// Silencia (o restaura) un únic grup de sons sense tocar el volum cachejat
// de l'usuari ni la resta de grups. Pensat per a l'attract/demo: vol callar
// els SFX de joc (Group::GAME) pero mantenir els del menu de servei
// (Group::INTERFACE) i la música. En restaurar, reaplica el volum efectiu
// normal del canal (que ja respecta els gates master/sound).
void silenceGroup(Group group, bool silenced);
// --- Configuración de música --- // --- Configuración de música ---
void enableMusic(bool value); // Estableix l'estat de la música (reaplica volum) void enableMusic(bool value); // Estableix l'estat de la música (reaplica volum)
-1
View File
@@ -25,7 +25,6 @@
#include "core/defaults/physics.hpp" #include "core/defaults/physics.hpp"
#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/starfield_parallax.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"
+4 -3
View File
@@ -35,10 +35,11 @@ namespace Defaults::Music {
namespace Defaults::Sound { namespace Defaults::Sound {
constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras
constexpr const char* EXPLOSION = "effects/explosion.wav"; // Explosión constexpr const char* ENEMY_EXPLOSION = "effects/enemy_explosion.wav"; // Explosió d'enemic (debris default)
constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa constexpr const char* ENEMY_HIT = "effects/enemy_hit.wav"; // Impacte parcial a enemic (debris_partial — HP > 1)
constexpr const char* PLAYER_EXPLOSION = "effects/player_explosion.wav"; // Explosió de la nau del jugador
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* BULLET_ZAP = "effects/bullet_zap.wav"; // Bala desintegrant-se (qualsevol impacte o eixida de playarea)
constexpr const char* HURT = "effects/hurt.wav"; // Nau pròpia entra a 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
+30 -86
View File
@@ -1,101 +1,45 @@
// enemies.hpp - Configuració per tipus d'enemic (Pentagon/Square/Molinillo), spawn i scoring // enemies.hpp - Constants tècniques compartides per al sistema d'enemics.
// © 2026 JailDesigner // © 2026 JailDesigner
//
// Tots els paràmetres jugables (physics, animation, wounded, spawn,
// behavior, colors, scoring) viuen a data/entities/<type>/<type>.yaml i
// s'accedeixen via EnemyRegistry::get(EnemyType). Aquí només queda el
// que no és per personalitzar per tipus.
#pragma once #pragma once
#include "core/defaults/entities.hpp" namespace Defaults::Enemies::Spawn {
namespace Defaults::Enemies { // Sostre de reintents al cercar una posició de spawn que respecti el
// safety_distance del tipus. No és un paràmetre jugable: és el llindar
// tècnic abans de caure a un fallback aleatori amb advertència.
constexpr int MAX_SPAWN_ATTEMPTS = 50;
// Cuerpo físico común (valores por defecto del constructor) } // namespace Defaults::Enemies::Spawn
namespace Body {
constexpr float DEFAULT_MASS = 5.0F; // Más liviano que la nave (10.0)
constexpr float RESTITUTION = 1.0F; // Rebote elástico perfecto contra paredes
constexpr float LINEAR_DAMPING = 0.0F; // Sin fricción: mantienen velocidad
constexpr float ANGULAR_DAMPING = 0.0F;
} // namespace Body
// Pentagon (esquivador - zigzag evasion) namespace Defaults::Enemies::Visual {
namespace Pentagon {
constexpr float SPEED = 35.0F; // px/s (slightly slower)
constexpr float MASS = 5.0F; // Masa estándar
constexpr float ANGLE_CHANGE_PROB = 0.20F; // 20% per wall hit (frequent zigzag)
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 ROTATION_DELTA_MIN = 0.75F; // Min 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";
} // namespace Pentagon
// Square (perseguidor - tracks player) // Duració del "flash" que dispara l'acció FLASH (feedback per impacte
namespace Square { // parcial en enemics HP>1). Curt: l'efecte ha de llegir-se com un cop,
constexpr float SPEED = 40.0F; // px/s (medium speed) // no com una transició.
constexpr float MASS = 8.0F; // Más pesado, "tanque" constexpr float FLASH_DURATION = 0.08F;
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 ROTATION_DELTA_MIN = 0.3F; // Slow rotation [+50%]
constexpr float ROTATION_DELTA_MAX = 1.5F; // [+50%]
constexpr const char* SHAPE_FILE = "enemy_square.shp";
} // namespace Square
// Molinillo (agressiu - fast straight lines, proximity spin-up) } // namespace Defaults::Enemies::Visual
namespace Pinwheel {
constexpr float SPEED = 50.0F; // px/s (fastest)
constexpr float MASS = 4.0F; // Más liviano, ágil
constexpr float ANGLE_CHANGE_PROB = 0.05F; // 5% per wall hit (rare direction change)
constexpr float ANGLE_CHANGE_MAX = 0.3F; // Small angle adjustments
constexpr float ROTATION_DELTA_MIN = 3.0F; // Base rotation (rad/s) [+50%]
constexpr float ROTATION_DELTA_MAX = 6.0F; // [+50%]
constexpr float ROTATION_DELTA_PROXIMITY_MULTIPLIER = 3.0F; // Spin-up multiplier when near ship
constexpr float PROXIMITY_DISTANCE = 100.0F; // Distance threshold (px)
constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp";
} // namespace Pinwheel
// Animation parameters (shared) namespace Defaults::Enemies::Debris {
namespace Animation {
// Palpitation
constexpr float PULSE_TRIGGER_PROB = 0.01F; // 1% chance per second
constexpr float PULSE_DURATION_MIN = 1.0F; // Min duration (seconds)
constexpr float PULSE_DURATION_MAX = 3.0F; // Max duration (seconds)
constexpr float PULSE_AMPLITUD_MIN = 0.08F; // Min scale variation
constexpr float PULSE_AMPLITUD_MAX = 0.20F; // Max scale variation
constexpr float PULSE_FREQ_MIN = 1.5F; // Min frequency (Hz)
constexpr float PULSE_FREQ_MAX = 3.0F; // Max frequency (Hz)
// Rotation acceleration // Escala dels fragments per a l'acció CREATE_DEBRIS_PARTIAL (xip d'impacte
constexpr float ROTATION_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent] // en enemics HP>1). 0.3 = trossos petits, com de "casc esquerdat".
constexpr float ROTATION_ACCEL_DURATION_MIN = 3.0F; // Min transition time constexpr float PARTIAL_PIECE_SCALE = 0.3F;
constexpr float ROTATION_ACCEL_DURATION_MAX = 8.0F; // Max transition time
constexpr float ROTATION_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic]
constexpr float ROTATION_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic]
} // namespace Animation
// Wounded state (entre primer impacto y explosión) } // namespace Defaults::Enemies::Debris
namespace Wounded {
constexpr float DURATION = 1.0F; // Segundos en estado herido antes de explotar
constexpr float BLINK_HZ = 10.0F; // Frecuencia de parpadeo color tipo ↔ dorado
} // namespace Wounded
// Spawn safety and invulnerability system namespace Defaults::Enemies::Fireworks {
namespace Spawn {
// Safe spawn distance from player
constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F; // 3x ship radius
constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px
constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position
// Invulnerability system // Paràmetres del firework "petit" per a l'acció CREATE_FIREWORKS_SMALL
constexpr float INVULNERABILITY_DURATION = 3.0F; // Seconds // (feedback per impacte parcial en enemics HP>1). Pocs punts i baixa
constexpr float INVULNERABILITY_BRIGHTNESS_START = 0.3F; // Dim // velocitat: una espurna breu, no una explosió.
constexpr float INVULNERABILITY_BRIGHTNESS_END = 0.7F; // Normal (same as Defaults::Brightness::ENEMIC) constexpr int SMALL_N_POINTS = 20;
constexpr float INVULNERABILITY_SCALE_START = 0.0F; // Invisible constexpr float SMALL_SPEED = 250.0F;
constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size
} // namespace Spawn
// Scoring system (puntuación per type de enemy) } // namespace Defaults::Enemies::Fireworks
namespace Scoring {
constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s)
constexpr int SQUARE_SCORE = 150; // Square (perseguidor, 40 px/s)
constexpr int PINWHEEL_SCORE = 200; // Molinillo (agressiu, 50 px/s)
} // namespace Scoring
} // namespace Defaults::Enemies
+19 -4
View File
@@ -3,13 +3,28 @@
#pragma once #pragma once
#include <cstdint>
namespace Defaults::Entities { namespace Defaults::Entities {
constexpr int MAX_ORNIS = 15; constexpr int MAX_ORNIS = 15;
constexpr int MAX_BULLETS = 50; constexpr int MAX_BULLETS = 50; // per jugador (P1 + P2 = 2× aquest valor)
constexpr int MAX_ENEMY_BULLETS = 50; // pool reservat per a bales d'enemic
constexpr float SHIP_RADIUS = 12.0F; // Total real de slots a l'array global bullets_: zona P1, zona P2 i zona enemic.
constexpr float ENEMY_RADIUS = 20.0F; // Reservar zones impedeix que les bales d'enemic ocupin slots del jugador.
constexpr float BULLET_RADIUS = 3.0F; constexpr int MAX_BULLETS_TOTAL = (MAX_BULLETS * 2) + MAX_ENEMY_BULLETS;
constexpr int ENEMY_BULLET_START_IDX = MAX_BULLETS * 2;
// Convenció d'owner_id per a Bullet::fire:
// 0..1 = players (P1, P2)
// ENEMY_OWNER_BASE + index = enemic concret (per identificar el seu autoimpacte)
// Una bala mata a qualsevol col·lisió excepte amb el seu propi creador.
constexpr uint8_t ENEMY_OWNER_BASE = 128;
// SHIP_RADIUS / ENEMY_RADIUS / BULLET_RADIUS han migrat: ara cada entitat
// calcula el seu collision_radius com a
// shape.bounding_radius × shape.scale × shape.collision_factor
// a partir del seu YAML (data/entities/<name>/<name>.yaml).
} // namespace Defaults::Entities } // namespace Defaults::Entities
+16 -2
View File
@@ -10,7 +10,8 @@ namespace Defaults::Game {
constexpr int HEIGHT = 720; constexpr int HEIGHT = 720;
// Regles de partida // Regles de partida
constexpr int STARTING_LIVES = 3; // Initial lives constexpr int MAX_VIDES = 3; // Vides màximes per jugador (font única; el HUD en deriva els slots)
constexpr int STARTING_LIVES = MAX_VIDES; // S'arrenca amb les vides al màxim
constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation constexpr float DEATH_DURATION = 3.0F; // Seconds of death animation
constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over constexpr float GAME_OVER_DURATION = 5.0F; // Seconds to display game over
@@ -29,7 +30,7 @@ namespace Defaults::Game {
// 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_SPEED = 700.0F; // Velocidad escalar (px/s). Pascal: 7 px/frame × 20 FPS // BULLET_SPEED migrat a data/entities/player/player.yaml (weapon.bullet_speed).
// Transición LEVEL_START (mensajes aleatorios PRE-level) // Transición LEVEL_START (mensajes aleatorios PRE-level)
constexpr float LEVEL_START_DURATION = 3.0F; // Duración total constexpr float LEVEL_START_DURATION = 3.0F; // Duración total
@@ -39,6 +40,19 @@ namespace Defaults::Game {
constexpr float LEVEL_COMPLETED_DURATION = 3.0F; // Duración total constexpr float LEVEL_COMPLETED_DURATION = 3.0F; // Duración total
constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.05F; // ~150ms de typewriter (escan ràpid però visible) constexpr float LEVEL_COMPLETED_TYPING_RATIO = 0.05F; // ~150ms de typewriter (escan ràpid però visible)
// Attract mode: transició TÍTOL → DEMO. Primer un "dive" de càmera cap al
// punt de fuga (deixa enrere títol/naus/logo) i després una cortinilla negra
// que cau per tapar; a la demo, la cortinilla segueix caient i destapa.
namespace Dive {
constexpr float DURATION = 0.55F; // Durada del dive (s), amb acceleració (ease-in)
constexpr float CAMERA_DISTANCE = 450.0F; // Avanç de la càmera en +Z (passa les naus, a Z≈323)
constexpr float ZOOM_MAX = 7.0F; // Zoom final dels elements 2D (logo + peu) en travessar-los
} // namespace Dive
namespace Curtain {
constexpr float COVER_DURATION = 0.35F; // TÍTOL: la tela cau i tapa
constexpr float REVEAL_DURATION = 0.45F; // DEMO: la tela segueix caient i destapa
} // namespace Curtain
// Transición INIT_HUD (animación inicial del HUD) // Transición INIT_HUD (animación inicial del HUD)
constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado constexpr float INIT_HUD_DURATION = 3.0F; // Duración total del estado
+38
View File
@@ -5,6 +5,8 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cstdint>
namespace Defaults::Hud { namespace Defaults::Hud {
// Marcador (scoreboard inferior). Usado por GameScene::drawScoreboard() // Marcador (scoreboard inferior). Usado por GameScene::drawScoreboard()
@@ -12,6 +14,42 @@ namespace Defaults::Hud {
constexpr float SCOREBOARD_TEXT_SCALE = 0.85F; constexpr float SCOREBOARD_TEXT_SCALE = 0.85F;
constexpr float SCOREBOARD_TEXT_SPACING = 0.0F; constexpr float SCOREBOARD_TEXT_SPACING = 0.0F;
// Mode de presentació de les vides al marcador (no es canvia en calent;
// es defineix ací mentre no estiga decidit si el nombre de vides serà fix).
// SLOTS → naus en miniatura en posicions fixes (s'encenen/atenuen).
// DIGITS → número de 2 dígits (mateixa regla que el nivell: zeros a
// l'esquerra atenuats, dígit significatiu en endavant encès).
enum class LivesDisplay : std::uint8_t { SLOTS,
DIGITS };
constexpr LivesDisplay LIVES_DISPLAY = LivesDisplay::DIGITS;
// Ajust fi de l'alçada dels slots de vides respecte a l'alçada del glif del
// dígit: la silueta de la nau ompli menys que un dígit, així que un xicotet
// factor >1 la fa casar visualment amb les xifres (calibrat a ull).
constexpr float LIFE_SLOT_HEIGHT_FACTOR = 1.2F;
// Esquema de color del marcador: "per jugador + sistema". Cada jugador usa
// el SEU color (parella brillant/atenuat) en tot el seu bloc (punts + vides);
// el nivell central va sempre en verd de sistema. Colors plans i purs: el
// glow/bloom el posa el shader de postpro, NO s'horneja al color. Amb
// alpha=255 el line_renderer usa el color directament sense caure al fallback
// verd (Rendering::DEFAULT_LINE_COLOR).
namespace Colors {
// Jugador 1 → cian.
constexpr SDL_Color P1_BRIGHT = {.r = 41, .g = 231, .b = 255, .a = 255}; // #29E7FF
constexpr SDL_Color P1_DIM = {.r = 12, .g = 90, .b = 102, .a = 255}; // #0C5A66
// Jugador 2 → groc.
constexpr SDL_Color P2_BRIGHT = {.r = 255, .g = 226, .b = 58, .a = 255}; // #FFE23A
constexpr SDL_Color P2_DIM = {.r = 90, .g = 82, .b = 16, .a = 255}; // #5A5210
// Nivell / sistema → verd.
constexpr SDL_Color LEVEL_BRIGHT = {.r = 77, .g = 255, .b = 102, .a = 255}; // #4DFF66
constexpr SDL_Color LEVEL_DIM = {.r = 29, .g = 107, .b = 44, .a = 255}; // #1D6B2C
} // namespace Colors
// Les vides es dibuixen com a slots fixos de naus en miniatura (NUM_SLOTS =
// MAX_VIDES 1). Mida i pas dels slots es deriven de la mètrica del glif del
// dígit a init_hud_animator, no de constants soltes.
// Animación de entrada del HUD (init_hud_animator). // Animación de entrada del HUD (init_hud_animator).
namespace InitAnim { namespace InitAnim {
// Spawn vertical de la nave: 50 px bajo la PLAYAREA (sale desde fuera). // Spawn vertical de la nave: 50 px bajo la PLAYAREA (sale desde fuera).
+7 -8
View File
@@ -6,19 +6,18 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
// Paleta semántica por tipo de entidad. Si una entity declara color, lo // Paleta semántica por tipo de entidad. Si una entity declara color, lo
// pasa al pipeline con alpha=255 (sentinela "color válido"); si no, se // pasa al pipeline con alpha=255 (sentinela "color válido"); si no,
// usa el color global del oscilador (g_current_line_color). // line_renderer::linea() cau a DEFAULT_LINE_COLOR (verd fòsfor fallback).
namespace Defaults::Palette { namespace Defaults::Palette {
// Paleta neon: pujada lleugera dels canals secundaris per millorar la // Paleta neon: pujada lleugera dels canals secundaris per millorar la
// brillantor perceptual sota el bloom (sense alterar la identitat de color). // brillantor perceptual sota el bloom (sense alterar la identitat de color).
// El canal dominant es manté a 255 a cada color per maximitzar la saturació // El canal dominant es manté a 255 a cada color per maximitzar la saturació
// visible quan el halo s'expandeix. // visible quan el halo s'expandeix.
constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro // Tots els colors d'entitats han migrat al seu YAML respectiu
constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser // (data/entities/<name>/<name>.yaml, secció `colors`):
constexpr SDL_Color PENTAGON = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cyan pur "esquivador" // - SHIP → player.yaml
constexpr SDL_Color SQUARE = {.r = 255, .g = 0, .b = 0, .a = 255}; // Roig pur "tank" // - PENTAGON / SQUARE / PINWHEEL / WOUNDED → cada enemy.yaml
constexpr SDL_Color PINWHEEL = {.r = 255, .g = 0, .b = 255, .a = 255}; // Magenta pur "agressiu" // - BULLET → bullet.yaml
constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido
} // namespace Defaults::Palette } // namespace Defaults::Palette
+48 -58
View File
@@ -3,72 +3,62 @@
#pragma once #pragma once
namespace Defaults::Physics { // NOTA: els paràmetres del player (rotation_speed, acceleration,
// max_velocity, death_impact_factor) viuen a data/entities/player/player.yaml.
// Els paràmetres específics de la bala (mass, restitution, damping,
// impact_momentum_factor) viuen a data/entities/bullet/bullet.yaml.
// Aquest fitxer només conté els paràmetres compartits del subsistema de
// debris (explosions visuals).
constexpr float ROTATION_SPEED = 3.14F; // rad/s (~180°/s) namespace Defaults::Physics::Debris {
constexpr float ACCELERATION = 400.0F; // px/s²
constexpr float MAX_VELOCITY = 180.0F; // px/s
constexpr float FRICTION = 20.0F; // px/s²
// Bullet — impacto físico contra enemigo (impulse mass-aware). constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
// Model: el impulse és el moment lineal de la bala (m·v) multiplicat per constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s)
// un factor de transferència [0..1]. 1.0 = transfereix tot el moment constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
// (col·lisió perfectament inelàstica), 0.5 = transfereix la meitat. constexpr float ROTATION_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
namespace Bullet { constexpr float ROTATION_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa
} // namespace Bullet 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)
// Ship → enemy: impuls explícit aplicat a l'enemic en el moment exacte // Política de mort: passat el min_lifetime, el fragment mor quan la
// que la nau mor per col·lisió amb ell (afegit per damunt del rebot // seva velocity cau per sota d'aquest llindar. Així els fragments
// natural de PhysicsWorld, que ja és present però subtil amb la // ràpids no "popen" en moviment.
// damping de la nau). constexpr float MIN_SPEED_TO_DIE = 5.0F; // px/s — al cuadrat per evitar sqrt en update
namespace Ship { constexpr float MIN_SPEED_TO_DIE_SQ = MIN_SPEED_TO_DIE * MIN_SPEED_TO_DIE;
constexpr float DEATH_IMPACT_MOMENTUM_FACTOR = 0.3F;
} // namespace Ship
// Explosions (debris physics) // Rebot contra els límits del PLAYAREA (mateix patró que enemics/ship).
namespace Debris { // 0.7 = 70% de l'energia conservada al rebot.
constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s) constexpr float RESTITUTION_BOUNDS = 0.7F;
constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s)
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
constexpr float ROTATION_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/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_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)
// Política de mort: passat el min_lifetime, el fragment mor quan la // Herència de velocity angular (trayectorias curvas)
// seva velocity cau per sota d'aquest llindar. Així els fragments constexpr float INHERITANCE_FACTOR_MIN = 0.7F; // Mínimo 70% del drotacio heredat
// ràpids no "popen" en moviment. constexpr float INHERITANCE_FACTOR_MAX = 1.0F; // Màxim 100% del drotacio heredat
constexpr float MIN_SPEED_TO_DIE = 5.0F; // px/s — al cuadrat per evitar sqrt en update constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
constexpr float MIN_SPEED_TO_DIE_SQ = MIN_SPEED_TO_DIE * MIN_SPEED_TO_DIE;
// Rebot contra els límits del PLAYAREA (mateix patró que enemics/ship). // Velocity heredada de la nau a l'explosió (80% del feel original).
// 0.7 = 70% de l'energia conservada al rebot. constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F;
constexpr float RESTITUTION_BOUNDS = 0.7F;
// Herència de velocity angular (trayectorias curvas) // Velocity heredada de l'enemic a l'explosió (palanca per a tuneo).
constexpr float INHERITANCE_FACTOR_MIN = 0.7F; // Mínimo 70% del drotacio heredat // 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
constexpr float INHERITANCE_FACTOR_MAX = 1.0F; // Màxim 100% del drotacio heredat constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
// Velocity heredada de la nau a l'explosió (80% del feel original). // Velocitat de la bala traspassada a cada fragment de debris al moment
constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F; // de l'impacte. Separat de la inèrcia del cos (velocitat_objecte): permet
// que els trossos volin "amb la força de la bala" encara que el cos pesi
// molt i amb prou feines es mogui. 0.4 a 700 px/s = ~280 px/s extra per
// fragment, molt visible sense ser excessiu.
constexpr float BULLET_IMPULSE_FACTOR = 0.4F;
// Velocity heredada de l'enemic a l'explosió (palanca per a tuneo). // Tuneig específic de l'explosió d'enemic (overrides als defaults
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua. // que es passen com a paràmetres opcionals a explode()).
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F; constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més
constexpr float ENEMY_FRICTION = -30.0F; // Fricció més suau perquè s'estenguin més
constexpr int ENEMY_SEGMENT_MULTIPLIER = 1; // Sense còpies (5 cares = 5 trossos); >1 produeix grups sincronitzats
// Tuneig específic de l'explosió d'enemic (overrides als defaults // Angular velocity sin for trajectory inheritance
// que es passen com a paràmetres opcionals a explode()). // Excess above this threshold is converted to tangential linear velocity
constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més // Prevents "vortex trap" problem with high-rotation enemies
constexpr float ENEMY_FRICTION = -30.0F; // Fricció més suau perquè s'estenguin més constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s)
constexpr int ENEMY_SEGMENT_MULTIPLIER = 1; // Sense còpies (5 cares = 5 trossos); >1 produeix grups sincronitzats
// Angular velocity sin for trajectory inheritance } // namespace Defaults::Physics::Debris
// Excess above this threshold is converted to tangential linear velocity
// Prevents "vortex trap" problem with high-rotation enemies
constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s)
} // namespace Debris
} // namespace Defaults::Physics
+6 -3
View File
@@ -3,7 +3,6 @@
#pragma once #pragma once
#include <algorithm>
#include <array> #include <array>
namespace Defaults::Rendering { namespace Defaults::Rendering {
@@ -35,8 +34,12 @@ namespace Defaults::Rendering {
constexpr int RENDER_HEIGHT_DEFAULT = 720; constexpr int RENDER_HEIGHT_DEFAULT = 720;
constexpr auto isValidRenderResolution(int w, int h) -> bool { constexpr auto isValidRenderResolution(int w, int h) -> bool {
return std::ranges::any_of(RESOLUTION_PRESETS, for (const auto& preset : RESOLUTION_PRESETS) {
[w, h](const ResolutionPreset& preset) { return preset.w == w && preset.h == h; }); if (preset.w == w && preset.h == h) {
return true;
}
}
return false;
} }
} // namespace Defaults::Rendering } // namespace Defaults::Rendering
-33
View File
@@ -1,33 +0,0 @@
// ship.hpp - Configuració de la nau (invulnerabilitat, parpelleig)
// © 2026 JailDesigner
#pragma once
namespace Defaults::Ship {
// Invulnerabilidad post-respawn
constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad
// Parpadeo visual durante invulnerabilidad
constexpr float BLINK_VISIBLE_TIME = 0.1F; // Tiempo visible (segundos)
constexpr float BLINK_INVISIBLE_TIME = 0.1F; // Tiempo invisible (segundos)
// Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s)
// Cuerpo físico
constexpr float MASS = 10.0F; // Masa de referencia para choques
constexpr float RESTITUTION = 0.6F; // Rebote moderado contra paredes
constexpr float LINEAR_DAMPING = 1.5F; // Fricción exponencial (s⁻¹)
constexpr float ANGULAR_DAMPING = 0.0F; // Rotación 100% por input (no inercial)
// Empuje visual: escala proporcional a la velocidad (0..200 px/s → 1.0..1.5)
// Mantiene la sensación del Pascal original.
constexpr float VISUAL_PUSH_DIVISOR = 33.33F; // SPEED / DIVISOR = empuje visual
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
+24 -13
View File
@@ -105,7 +105,7 @@ namespace Defaults::Title {
namespace Layout { namespace Layout {
// Posicions verticals (anclatges des del TOP de pantalla lógica, 0.0-1.0) // Posicions verticals (anclatges des del TOP de pantalla lógica, 0.0-1.0)
constexpr float LOGO_POS = 0.20F; // Logo "ORNI" constexpr float LOGO_POS = 0.20F; // Logo "ORNI"
constexpr float PRESS_START_POS = 0.75F; // "PRESS START TO PLAY" constexpr float PRESS_START_POS = 0.72F; // "PRESS START TO PLAY" (una mica més amunt)
constexpr float COPYRIGHT1_POS = 0.90F; // Primera línia copyright constexpr float COPYRIGHT1_POS = 0.90F; // Primera línia copyright
// Separacions relatives (proporció respecte Game::HEIGHT = 480px) // Separacions relatives (proporció respecte Game::HEIGHT = 480px)
@@ -113,10 +113,17 @@ namespace Defaults::Title {
constexpr float COPYRIGHT_LINE_SPACING = 0.0F; // Entre línies copyright (5px) constexpr float COPYRIGHT_LINE_SPACING = 0.0F; // Entre línies copyright (5px)
// Factors de scale // Factors de scale
constexpr float LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!" constexpr float LOGO_SCALE = 0.6F; // Escala "ORNI ATTACK!"
constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY" constexpr float PRESS_START_SCALE = 1.0F; // Escala "PRESS START TO PLAY"
constexpr float COPYRIGHT_SCALE = 0.5F; // Escala copyright constexpr float COPYRIGHT_SCALE = 0.5F; // Escala copyright
constexpr float JAILGAMES_SCALE = 0.25F; // Escala del logo JAILGAMES pequeño sobre el copyright constexpr float JAILGAMES_BRIGHTNESS = 0.8F; // Logo JAILGAMES una mica menys brillant
constexpr float COPYRIGHT_BRIGHTNESS = 0.55F; // Mateix cian que JAILGAMES, però menys brillant
// Parpelleig del "PRESS START" (blinks per segon). Ritme pausat quan el
// text apareix (MAIN) i més ràpid quan ja s'ha premut START (join phase).
constexpr float PRESS_START_BLINK_HZ_SLOW = 1.0F;
constexpr float PRESS_START_BLINK_HZ_FAST = 3.0F;
constexpr float JAILGAMES_SCALE = 0.25F; // Escala del logo JAILGAMES pequeño sobre el copyright
// Separación entre el logo JAILGAMES y la línea de copyright (proporción de Game::HEIGHT). // Separación entre el logo JAILGAMES y la línea de copyright (proporción de Game::HEIGHT).
constexpr float JAILGAMES_COPYRIGHT_GAP = 0.015F; constexpr float JAILGAMES_COPYRIGHT_GAP = 0.015F;
@@ -157,14 +164,18 @@ namespace Defaults::Title {
// alpha = 255 (sentinela "color vàlid") fa que el pipeline ignori // alpha = 255 (sentinela "color vàlid") fa que el pipeline ignori
// el color global de l'oscil·lador per a aquesta crida. // el color global de l'oscil·lador per a aquesta crida.
namespace Colors { namespace Colors {
constexpr SDL_Color LOGO_MAIN = {.r = 80, .g = 240, .b = 255, .a = 255}; // Cian elèctric // Ambre neon: el mateix to dels missatges d'inici/fi de fase
constexpr SDL_Color LOGO_SHADOW = {.r = 255, .g = 60, .b = 180, .a = 255}; // Magenta neon (offset) // (STAGE_MESSAGE_COLOR a game_scene.cpp) per unificar el feel.
constexpr SDL_Color SHIP_P1 = {.r = 255, .g = 100, .b = 200, .a = 255}; // Rosa hot constexpr SDL_Color AMBER = {.r = 255, .g = 200, .b = 70, .a = 255};
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 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 LOGO_MAIN = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cian pur
constexpr SDL_Color JAILGAMES_LOGO = {.r = 120, .g = 220, .b = 200, .a = 255}; // Teal suau constexpr SDL_Color LOGO_SHADOW = STARFIELD; // Color de l'starfield (offset)
constexpr SDL_Color COPYRIGHT = {.r = 140, .g = 180, .b = 200, .a = 255}; // Gris-cian apagat constexpr SDL_Color SHIP_P1 = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanc
constexpr SDL_Color SHIP_P2 = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanc
constexpr SDL_Color PRESS_START = AMBER; // Ambre (com les frases de fase)
constexpr SDL_Color JAILGAMES_LOGO = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cian pur
constexpr SDL_Color COPYRIGHT = {.r = 0, .g = 255, .b = 255, .a = 255}; // Mateix cian (el brillo es baixa al render: COPYRIGHT_BRIGHTNESS)
} // namespace Colors } // namespace Colors
} // namespace Defaults::Title } // namespace Defaults::Title
+1 -1
View File
@@ -7,7 +7,7 @@ namespace Defaults::Trail {
constexpr int POOL_SIZE = 200; constexpr int POOL_SIZE = 200;
constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de Physics::MAX_VELOCITY (180) constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de player.yaml::physics.max_velocity (180 px/s)
constexpr float EMIT_INTERVAL_S = 0.04F; // ~25 Hz nominal constexpr float EMIT_INTERVAL_S = 0.04F; // ~25 Hz nominal
constexpr float EMIT_JITTER_S = 0.015F; // ±15 ms al cooldown constexpr float EMIT_JITTER_S = 0.015F; // ±15 ms al cooldown
constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement
+56
View File
@@ -0,0 +1,56 @@
// entity_loader.cpp - Implementació del carregador d'entitats YAML
// © 2026 JailDesigner
#include "core/entities/entity_loader.hpp"
#include <cstdint>
#include <exception>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include "core/resources/resource_helper.hpp"
namespace Entities {
std::unordered_map<std::string, std::shared_ptr<fkyaml::node>> EntityLoader::cache;
auto EntityLoader::load(const std::string& name) -> std::shared_ptr<fkyaml::node> {
// Cache hit
auto it = cache.find(name);
if (it != cache.end()) {
std::cout << "[EntityLoader] Cache hit: " << name << '\n';
return it->second;
}
const std::string PATH = "entities/" + name + "/" + name + ".yaml";
std::vector<uint8_t> data = Resource::Helper::loadFile(PATH);
if (data.empty()) {
std::cerr << "[EntityLoader] Error: no s'ha pogut load " << PATH << '\n';
return nullptr;
}
try {
std::string yaml_content(data.begin(), data.end());
std::stringstream stream(yaml_content);
auto node = std::make_shared<fkyaml::node>(fkyaml::node::deserialize(stream));
std::cout << "[EntityLoader] Carregat: " << PATH << '\n';
cache[name] = node;
return node;
} catch (const std::exception& e) {
std::cerr << "[EntityLoader] Excepció parsejant " << PATH << ": " << e.what() << '\n';
return nullptr;
}
}
void EntityLoader::clearCache() {
std::cout << "[EntityLoader] Netejant caché (" << cache.size() << " entitats)" << '\n';
cache.clear();
}
auto EntityLoader::getCacheSize() -> size_t { return cache.size(); }
} // namespace Entities
+38
View File
@@ -0,0 +1,38 @@
// entity_loader.hpp - Carregador genèric de descriptors d'entitats en YAML
// © 2026 JailDesigner
//
// Cada entitat viu a `data/entities/<name>/<name>.yaml` (mateix patró que el
// projecte germà aee_arcade). Aquest loader resol el path, llegeix del
// resource pack via Resource::Helper, parseja amb fkyaml i cacheja el node
// per evitar relectures. Retorna nullptr en cas d'error (el caller decideix
// si abortar).
#pragma once
#include <memory>
#include <string>
#include <unordered_map>
#include "external/fkyaml_node.hpp"
namespace Entities {
class EntityLoader {
public:
EntityLoader() = delete; // tot estàtic
// Carrega el descriptor d'una entitat per nom (ex. "player" →
// "entities/player/player.yaml"). Retorna nullptr si no es pot
// carregar o parsejar. Cachejat per nom.
static auto load(const std::string& name) -> std::shared_ptr<fkyaml::node>;
// Buidar caché (útil per debug/recàrrega).
static void clearCache();
[[nodiscard]] static auto getCacheSize() -> size_t;
private:
static std::unordered_map<std::string, std::shared_ptr<fkyaml::node>> cache;
};
} // namespace Entities
+4 -4
View File
@@ -51,12 +51,12 @@ namespace Graphics {
namespace { namespace {
// Lerp de l'oscil·lador (color base actual) cap a un color "flash" en // Lerp del color base verd fòsfor cap a un color "flash" en funció de
// funció de f ∈ [0, 1]. Retorna sempre amb alpha>0 perquè el line_renderer // f ∈ [0, 1]. Retorna sempre amb alpha>0 perquè el line_renderer l'usi
// l'use directament (sense barrejar amb el global). // directament (sense caure al fallback DEFAULT_LINE_COLOR).
auto lerpColor(SDL_Color flash, float f) -> SDL_Color { auto lerpColor(SDL_Color flash, float f) -> SDL_Color {
const float CLAMPED = std::clamp(f, 0.0F, 1.0F); const float CLAMPED = std::clamp(f, 0.0F, 1.0F);
const SDL_Color BASE = Rendering::getLineColor(); constexpr SDL_Color BASE = Rendering::DEFAULT_LINE_COLOR;
const auto LERP_U8 = [&](unsigned char a, unsigned char b) { const auto LERP_U8 = [&](unsigned char a, unsigned char b) {
const float OUT = (static_cast<float>(a) * (1.0F - CLAMPED)) + (static_cast<float>(b) * CLAMPED); const float OUT = (static_cast<float>(a) * (1.0F - CLAMPED)) + (static_cast<float>(b) * CLAMPED);
return static_cast<unsigned char>(OUT); return static_cast<unsigned char>(OUT);
+71
View File
@@ -0,0 +1,71 @@
// curtain.cpp - Implementació de la cortinilla negra
// © 2026 JailDesigner
#include "core/graphics/curtain.hpp"
#include <algorithm>
#include "core/defaults/game.hpp"
#include "core/math/easing.hpp"
namespace Graphics {
namespace {
constexpr float SCREEN_H = static_cast<float>(Defaults::Game::HEIGHT);
constexpr float SCREEN_W = static_cast<float>(Defaults::Game::WIDTH);
} // namespace
Curtain::Curtain(Rendering::Renderer* renderer)
: renderer_(renderer) {}
void Curtain::cover(float duration) {
// Caire superior de -H (fora, a dalt) fins a 0 (tela tapant tota la pantalla).
from_ = -SCREEN_H;
to_ = 0.0F;
duration_ = duration;
elapsed_ = 0.0F;
active_ = true;
}
void Curtain::reveal(float duration) {
// Caire superior de 0 (tapant) fins a +H (tela fora per baix).
from_ = 0.0F;
to_ = SCREEN_H;
duration_ = duration;
elapsed_ = 0.0F;
active_ = true;
}
void Curtain::update(float delta_time) {
if (!active_) {
return;
}
elapsed_ += delta_time;
}
auto Curtain::topY() const -> float {
if (duration_ <= 0.0F) {
return to_;
}
const float T = std::clamp(elapsed_ / duration_, 0.0F, 1.0F);
// Ease-in: la tela "cau" accelerant, com per gravetat.
return Easing::lerp(from_, to_, Easing::easeInQuad(T));
}
auto Curtain::isDone() const -> bool {
return !active_ || elapsed_ >= duration_;
}
void Curtain::draw() const {
if (!active_) {
return;
}
const float TOP = topY();
// Si la tela ja ha sortit completament per baix, no hi ha res a pintar.
if (TOP >= SCREEN_H) {
return;
}
renderer_->pushRect(0.0F, TOP, SCREEN_W, SCREEN_H, 0.0F, 0.0F, 0.0F, 1.0F);
}
} // namespace Graphics
+48
View File
@@ -0,0 +1,48 @@
// curtain.hpp - Cortinilla negra per a transicions d'escena
// © 2026 JailDesigner
//
// Tela negra a pantalla completa que es mou SEMPRE cap avall:
// - cover(): cau des de dalt fins a tapar-ho tot (queda negre).
// - reveal(): segueix caient i surt per baix, deixant veure l'escena.
// Una escena la posseeix, l'actualitza cada frame i la dibuixa l'ÚLTIM (per
// damunt de tot). En repòs (no arrencada o reveal acabada) el draw() és no-op.
#pragma once
#include "core/rendering/render_context.hpp"
namespace Graphics {
class Curtain {
public:
explicit Curtain(Rendering::Renderer* renderer);
// Tela que cau des de dalt fins a tapar tota la pantalla en 'duration' s.
void cover(float duration);
// Tela que segueix caient i surt per baix (destapa) en 'duration' s.
void reveal(float duration);
// Avança el temporitzador intern.
void update(float delta_time);
// Dibuixa la tela negra a la seva posició vertical actual. No-op si no
// queda res visible.
void draw() const;
// Cert quan el moviment actual ha acabat (o no s'ha arrencat mai).
[[nodiscard]] auto isDone() const -> bool;
private:
// Posició actual del caire superior de la tela (píxels lògics).
[[nodiscard]] auto topY() const -> float;
Rendering::Renderer* renderer_;
float from_{0.0F};
float to_{0.0F};
float duration_{0.0F};
float elapsed_{0.0F};
bool active_{false};
};
} // namespace Graphics
+6
View File
@@ -62,6 +62,12 @@ namespace Graphics {
buildLines(); buildLines();
} }
void Playfield::completeBuild() {
// Avançar el rellotge intern més enllà de tota la finestra d'spawn + el
// creixement de l'última línia: computeLineProgress() retorna 1.0 per a totes.
elapsed_s_ = Defaults::Playfield::TOTAL_ANIMATION_DURATION_S;
}
void Playfield::update(float delta_time) { void Playfield::update(float delta_time) {
elapsed_s_ += delta_time; elapsed_s_ += delta_time;
for (auto& ripple : ripples_) { for (auto& ripple : ripples_) {
+5
View File
@@ -30,6 +30,11 @@ namespace Graphics {
// Avança timers interns (creació + ripples). // Avança timers interns (creació + ripples).
void update(float delta_time); void update(float delta_time);
// Completa instantàniament l'animació de creació de la graella (totes les
// línies al 100%). Útil per a la demo (attract), que arrenca amb la
// partida "ja començada" i no ha de mostrar el muntatge del fons.
void completeBuild();
// 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. // i s'aplica deformació radial per cada ripple activa que afecti la línia.
void draw() const; void draw() const;
+3 -2
View File
@@ -20,7 +20,7 @@ namespace Graphics {
return it->second; // Cache hit return it->second; // Cache hit
} }
// Normalize path: "ship.shp" → "shapes/ship.shp" // Normalize path: "ship/arrow.shp" → "shapes/ship/arrow.shp"
// "logo/letra_j.shp" → "shapes/logo/letra_j.shp" // "logo/letra_j.shp" → "shapes/logo/letra_j.shp"
std::string normalized = filename; std::string normalized = filename;
if (!normalized.starts_with("shapes/")) { if (!normalized.starts_with("shapes/")) {
@@ -53,7 +53,8 @@ namespace Graphics {
// Cache and return // Cache and return
std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName() std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName()
<< ", " << shape->getNumPrimitives() << " primitives)" << '\n'; << ", " << shape->getNumPrimitives() << " primitives, bounding_radius="
<< shape->getBoundingRadius() << ")" << '\n';
cache[filename] = shape; cache[filename] = shape;
return shape; return shape;
+1 -1
View File
@@ -19,7 +19,7 @@ namespace Graphics {
// Carregar shape desde file (con caché) // Carregar shape desde file (con caché)
// Retorna punter compartit (nullptr si error) // Retorna punter compartit (nullptr si error)
// Exemple: load("ship.shp") → busca a "data/shapes/ship.shp" // Exemple: load("ship/arrow.shp") → busca a "data/shapes/ship/arrow.shp"
static auto load(const std::string& filename) -> std::shared_ptr<Shape>; static auto load(const std::string& filename) -> std::shared_ptr<Shape>;
// Netejar caché (útil per debug/recàrrega) // Netejar caché (útil per debug/recàrrega)
+22 -3
View File
@@ -11,8 +11,9 @@
namespace Graphics { namespace Graphics {
// Constants para mides base dels caràcters // Constants para mides base dels caràcters
constexpr float BASE_CHAR_WIDTH = 20.0F; // Amplada base del caràcter constexpr float BASE_CHAR_WIDTH = 20.0F; // Amplada base del caràcter (cel·la)
constexpr float BASE_CHAR_HEIGHT = 40.0F; // Altura base del caràcter constexpr float BASE_CHAR_HEIGHT = 40.0F; // Altura base del caràcter (cel·la, amb marge)
constexpr float BASE_GLYPH_HEIGHT = 20.0F; // Altura real del glif (la majúscula/dígit ocupa 20 dels 40)
VectorText::VectorText(Rendering::Renderer* renderer) VectorText::VectorText(Rendering::Renderer* renderer)
: renderer_(renderer) { : renderer_(renderer) {
@@ -47,7 +48,7 @@ namespace Graphics {
} }
// Cargar símbolos // Cargar símbolos
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?", "/", "(", ")"}; const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?", "/", "(", ")", "_"};
for (const auto& sym : SYMBOLS) { for (const auto& sym : SYMBOLS) {
char c = sym[0]; char c = sym[0];
std::string filename = getShapeFilename(c); std::string filename = getShapeFilename(c);
@@ -170,6 +171,8 @@ namespace Graphics {
return "font/char_lparen.shp"; return "font/char_lparen.shp";
case ')': case ')':
return "font/char_rparen.shp"; return "font/char_rparen.shp";
case '_':
return "font/char_underscore.shp";
case ' ': case ' ':
return ""; // Espai es maneja sin load shape return ""; // Espai es maneja sin load shape
@@ -182,6 +185,10 @@ namespace Graphics {
} }
auto VectorText::isSupported(char c) const -> bool { auto VectorText::isSupported(char c) const -> bool {
// Mateix fallback que render(): a-z es resol al glif A-Z.
if (c >= 'a' && c <= 'z') {
c -= 32;
}
return chars_.contains(c); return chars_.contains(c);
} }
@@ -220,6 +227,14 @@ namespace Graphics {
continue; continue;
} }
// Fallback de la font (només tenim glifs en majúscula): tractem
// les minúscules a-z com les seves majúscules A-Z. Mentre no hi
// haja glifs de minúscula, això evita que el text en minúscules
// (p. ex. rutes de fitxer) desaparega.
if (c >= 'a' && c <= 'z') {
c -= 32;
}
// Verificar si el carácter está soportado // Verificar si el carácter está soportado
auto it = chars_.find(c); auto it = chars_.find(c);
if (it != chars_.end()) { if (it != chars_.end()) {
@@ -287,4 +302,8 @@ namespace Graphics {
return BASE_CHAR_HEIGHT * scale; return BASE_CHAR_HEIGHT * scale;
} }
auto VectorText::getGlyphHeight(float scale) -> float {
return BASE_GLYPH_HEIGHT * scale;
}
} // namespace Graphics } // namespace Graphics
+6 -2
View File
@@ -26,7 +26,7 @@ namespace Graphics {
// - 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) es fa fallback a Rendering::DEFAULT_LINE_COLOR
void render(const std::string& text, const Vec2& position, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const; void render(const std::string& text, const Vec2& position, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
// Renderizar string centrado en un punto // Renderizar string centrado en un punto
@@ -35,7 +35,7 @@ namespace Graphics {
// - 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) es fa fallback a Rendering::DEFAULT_LINE_COLOR
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; 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).
@@ -46,6 +46,10 @@ namespace Graphics {
// Calcular altura del texto (útil para centrado vertical). // Calcular altura del texto (útil para centrado vertical).
[[nodiscard]] static auto getTextHeight(float scale = 1.0F) -> float; [[nodiscard]] static auto getTextHeight(float scale = 1.0F) -> float;
// Altura real del glif (la majúscula/dígit, sense el marge vertical de la
// cel·la). Útil per dimensionar icones que han de casar amb el text.
[[nodiscard]] static auto getGlyphHeight(float scale = 1.0F) -> float;
// Verificar si un carácter está soportado // Verificar si un carácter está soportado
[[nodiscard]] auto isSupported(char c) const -> bool; [[nodiscard]] auto isSupported(char c) const -> bool;
+1 -1
View File
@@ -4,7 +4,7 @@
// Mesh3D = llista de vèrtexs Vec3 + llista d'arestes (parells d'índexs). // Mesh3D = llista de vèrtexs Vec3 + llista d'arestes (parells d'índexs).
// drawWireframe() aplica una Transform3D al mesh, projecta amb Camera3D i // drawWireframe() aplica una Transform3D al mesh, projecta amb Camera3D i
// emet cada aresta com una línia 2D pel pipeline `Rendering::linea` (mateix // emet cada aresta com una línia 2D pel pipeline `Rendering::linea` (mateix
// pipeline que la resta del joc: glow verd via ColorOscillator si color.a==0). // pipeline que la resta del joc: verd fòsfor via Rendering::DEFAULT_LINE_COLOR si color.a==0).
// //
// Sense depth buffer: el caller és responsable d'ordenar els meshos per // Sense depth buffer: el caller és responsable d'ordenar els meshos per
// profunditat decreixent si vol oclusió coherent (la pipeline és LINE_LIST // profunditat decreixent si vol oclusió coherent (la pipeline és LINE_LIST
+6 -4
View File
@@ -3,7 +3,6 @@
#include "core/input/define_inputs.hpp" #include "core/input/define_inputs.hpp"
#include <algorithm>
#include <format> #include <format>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -154,9 +153,12 @@ namespace System {
} }
auto DefineInputs::isInUse(int code) const -> bool { auto DefineInputs::isInUse(int code) const -> bool {
return std::ranges::any_of(sequence_, [code](const Step& s) { for (const auto& s : sequence_) {
return s.captured == code; if (s.captured == code) {
}); return true;
}
}
return false;
} }
void DefineInputs::captureAndAdvance(int code) { void DefineInputs::captureAndAdvance(int code) {
+19 -11
View File
@@ -2,7 +2,6 @@
#include <SDL3/SDL.h> // Para SDL_GetGamepadAxis, SDL_GamepadAxis, SDL_GamepadButton, SDL_GetError, SDL_JoystickID, SDL_AddGamepadMappingsFromFile, SDL_Event, SDL_EventType, SDL_GetGamepadButton, SDL_GetKeyboardState, SDL_INIT_GAMEPAD, SDL_InitSubSystem, SDL_LogError, SDL_OpenGamepad, SDL_PollEvent, SDL_WasInit, Sint16, SDL_Gamepad, SDL_LogCategory, SDL_Scancode #include <SDL3/SDL.h> // Para SDL_GetGamepadAxis, SDL_GamepadAxis, SDL_GamepadButton, SDL_GetError, SDL_JoystickID, SDL_AddGamepadMappingsFromFile, SDL_Event, SDL_EventType, SDL_GetGamepadButton, SDL_GetKeyboardState, SDL_INIT_GAMEPAD, SDL_InitSubSystem, SDL_LogError, SDL_OpenGamepad, SDL_PollEvent, SDL_WasInit, Sint16, SDL_Gamepad, SDL_LogCategory, SDL_Scancode
#include <algorithm> // Para std::ranges::any_of
#include <iostream> // Para basic_ostream, operator<<, cout, cerr #include <iostream> // Para basic_ostream, operator<<, cout, cerr
#include <memory> // Para shared_ptr, __shared_ptr_access, allocator, operator==, make_shared #include <memory> // Para shared_ptr, __shared_ptr_access, allocator, operator==, make_shared
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator #include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
@@ -166,9 +165,12 @@ auto Input::checkAnyButton(bool repeat) -> bool {
// Comprueba si algún player (P1 o P2) presionó alguna acción de una lista // Comprueba si algún player (P1 o P2) presionó alguna acción de una lista
auto Input::checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat) -> bool { auto Input::checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat) -> bool {
return std::ranges::any_of(actions, [this, repeat](const InputAction& action) { for (const auto& action : actions) {
return checkActionPlayer1(action, repeat) || checkActionPlayer2(action, repeat); if (checkActionPlayer1(action, repeat) || checkActionPlayer2(action, repeat)) {
}); return true;
}
}
return false;
} }
// Comprueba si hay algun mando conectado // Comprueba si hay algun mando conectado
@@ -307,8 +309,11 @@ auto Input::checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gam
} }
void Input::addGamepadMappingsFromFile() { void Input::addGamepadMappingsFromFile() {
if (SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str()) < 0) { const int COUNT = SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str());
std::cout << "Error, could not load " << gamepad_mappings_file_.c_str() << " file: " << SDL_GetError() << '\n'; if (COUNT < 0) {
std::cerr << "[Input] Error carregant " << gamepad_mappings_file_ << ": " << SDL_GetError() << '\n';
} else {
std::cout << "[Input] " << gamepad_mappings_file_ << " carregat (" << COUNT << " mappings)\n";
} }
} }
@@ -326,8 +331,7 @@ void Input::initSDLGamePad() {
} else { } else {
addGamepadMappingsFromFile(); addGamepadMappingsFromFile();
discoverGamepads(); discoverGamepads();
std::cout << "\n** INPUT SYSTEM **\n"; std::cout << "[Input] inicialitzat\n";
std::cout << "Input System initialized successfully\n";
} }
} }
} }
@@ -441,9 +445,13 @@ auto Input::addGamepad(int device_index) -> std::string {
} }
auto Input::removeGamepad(SDL_JoystickID id) -> std::string { auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
auto it = std::ranges::find_if(gamepads_, [id](const std::shared_ptr<Gamepad>& gamepad) { auto it = gamepads_.end();
return gamepad->instance_id == id; for (auto i = gamepads_.begin(); i != gamepads_.end(); ++i) {
}); if ((*i)->instance_id == id) {
it = i;
break;
}
}
if (it != gamepads_.end()) { if (it != gamepads_.end()) {
std::string name = (*it)->name; std::string name = (*it)->name;
+5 -3
View File
@@ -3,7 +3,6 @@
#include "core/physics/physics_world.hpp" #include "core/physics/physics_world.hpp"
#include <algorithm>
#include <cmath> #include <cmath>
#include "core/physics/rigid_body.hpp" #include "core/physics/rigid_body.hpp"
@@ -14,9 +13,12 @@ namespace Physics {
if (body == nullptr) { if (body == nullptr) {
return; return;
} }
if (std::ranges::find(bodies_, body) == bodies_.end()) { for (const auto* b : bodies_) {
bodies_.push_back(body); if (b == body) {
return;
}
} }
bodies_.push_back(body);
} }
void PhysicsWorld::removeBody(RigidBody* body) { void PhysicsWorld::removeBody(RigidBody* body) {
@@ -7,6 +7,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstddef>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <iostream> #include <iostream>
@@ -132,6 +133,7 @@ namespace Rendering::GPU {
} }
void GpuFrameRenderer::destroy() { void GpuFrameRenderer::destroy() {
destroyCaptureResources();
destroyOffscreen(); destroyOffscreen();
postfx_pipeline_.destroy(); postfx_pipeline_.destroy();
bloom_pipeline_.destroy(); bloom_pipeline_.destroy();
@@ -172,7 +174,7 @@ namespace Rendering::GPU {
return false; return false;
} }
if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmd_buffer_, device_.window(), &swapchain_texture_, nullptr, nullptr)) { if (!SDL_WaitAndAcquireGPUSwapchainTexture(cmd_buffer_, device_.window(), &swapchain_texture_, &swapchain_w_, &swapchain_h_)) {
std::cerr << "[GpuFrameRenderer] WaitAndAcquire: " << SDL_GetError() << '\n'; std::cerr << "[GpuFrameRenderer] WaitAndAcquire: " << SDL_GetError() << '\n';
SDL_SubmitGPUCommandBuffer(cmd_buffer_); SDL_SubmitGPUCommandBuffer(cmd_buffer_);
cmd_buffer_ = nullptr; cmd_buffer_ = nullptr;
@@ -540,11 +542,20 @@ namespace Rendering::GPU {
SDL_EndGPURenderPass(render_pass_); SDL_EndGPURenderPass(render_pass_);
render_pass_ = nullptr; render_pass_ = nullptr;
} }
compositeTo(swapchain_texture_);
}
// Pase final: render pass sobre SWAPCHAIN con clear a negro (cubre el void GpuFrameRenderer::compositeTo(SDL_GPUTexture* target_tex) {
// Cos comú del pase de postpro: sample offscreen + bloom → target_tex
// (swapchain en el camí normal, o la textura de captura per al
// screenshot). El caller s'encarrega de tancar qualsevol render pass
// previ. Com que la textura de captura té la mateixa mida que la
// swapchain, el mateix viewport/letterbox produeix píxels idèntics.
// Pase final: render pass sobre el target con clear a negro (cubre el
// letterbox del viewport físico). // letterbox del viewport físico).
SDL_GPUColorTargetInfo target{}; SDL_GPUColorTargetInfo target{};
target.texture = swapchain_texture_; target.texture = target_tex;
target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F}; target.clear_color = SDL_FColor{.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
target.load_op = SDL_GPU_LOADOP_CLEAR; target.load_op = SDL_GPU_LOADOP_CLEAR;
target.store_op = SDL_GPU_STOREOP_STORE; target.store_op = SDL_GPU_STOREOP_STORE;
@@ -664,14 +675,150 @@ namespace Rendering::GPU {
} }
flushBatch(); flushBatch();
bloomPass(); bloomPass();
compositePass(); compositePass(); // → swapchain (camí de presentació normal)
if (render_pass_ != nullptr) { if (render_pass_ != nullptr) {
SDL_EndGPURenderPass(render_pass_); SDL_EndGPURenderPass(render_pass_);
render_pass_ = nullptr; render_pass_ = nullptr;
} }
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
cmd_buffer_ = nullptr; // Captura (screenshot): segon composite sobre la textura pròpia +
// readback a CPU. Només quan hi ha petició pendent i els recursos es
// poden crear; si no, submit normal i prou. La petició es consumeix
// sempre (s'haja pogut capturar o no) per no quedar enganxada.
if (capture_requested_ && ensureCaptureResources()) {
compositeTo(capture_texture_);
if (render_pass_ != nullptr) {
SDL_EndGPURenderPass(render_pass_);
render_pass_ = nullptr;
}
downloadCapture(); // fa submit + fence + map; deixa cmd_buffer_ a nullptr
} else {
SDL_SubmitGPUCommandBuffer(cmd_buffer_);
cmd_buffer_ = nullptr;
}
capture_requested_ = false;
swapchain_texture_ = nullptr; swapchain_texture_ = nullptr;
} }
auto GpuFrameRenderer::ensureCaptureResources() -> bool {
SDL_GPUDevice* dev = device_.get();
if (dev == nullptr || swapchain_w_ == 0 || swapchain_h_ == 0) {
return false;
}
const int W = static_cast<int>(swapchain_w_);
const int H = static_cast<int>(swapchain_h_);
if (capture_texture_ != nullptr && capture_tex_w_ == W && capture_tex_h_ == H) {
return true; // ja són de la mida correcta
}
destroyCaptureResources();
// Mateix format que la swapchain: és el target del postfx_pipeline_.
SDL_GPUTextureCreateInfo tex_info{};
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
tex_info.format = device_.swapchainFormat();
tex_info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;
tex_info.width = swapchain_w_;
tex_info.height = swapchain_h_;
tex_info.layer_count_or_depth = 1;
tex_info.num_levels = 1;
tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
capture_texture_ = SDL_CreateGPUTexture(dev, &tex_info);
if (capture_texture_ == nullptr) {
std::cerr << "[GpuFrameRenderer] CreateGPUTexture (captura): " << SDL_GetError() << '\n';
return false;
}
SDL_GPUTransferBufferCreateInfo tbo_info{};
tbo_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_DOWNLOAD;
tbo_info.size = static_cast<uint32_t>(W * H * 4);
capture_transfer_ = SDL_CreateGPUTransferBuffer(dev, &tbo_info);
if (capture_transfer_ == nullptr) {
std::cerr << "[GpuFrameRenderer] CreateGPUTransferBuffer (captura): " << SDL_GetError() << '\n';
SDL_ReleaseGPUTexture(dev, capture_texture_);
capture_texture_ = nullptr;
return false;
}
capture_tex_w_ = W;
capture_tex_h_ = H;
return true;
}
void GpuFrameRenderer::downloadCapture() {
SDL_GPUDevice* dev = device_.get();
// Copy pass: descarrega la textura de captura al transfer buffer.
SDL_GPUCopyPass* copy_pass = SDL_BeginGPUCopyPass(cmd_buffer_);
SDL_GPUTextureRegion src{};
src.texture = capture_texture_;
src.w = swapchain_w_;
src.h = swapchain_h_;
src.d = 1;
SDL_GPUTextureTransferInfo dst{};
dst.transfer_buffer = capture_transfer_;
dst.offset = 0;
dst.pixels_per_row = swapchain_w_;
dst.rows_per_layer = swapchain_h_;
SDL_DownloadFromGPUTexture(copy_pass, &src, &dst);
SDL_EndGPUCopyPass(copy_pass);
// Submit amb fence i esperar: el readback ha d'estar complet abans de mapar.
SDL_GPUFence* fence = SDL_SubmitGPUCommandBufferAndAcquireFence(cmd_buffer_);
cmd_buffer_ = nullptr;
if (fence == nullptr) {
std::cerr << "[GpuFrameRenderer] SubmitAndAcquireFence (captura): " << SDL_GetError() << '\n';
return;
}
SDL_WaitForGPUFences(dev, true, &fence, 1);
SDL_ReleaseGPUFence(dev, fence);
const auto* mapped = static_cast<const uint8_t*>(SDL_MapGPUTransferBuffer(dev, capture_transfer_, false));
if (mapped == nullptr) {
std::cerr << "[GpuFrameRenderer] MapGPUTransferBuffer (captura): " << SDL_GetError() << '\n';
return;
}
// Conversió a ARGB8888 (0xAARRGGBB), que és el que espera Screenshot::save.
// El swapchain a Linux/Vulkan sol ser B8G8R8A8_UNORM (bytes B,G,R,A);
// altrament tractem com R8G8B8A8_UNORM (bytes R,G,B,A). Alpha forçat a 255
// perquè el composite ja escriu opac.
const int W = static_cast<int>(swapchain_w_);
const int H = static_cast<int>(swapchain_h_);
const auto COUNT = static_cast<std::size_t>(W) * static_cast<std::size_t>(H);
capture_pixels_.resize(COUNT);
const bool BGRA = device_.swapchainFormat() == SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
for (std::size_t i = 0; i < COUNT; ++i) {
const uint8_t* px = mapped + (i * 4);
const std::uint32_t R = BGRA ? px[2] : px[0];
const std::uint32_t G = px[1];
const std::uint32_t B = BGRA ? px[0] : px[2];
capture_pixels_[i] = (0xFFU << 24) | (R << 16) | (G << 8) | B;
}
SDL_UnmapGPUTransferBuffer(dev, capture_transfer_);
capture_w_ = W;
capture_h_ = H;
capture_ready_ = true;
}
void GpuFrameRenderer::destroyCaptureResources() {
SDL_GPUDevice* dev = device_.get();
if (dev == nullptr) {
capture_texture_ = nullptr;
capture_transfer_ = nullptr;
capture_tex_w_ = 0;
capture_tex_h_ = 0;
return;
}
if (capture_texture_ != nullptr) {
SDL_ReleaseGPUTexture(dev, capture_texture_);
capture_texture_ = nullptr;
}
if (capture_transfer_ != nullptr) {
SDL_ReleaseGPUTransferBuffer(dev, capture_transfer_);
capture_transfer_ = nullptr;
}
capture_tex_w_ = 0;
capture_tex_h_ = 0;
}
} // namespace Rendering::GPU } // namespace Rendering::GPU
@@ -133,6 +133,19 @@ namespace Rendering::GPU {
void setPostFxEnabled(bool enabled) { postfx_enabled_ = enabled; } void setPostFxEnabled(bool enabled) { postfx_enabled_ = enabled; }
[[nodiscard]] auto isPostFxEnabled() const -> bool { return postfx_enabled_; } [[nodiscard]] auto isPostFxEnabled() const -> bool { return postfx_enabled_; }
// === Captura de pantalla (screenshot) ===
// Demana una captura del frame actual: el pròxim endFrame fa un segon
// composite sobre una textura pròpia (mida swapchain) i el descarrega a
// CPU en ARGB8888. Cost zero quan no hi ha cap petició pendent. Després
// del present, el caller comprova hasCapture(), llig captureData() i
// crida clearCapture().
void requestCapture() { capture_requested_ = true; }
[[nodiscard]] auto hasCapture() const -> bool { return capture_ready_; }
[[nodiscard]] auto captureData() const -> const std::uint32_t* { return capture_pixels_.data(); }
[[nodiscard]] auto captureWidth() const -> int { return capture_w_; }
[[nodiscard]] auto captureHeight() const -> int { return capture_h_; }
void clearCapture() { capture_ready_ = false; }
// Acceso a internals. // Acceso a internals.
[[nodiscard]] auto device() -> GpuDevice& { return device_; } [[nodiscard]] auto device() -> GpuDevice& { return device_; }
[[nodiscard]] auto isInsideFrame() const -> bool { return cmd_buffer_ != nullptr; } [[nodiscard]] auto isInsideFrame() const -> bool { return cmd_buffer_ != nullptr; }
@@ -173,6 +186,26 @@ namespace Rendering::GPU {
SDL_GPUTexture* bloom_texture_a_{nullptr}; SDL_GPUTexture* bloom_texture_a_{nullptr};
SDL_GPUTexture* bloom_texture_b_{nullptr}; SDL_GPUTexture* bloom_texture_b_{nullptr};
// === Captura de pantalla ===
// Dimensions reals de la swapchain (capturades a beginFrame). El target
// de captura es crea a aquesta mida perquè el PNG surti exactament com
// es veu en pantalla (amb letterbox), no a la resolució del offscreen.
uint32_t swapchain_w_{0};
uint32_t swapchain_h_{0};
// Textura on es fa el segon composite (mateix format que la swapchain) i
// transfer buffer per a descarregar-la a CPU. Es recreen si canvia la
// mida de la finestra (capture_tex_w_/h_ = mida amb què es van crear).
SDL_GPUTexture* capture_texture_{nullptr};
SDL_GPUTransferBuffer* capture_transfer_{nullptr};
int capture_tex_w_{0};
int capture_tex_h_{0};
// Píxels descarregats (ARGB8888, 0xAARRGGBB) i estat de la petició.
std::vector<std::uint32_t> capture_pixels_;
int capture_w_{0};
int capture_h_{0};
bool capture_requested_{false};
bool capture_ready_{false};
// Batch del frame en curso. // Batch del frame en curso.
std::vector<LineVertex> vertices_; std::vector<LineVertex> vertices_;
std::vector<uint16_t> indices_; std::vector<uint16_t> indices_;
@@ -202,8 +235,14 @@ namespace Rendering::GPU {
void flushBatch(); void flushBatch();
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 compositeTo(SDL_GPUTexture* target_tex); // cos comú del pase de postpro
void applyFinalViewport(); void applyFinalViewport();
void applyCurrentScissor(); // re-aplica el top de clip_stack_ al render_pass_ void applyCurrentScissor(); // re-aplica el top de clip_stack_ al render_pass_
// Captura: (re)crea recursos a mida swapchain, descarrega a CPU i allibera.
[[nodiscard]] auto ensureCaptureResources() -> bool;
void downloadCapture(); // copy pass + fence + map → capture_pixels_
void destroyCaptureResources();
}; };
} // namespace Rendering::GPU } // namespace Rendering::GPU
+2 -11
View File
@@ -8,11 +8,6 @@
namespace Rendering { namespace Rendering {
// Color global compartido para líneas sin paleta propia (HUD, debug, texto
// genérico). Equivale al "color máximo" de la antigua oscilación CPU: verde
// fósforo CRT. El pulso de brillo lo aplica ahora el shader de postpro.
SDL_Color g_current_line_color = {100, 255, 100, 255};
// Grosor global por defecto. Configurable via setLineThickness. // Grosor global por defecto. Configurable via setLineThickness.
float g_current_line_thickness = Defaults::Rendering::LINE_THICKNESS_DEFAULT; float g_current_line_thickness = Defaults::Rendering::LINE_THICKNESS_DEFAULT;
@@ -36,8 +31,8 @@ namespace Rendering {
const auto FX2 = static_cast<float>(x2); const auto FX2 = static_cast<float>(x2);
const auto FY2 = static_cast<float>(y2); const auto FY2 = static_cast<float>(y2);
// color.alpha==0 → usar color global (verde fósforo). alpha>0 → color directo. // color.alpha==0 → fallback a DEFAULT_LINE_COLOR (verd fòsfor). alpha>0 → color directo.
const SDL_Color SOURCE = (color.a > 0) ? color : g_current_line_color; const SDL_Color SOURCE = (color.a > 0) ? color : DEFAULT_LINE_COLOR;
const float R = (static_cast<float>(SOURCE.r) * brightness) / 255.0F; const float R = (static_cast<float>(SOURCE.r) * brightness) / 255.0F;
const float G = (static_cast<float>(SOURCE.g) * brightness) / 255.0F; const float G = (static_cast<float>(SOURCE.g) * brightness) / 255.0F;
const float B = (static_cast<float>(SOURCE.b) * brightness) / 255.0F; const float B = (static_cast<float>(SOURCE.b) * brightness) / 255.0F;
@@ -68,10 +63,6 @@ namespace Rendering {
} }
} }
void setLineColor(SDL_Color color) { g_current_line_color = color; }
auto getLineColor() -> SDL_Color { return g_current_line_color; }
void setLineThickness(float thickness) { void setLineThickness(float thickness) {
if (thickness > 0.0F) { if (thickness > 0.0F) {
g_current_line_thickness = thickness; g_current_line_thickness = thickness;
+11 -9
View File
@@ -3,9 +3,10 @@
// //
// El dibujo de líneas pasa por el pipeline GPU. Las coordenadas (x1,y1,x2,y2) // El dibujo de líneas pasa por el pipeline GPU. Las coordenadas (x1,y1,x2,y2)
// son lógicas (1280×720); el shader las mapea a NDC y el viewport del SDLManager // son lógicas (1280×720); el shader las mapea a NDC y el viewport del SDLManager
// hace el letterbox a píxeles físicos. El brillo modula el color global de // hace el letterbox a píxeles físicos. El pulse de brillo lo aplica el shader
// línea (lo gestiona ColorOscillator). El grosor es configurable por línea // de postpro (ya no hi ha un ColorOscillator a CPU). El grosor es configurable
// (parámetro thickness>0) o global (g_current_line_thickness vía setLineThickness). // per línia (parámetro thickness>0) o global (g_current_line_thickness vía
// setLineThickness).
#pragma once #pragma once
@@ -15,12 +16,17 @@
namespace Rendering { namespace Rendering {
// Color verd fòsfor CRT per defecte: s'usa quan el caller passa color amb
// alpha==0 (sentinella "sense color propi"). Constant immutable: la
// semàntica de "color global" ja no existeix (era de l'antic ColorOscillator).
constexpr SDL_Color DEFAULT_LINE_COLOR = {.r = 100, .g = 255, .b = 100, .a = 255};
// 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). // 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 DEFAULT_LINE_COLOR (verd fòsfor fallback);
// usa este color directo (paleta semántica por entidad). // si alpha>0 se usa este color directo (paleta semántica por entidad).
// alpha: alpha que arriba al GPU (default 1.0 = opac, behavior original). // 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 // 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). // en comptes de sobrepintar-lo (útil per halos translúcids).
@@ -49,10 +55,6 @@ namespace Rendering {
SDL_Color color = {0, 0, 0, 0}, SDL_Color color = {0, 0, 0, 0},
SDL_Color glow_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).
void setLineColor(SDL_Color color);
[[nodiscard]] auto getLineColor() -> SDL_Color;
// Grosor global por defecto (en píxeles lógicos). Default: 1.5. // Grosor global por defecto (en píxeles lógicos). Default: 1.5.
void setLineThickness(float thickness); void setLineThickness(float thickness);
[[nodiscard]] auto getLineThickness() -> float; [[nodiscard]] auto getLineThickness() -> float;
+72
View File
@@ -0,0 +1,72 @@
#include "core/rendering/screenshot.hpp"
#include <chrono>
#include <cstdint>
#include <ctime>
#include <filesystem>
#include <string>
#include <utility>
#include <vector>
#include "external/stb_image_write.h"
namespace Screenshot {
namespace {
// Estat de mòdul: ruta base on s'escriu la subcarpeta
// `screenshots/`. Buida per defecte ⇒ relativa al CWD (fallback
// si el Director no ha pogut establir la ruta per plataforma).
std::string g_base_dir; // NOLINT(*-avoid-non-const-global-variables) -- estat de mòdul (singleton-like): s'estableix una vegada al startup via setBaseDir() i es llegeix N vegades; encapsulat dins del namespace anònim, no és accessible des de fora.
// Construeix la ruta del directori on van les captures:
// `<base>/screenshots/`. Si la base és buida, retorna
// "screenshots/" relativa al CWD.
auto screenshotsDir() -> std::string {
if (g_base_dir.empty()) { return "screenshots/"; }
const bool ENDS_WITH_SEP = (g_base_dir.back() == '/' || g_base_dir.back() == '\\');
return g_base_dir + (ENDS_WITH_SEP ? "" : "/") + "screenshots/";
}
// Converteix ARGB8888 → RGBA8888 in-place i escriu el PNG. stb_image_write
// espera RGBA en ordre byte (little-endian: R al byte baix, A al byte alt).
auto writePng(std::uint32_t* buffer, int width, int height, const std::string& filepath) -> bool {
const int TOTAL = width * height;
for (int i = 0; i < TOTAL; ++i) {
const std::uint32_t P = buffer[i];
const std::uint32_t A = (P >> 24) & 0xFF;
const std::uint32_t R = (P >> 16) & 0xFF;
const std::uint32_t G = (P >> 8) & 0xFF;
const std::uint32_t B = P & 0xFF;
buffer[i] = R | (G << 8) | (B << 16) | (A << 24);
}
return stbi_write_png(filepath.c_str(), width, height, 4, buffer, width * 4) != 0;
}
} // namespace
void setBaseDir(std::string dir) {
g_base_dir = std::move(dir);
}
auto save(const std::uint32_t* buffer, int width, int height) -> Result {
const std::string FOLDER = screenshotsDir();
std::error_code ec;
std::filesystem::create_directories(FOLDER, ec);
const auto NOW = std::chrono::system_clock::now();
const std::time_t TIME = std::chrono::system_clock::to_time_t(NOW);
const std::tm* tm = std::localtime(&TIME);
char timestamp[20];
std::strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", tm);
const std::string FILENAME = std::string("scr_") + timestamp + ".png";
const std::string FILEPATH = FOLDER + FILENAME;
// Còpia local per no tocar el canvas original durant la conversió.
std::vector<std::uint32_t> copy(buffer, buffer + (static_cast<size_t>(width) * height));
if (!writePng(copy.data(), width, height, FILEPATH)) { return {}; }
return {.filename = FILENAME, .folder = FOLDER};
}
} // namespace Screenshot
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <cstdint>
#include <string>
// Volca un buffer ARGB8888 a un fitxer PNG dins de la subcarpeta
// `screenshots/` del directori configurat amb `setBaseDir`. El nom es
// genera amb un timestamp i la funció retorna el basename i la ruta
// del directori on s'ha escrit (o un Result amb `filename` buit si
// l'escriptura ha fallat).
namespace Screenshot {
// Configura el directori base on s'escriuran les captures (s'hi
// crea una subcarpeta `screenshots/`). El Director el crida una
// vegada al boot amb `system_folder_` perquè les captures vagin
// al mateix lloc per plataforma que la resta de fitxers generats
// (config.yaml, gamepad_configs.yaml, shaders/, etc.). Si no es
// crida, el fallback és el CWD — comportament antic.
void setBaseDir(std::string dir);
// Resultat d'una captura: el `filename` és el basename del PNG
// (per a notificacions/títol) i el `folder` és la ruta absoluta
// on s'ha guardat (per a poder dir "guardada a {folder}"). Si
// l'escriptura falla, ambdós camps tornen buits.
struct Result {
std::string filename;
std::string folder;
};
auto save(const std::uint32_t* buffer, int width, int height) -> Result;
} // namespace Screenshot
+29
View File
@@ -16,6 +16,7 @@
#include "core/input/mouse.hpp" #include "core/input/mouse.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/coordinate_transform.hpp" #include "core/rendering/coordinate_transform.hpp"
#include "core/rendering/screenshot.hpp"
#include "core/system/notifier.hpp" #include "core/system/notifier.hpp"
#include "project.h" #include "project.h"
@@ -360,8 +361,36 @@ auto SDLManager::clear(uint8_t r, uint8_t g, uint8_t b) -> bool {
return gpu_renderer_.beginFrame(0.0F, 0.0F, 0.0F); return gpu_renderer_.beginFrame(0.0F, 0.0F, 0.0F);
} }
void SDLManager::requestScreenshot() {
// La captura es fa dins del pròxim endFrame (segon composite + readback);
// el resultat es recull ací mateix, a present(), un cop el frame ja s'ha
// compost. Així el PNG mostra exactament el que es veu en pantalla.
gpu_renderer_.requestCapture();
}
void SDLManager::present() { void SDLManager::present() {
gpu_renderer_.endFrame(); gpu_renderer_.endFrame();
// Si el frame que s'acaba de presentar portava una captura demanada,
// l'escrivim a PNG i notifiquem la ruta. La notificació arriba DESPRÉS de
// la captura, així que el toast "guardada" no apareix dins de la imatge.
if (gpu_renderer_.hasCapture()) {
const auto RESULT = Screenshot::save(
gpu_renderer_.captureData(),
gpu_renderer_.captureWidth(),
gpu_renderer_.captureHeight());
gpu_renderer_.clearCapture();
if (!RESULT.filename.empty()) {
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
std::string msg = localeSubstitute(
Locale::get().text("notification.screenshot"),
"{file}",
RESULT.filename);
msg = localeSubstitute(msg, "{folder}", RESULT.folder);
notifier->notifyInfo(msg);
}
}
}
} }
void SDLManager::toggleVSync() { void SDLManager::toggleVSync() {
+1
View File
@@ -36,6 +36,7 @@ class SDLManager {
void toggleVSync(); // F4 void toggleVSync(); // F4
void toggleAntialias(); // F5 void toggleAntialias(); // F5
void togglePostFx(); // F6 void togglePostFx(); // F6
void requestScreenshot(); // F9: demana una captura del pròxim frame
// Canvia la resolució del render target offscreen (recrea la textura). // Canvia la resolució del render target offscreen (recrea la textura).
// Cal cridar-lo fora d'un frame (event phase, no draw phase). Si el // 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. // valor no es un preset valid o ja es l'actual, es no-op.
+238 -251
View File
@@ -10,300 +10,287 @@
namespace Resource { namespace Resource {
// Calcular checksum CRC32 simplificat // Calcular checksum CRC32 simplificat
auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t { auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
uint32_t checksum = 0x12345678; uint32_t checksum = 0x12345678;
for (unsigned char byte : data) { for (unsigned char byte : data) {
checksum = ((checksum << 5) + checksum) + byte; checksum = ((checksum << 5) + checksum) + byte;
} }
return checksum; return checksum;
}
// Encriptació XOR (simètrica)
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
if (key.empty()) {
return;
}
for (size_t i = 0; i < data.size(); ++i) {
data[i] ^= key[i % key.length()];
}
}
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
// XOR es simètric
encryptData(data, key);
}
// Llegir file complet a memòria
auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "[ResourcePack] Error: no es pot obrir " << filepath << '\n';
return {};
} }
std::streamsize file_size = file.tellg(); // Encriptació XOR (simètrica)
file.seekg(0, std::ios::beg); void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
if (key.empty()) {
std::vector<uint8_t> data(file_size); return;
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) { }
std::cerr << "[ResourcePack] Error: no es pot llegir " << filepath << '\n'; for (size_t i = 0; i < data.size(); ++i) {
return {}; data[i] ^= key[i % key.length()];
}
} }
return data; void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
} // XOR es simètric
encryptData(data, key);
// Añadir un file individual al paquet
auto Pack::addFile(const std::string& filepath, const std::string& pack_name) -> bool {
auto file_data = readFile(filepath);
if (file_data.empty()) {
return false;
} }
ResourceEntry entry{ // Llegir file complet a memòria
.filename = pack_name, auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> {
.offset = data_.size(), std::ifstream file(filepath, std::ios::binary | std::ios::ate);
.size = file_data.size(), if (!file) {
.checksum = calculateChecksum(file_data)}; std::cerr << "[ResourcePack] Error: no es pot obrir " << filepath << '\n';
return {};
// Añadir dades al bloc de dades
data_.insert(data_.end(), file_data.begin(), file_data.end());
resources_[pack_name] = entry;
std::cout << "[ResourcePack] Añadido: " << pack_name << " (" << file_data.size()
<< " bytes)\n";
return true;
}
// Añadir todos los archivos de un directori recursivament
auto Pack::addDirectory(const std::string& dir_path,
const std::string& base_path) -> bool {
namespace fs = std::filesystem;
if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
std::cerr << "[ResourcePack] Error: directori no trobat: " << dir_path << '\n';
return false;
}
std::string current_base = base_path.empty() ? "" : base_path + "/";
for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
if (!entry.is_regular_file()) {
continue;
} }
std::string full_path = entry.path().string(); std::streamsize file_size = file.tellg();
std::string relative_path = entry.path().lexically_relative(dir_path).string(); file.seekg(0, std::ios::beg);
// Convertir barres invertides a normals (Windows) std::vector<uint8_t> data(file_size);
std::ranges::replace(relative_path, '\\', '/'); if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
std::cerr << "[ResourcePack] Error: no es pot llegir " << filepath << '\n';
// Saltar archivos de desenvolupament return {};
if (relative_path.find(".world") != std::string::npos ||
relative_path.find(".tsx") != std::string::npos ||
relative_path.find(".DS_Store") != std::string::npos ||
relative_path.find(".git") != std::string::npos) {
std::cout << "[ResourcePack] Saltant: " << relative_path << '\n';
continue;
} }
std::string pack_name = current_base + relative_path; return data;
addFile(full_path, pack_name);
} }
return true; // Añadir un file individual al paquet
} auto Pack::addFile(const std::string& filepath, const std::string& pack_name) -> bool {
auto file_data = readFile(filepath);
if (file_data.empty()) {
return false;
}
// Guardar paquet a disc ResourceEntry entry{
auto Pack::savePack(const std::string& pack_file) -> bool { .filename = pack_name,
std::ofstream file(pack_file, std::ios::binary); .offset = data_.size(),
if (!file) { .size = file_data.size(),
std::cerr << "[ResourcePack] Error: no es pot crear " << pack_file << '\n'; .checksum = calculateChecksum(file_data)};
return false;
// Añadir dades al bloc de dades
data_.insert(data_.end(), file_data.begin(), file_data.end());
resources_[pack_name] = entry;
return true;
} }
// Escriure capçalera // Añadir todos los archivos de un directori recursivament
file.write(MAGIC_HEADER, 4); auto Pack::addDirectory(const std::string& dir_path,
file.write(reinterpret_cast<const char*>(&VERSION), sizeof(VERSION)); const std::string& base_path) -> bool {
namespace fs = std::filesystem;
// Escriure nombre de recursos if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
auto resource_count = static_cast<uint32_t>(resources_.size()); std::cerr << "[ResourcePack] Error: directori no trobat: " << dir_path << '\n';
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count)); return false;
}
// Escriure metadades de recursos std::string current_base = base_path.empty() ? "" : base_path + "/";
for (const auto& [name, entry] : resources_) {
// Nom del file
auto name_len = static_cast<uint32_t>(entry.filename.length());
file.write(reinterpret_cast<const char*>(&name_len), sizeof(name_len));
file.write(entry.filename.c_str(), name_len);
// Offset, mida, checksum for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset)); if (!entry.is_regular_file()) {
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size)); continue;
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum)); }
std::string full_path = entry.path().string();
std::string relative_path = entry.path().lexically_relative(dir_path).string();
// Convertir barres invertides a normals (Windows)
std::ranges::replace(relative_path, '\\', '/');
// Saltar archivos de desenvolupament
if (relative_path.find(".world") != std::string::npos ||
relative_path.find(".tsx") != std::string::npos ||
relative_path.find(".DS_Store") != std::string::npos ||
relative_path.find(".git") != std::string::npos) {
continue;
}
std::string pack_name = current_base + relative_path;
addFile(full_path, pack_name);
}
return true;
} }
// Encriptar dades // Guardar paquet a disc
std::vector<uint8_t> encrypted_data = data_; auto Pack::savePack(const std::string& pack_file) -> bool {
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY); std::ofstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "[ResourcePack] Error: no es pot crear " << pack_file << '\n';
return false;
}
// Escriure mida de dades y dades encriptades // Escriure capçalera
auto data_size = static_cast<uint64_t>(encrypted_data.size()); file.write(MAGIC_HEADER, 4);
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size)); file.write(reinterpret_cast<const char*>(&VERSION), sizeof(VERSION));
file.write(reinterpret_cast<const char*>(encrypted_data.data()), encrypted_data.size());
std::cout << "[ResourcePack] Guardat: " << pack_file << " (" << resources_.size() // Escriure nombre de recursos
<< " recursos, " << data_size << " bytes)\n"; auto resource_count = static_cast<uint32_t>(resources_.size());
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
return true; // Escriure metadades de recursos
} for (const auto& [name, entry] : resources_) {
// Nom del file
auto name_len = static_cast<uint32_t>(entry.filename.length());
file.write(reinterpret_cast<const char*>(&name_len), sizeof(name_len));
file.write(entry.filename.c_str(), name_len);
// Carregar paquet desde disc // Offset, mida, checksum
auto Pack::loadPack(const std::string& pack_file) -> bool { file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
std::ifstream file(pack_file, std::ios::binary); file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
if (!file) { file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
std::cerr << "[ResourcePack] Error: no es pot obrir " << pack_file << '\n'; }
return false;
// Encriptar dades
std::vector<uint8_t> encrypted_data = data_;
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
// Escriure mida de dades y dades encriptades
auto data_size = static_cast<uint64_t>(encrypted_data.size());
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
file.write(reinterpret_cast<const char*>(encrypted_data.data()), encrypted_data.size());
return true;
} }
// Llegir capçalera // Carregar paquet desde disc
char magic[4]; auto Pack::loadPack(const std::string& pack_file) -> bool {
file.read(magic, 4); std::ifstream file(pack_file, std::ios::binary);
if (std::string(magic, 4) != MAGIC_HEADER) { if (!file) {
std::cerr << "[ResourcePack] Error: capçalera invàlida (esperava " << MAGIC_HEADER std::cerr << "[ResourcePack] Error: no es pot obrir " << pack_file << '\n';
<< ")\n"; return false;
return false; }
// Llegir capçalera
char magic[4];
file.read(magic, 4);
if (std::string(magic, 4) != MAGIC_HEADER) {
std::cerr << "[ResourcePack] Error: capçalera invàlida (esperava " << MAGIC_HEADER
<< ")\n";
return false;
}
uint32_t version;
file.read(reinterpret_cast<char*>(&version), sizeof(version));
if (version != VERSION) {
std::cerr << "[ResourcePack] Error: versión incompatible (esperava " << VERSION
<< ", trobat " << version << ")\n";
return false;
}
// Llegir nombre de recursos
uint32_t resource_count;
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
// Llegir metadades de recursos
resources_.clear();
for (uint32_t i = 0; i < resource_count; ++i) {
// Nom del file
uint32_t name_len;
file.read(reinterpret_cast<char*>(&name_len), sizeof(name_len));
std::string filename(name_len, '\0');
file.read(filename.data(), name_len);
// Offset, mida, checksum
ResourceEntry entry;
entry.filename = filename;
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
resources_[filename] = entry;
}
// Llegir dades encriptades
uint64_t data_size;
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
data_.resize(data_size);
file.read(reinterpret_cast<char*>(data_.data()), data_size);
// Desencriptar
decryptData(data_, DEFAULT_ENCRYPT_KEY);
return true;
} }
uint32_t version; // Obtenir un recurs del paquet
file.read(reinterpret_cast<char*>(&version), sizeof(version)); auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> {
if (version != VERSION) { auto it = resources_.find(filename);
std::cerr << "[ResourcePack] Error: versión incompatible (esperava " << VERSION if (it == resources_.end()) {
<< ", trobat " << version << ")\n"; std::cerr << "[ResourcePack] Error: recurs no trobat: " << filename << '\n';
return false; return {};
} }
// Llegir nombre de recursos const auto& entry = it->second;
uint32_t resource_count;
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
// Llegir metadades de recursos // Extreure dades
resources_.clear();
for (uint32_t i = 0; i < resource_count; ++i) {
// Nom del file
uint32_t name_len;
file.read(reinterpret_cast<char*>(&name_len), sizeof(name_len));
std::string filename(name_len, '\0');
file.read(filename.data(), name_len);
// Offset, mida, checksum
ResourceEntry entry;
entry.filename = filename;
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
resources_[filename] = entry;
}
// Llegir dades encriptades
uint64_t data_size;
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
data_.resize(data_size);
file.read(reinterpret_cast<char*>(data_.data()), data_size);
// Desencriptar
decryptData(data_, DEFAULT_ENCRYPT_KEY);
std::cout << "[ResourcePack] Carregat: " << pack_file << " (" << resources_.size()
<< " recursos)\n";
return true;
}
// Obtenir un recurs del paquet
auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> {
auto it = resources_.find(filename);
if (it == resources_.end()) {
std::cerr << "[ResourcePack] Error: recurs no trobat: " << filename << '\n';
return {};
}
const auto& entry = it->second;
// Extreure dades
if (entry.offset + entry.size > data_.size()) {
std::cerr << "[ResourcePack] Error: offset/mida invàlid per " << filename << '\n';
return {};
}
std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size);
// Verificar checksum
uint32_t computed_checksum = calculateChecksum(resource_data);
if (computed_checksum != entry.checksum) {
std::cerr << "[ResourcePack] ADVERTÈNCIA: checksum invàlid per " << filename
<< " (esperat " << entry.checksum << ", calculat " << computed_checksum
<< ")\n";
// No falla, pero adverteix
}
return resource_data;
}
// Comprovar si existeix un recurs
auto Pack::hasResource(const std::string& filename) const -> bool {
return resources_.contains(filename);
}
// Obtenir list de todos los recursos
auto Pack::getResourceList() const -> std::vector<std::string> {
std::vector<std::string> list;
list.reserve(resources_.size());
for (const auto& [name, entry] : resources_) {
list.push_back(name);
}
std::ranges::sort(list);
return list;
}
// Validar integritat del paquet
auto Pack::validatePack() const -> bool {
bool valid = true;
for (const auto& [name, entry] : resources_) {
// Verificar offset i mida
if (entry.offset + entry.size > data_.size()) { if (entry.offset + entry.size > data_.size()) {
std::cerr << "[ResourcePack] Error de validació: " << name std::cerr << "[ResourcePack] Error: offset/mida invàlid per " << filename << '\n';
<< " té offset/mida invàlid\n"; return {};
valid = false;
continue;
} }
// Extreure i verificar checksum
std::vector<uint8_t> resource_data(data_.begin() + entry.offset, std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size); data_.begin() + entry.offset + entry.size);
// Verificar checksum
uint32_t computed_checksum = calculateChecksum(resource_data); uint32_t computed_checksum = calculateChecksum(resource_data);
if (computed_checksum != entry.checksum) { if (computed_checksum != entry.checksum) {
std::cerr << "[ResourcePack] Error de validació: " << name std::cerr << "[ResourcePack] ADVERTÈNCIA: checksum invàlid per " << filename
<< " té checksum invàlid\n"; << " (esperat " << entry.checksum << ", calculat " << computed_checksum
valid = false; << ")\n";
// No falla, pero adverteix
} }
return resource_data;
} }
if (valid) { // Comprovar si existeix un recurs
std::cout << "[ResourcePack] Validació OK (" << resources_.size() << " recursos)\n"; auto Pack::hasResource(const std::string& filename) const -> bool {
return resources_.contains(filename);
} }
return valid; // Obtenir list de todos los recursos
} auto Pack::getResourceList() const -> std::vector<std::string> {
std::vector<std::string> list;
list.reserve(resources_.size());
for (const auto& [name, entry] : resources_) {
list.push_back(name);
}
std::ranges::sort(list);
return list;
}
// Validar integritat del paquet
auto Pack::validatePack() const -> bool {
bool valid = true;
for (const auto& [name, entry] : resources_) {
// Verificar offset i mida
if (entry.offset + entry.size > data_.size()) {
std::cerr << "[ResourcePack] Error de validació: " << name
<< " té offset/mida invàlid\n";
valid = false;
continue;
}
// Extreure i verificar checksum
std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
data_.begin() + entry.offset + entry.size);
uint32_t computed_checksum = calculateChecksum(resource_data);
if (computed_checksum != entry.checksum) {
std::cerr << "[ResourcePack] Error de validació: " << name
<< " té checksum invàlid\n";
valid = false;
}
}
return valid;
}
} // namespace Resource } // namespace Resource
+10 -2
View File
@@ -17,6 +17,7 @@
#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/locale/locale.hpp"
#include "core/rendering/screenshot.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"
@@ -99,6 +100,10 @@ Director::Director(int argc, char* argv[])
// Establir ruta del file de configuración // Establir ruta del file de configuración
ConfigYaml::setConfigFile(system_folder_ + "/config.yaml"); ConfigYaml::setConfigFile(system_folder_ + "/config.yaml");
// Les captures de pantalla van sota la mateixa carpeta per plataforma
// (subcarpeta screenshots/). Sense açò, Screenshot::save cauria al CWD.
Screenshot::setBaseDir(system_folder_);
// Carregar o crear configuración // Carregar o crear configuración
ConfigYaml::loadFromFile(); ConfigYaml::loadFromFile();
@@ -106,8 +111,11 @@ Director::Director(int argc, char* argv[])
// falla, Locale::text() retorna la clau crua i el joc segueix funcionant. // falla, Locale::text() retorna la clau crua i el joc segueix funcionant.
Locale::get().load(std::string("locale/") + cfg_->locale + ".yaml"); Locale::get().load(std::string("locale/") + cfg_->locale + ".yaml");
// Inicialitzar sistema de input // Inicialitzar sistema de input. El gamecontrollerdb.txt viu al costat del
Input::init("data/gamecontrollerdb.txt"); // binari (no dins de resources.pack, perquè SDL_AddGamepadMappingsFromFile
// necessita una ruta real de filesystem). resource_base ja apunta al directori
// de l'executable (o a Contents/Resources en bundles de macOS).
Input::init(resource_base + "/gamecontrollerdb.txt");
// Autoassignacio de primer arranque: si cap dels dos jugadors te mando // Autoassignacio de primer arranque: si cap dels dos jugadors te mando
// assignat al config, repartim els que hi haja detectats (P1 = pad 0, // assignat al config, repartim els que hi haja detectats (P1 = pad 0,
+5
View File
@@ -160,6 +160,11 @@ namespace GlobalEvents {
sdl.togglePostFx(); sdl.togglePostFx();
return true; return true;
case SDL_SCANCODE_F9:
// Captura de pantalla (PNG) amb shaders, a mida de finestra.
sdl.requestScreenshot();
return true;
case SDL_SCANCODE_F7: { case SDL_SCANCODE_F7: {
// Toggle d'idioma en runtime entre català i anglès. Els // Toggle d'idioma en runtime entre català i anglès. Els
// strings ja capturats (toast actiu, banner stage start) // strings ja capturats (toast actiu, banner stage start)
+14
View File
@@ -70,10 +70,24 @@ namespace SceneManager {
return match_config_; return match_config_;
} }
// Nombre d'escenaris demo curats (cicle del attract mode).
static constexpr std::uint8_t DEMO_SCENARIO_COUNT = 4;
// Índex de l'escenari demo actual. Persisteix entre transicions (el
// SceneContext el posseeix el Director), així cada entrada al mode demo
// mostra el següent escenari de la llista curada.
[[nodiscard]] auto demoScenarioIndex() const -> std::uint8_t {
return demo_scenario_index_;
}
void advanceDemoScenario() {
demo_scenario_index_ = (demo_scenario_index_ + 1) % DEMO_SCENARIO_COUNT;
}
private: private:
SceneType next_scene_{SceneType::LOGO}; // SceneType a la qual transicionar SceneType next_scene_{SceneType::LOGO}; // SceneType a la qual transicionar
Option option_{Option::NONE}; // Opción específica per l'escena Option option_{Option::NONE}; // Opción específica per l'escena
GameConfig::MatchConfig match_config_; // Configuración de match (jugadors active, mode) GameConfig::MatchConfig match_config_; // Configuración de match (jugadors active, mode)
std::uint8_t demo_scenario_index_{0}; // Índex de l'escenari demo (attract mode)
}; };
// Variable global inline per gestionar l'escena actual (backward compatibility) // Variable global inline per gestionar l'escena actual (backward compatibility)
+1724
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
// Unitat de compilació aïllada per a la implementació de stb_image_write.
// Viu dins de source/external/ perquè el `.clang-tidy` d'aquesta carpeta
// desactiva tots els checks (com fa per stb_vorbis.c) i el pre-commit hook
// ja filtra aquesta ruta de clang-format / clang-tidy. Així els fals
// positius de clang-analyzer-* dins de codi de tercers no afecten el
// nostre codi, que continua tenint tots els checks actius.
//
// La resta del codi inclou només el header (sense la macro d'implementació),
// que queda només amb declaracions — clang-analyzer no pot trobar cap bug
// dins d'una declaració, així que l'inclusió és innòcua.
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "external/stb_image_write.h"
+33 -15
View File
@@ -57,13 +57,18 @@ namespace Effects {
SDL_Color color, SDL_Color color,
float lifetime, float lifetime,
float friction, float friction,
int segment_multiplier) { int segment_multiplier,
const Vec2& bullet_impulse_velocity,
float piece_scale) {
if (!shape || !shape->isValid()) { if (!shape || !shape->isValid()) {
return; return;
} }
// Reproducir sonido de explosión // Reproducir sonido de explosión. Cadena buida = explosió silenciosa
Audio::get()->playSound(sound, Audio::Group::GAME); // (p. ex. el logo dins el cicle d'atracció).
if (!sound.empty()) {
Audio::get()->playSound(sound, Audio::Group::GAME);
}
// Notificar als subscriptors (border, playfield, etc.). // Notificar als subscriptors (border, playfield, etc.).
if (explosion_callback_) { if (explosion_callback_) {
@@ -84,7 +89,7 @@ namespace Effects {
Vec2 world_p2 = transformPoint(local_p2, shape_centre, centro, angle, scale); Vec2 world_p2 = transformPoint(local_p2, shape_centre, centro, angle, scale);
// Si el pool es ple, no té sentit continuar amb la resta de segments // Si el pool es ple, no té sentit continuar amb la resta de segments
if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness, velocitat_objecte, velocitat_angular, factor_herencia_visual, color, lifetime, friction)) { if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness, velocitat_objecte, velocitat_angular, factor_herencia_visual, color, lifetime, friction, bullet_impulse_velocity, piece_scale)) {
return; return;
} }
} }
@@ -110,34 +115,47 @@ namespace Effects {
return segments; return segments;
} }
auto DebrisManager::spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction) -> bool { auto DebrisManager::spawnDebris(const Vec2& world_p1_in, const Vec2& world_p2_in, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction, const Vec2& bullet_impulse_velocity, float piece_scale) -> bool {
Debris* debris = findFreeSlot(); Debris* debris = findFreeSlot();
if (debris == nullptr) { if (debris == nullptr) {
std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n"; std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n";
return false; return false;
} }
// Escala el segment al voltant del seu punt mitjà segons piece_scale
// (1.0 = original; 0.3 = "esquerda petita"). La resta del càlcul (angle,
// half_length, p1/p2) en deriva naturalment.
const Vec2 MID = {.x = (world_p1_in.x + world_p2_in.x) / 2.0F,
.y = (world_p1_in.y + world_p2_in.y) / 2.0F};
const Vec2 WORLD_P1 = {.x = MID.x + ((world_p1_in.x - MID.x) * piece_scale),
.y = MID.y + ((world_p1_in.y - MID.y) * piece_scale)};
const Vec2 WORLD_P2 = {.x = MID.x + ((world_p2_in.x - MID.x) * piece_scale),
.y = MID.y + ((world_p2_in.y - MID.y) * piece_scale)};
// Geometria autoritaritzada: centro + original_angle + original_half_length. // Geometria autoritaritzada: centro + original_angle + original_half_length.
// p1/p2 es reconstrueixen cada frame en update() des d'aquestes dades. // p1/p2 es reconstrueixen cada frame en update() des d'aquestes dades.
const float DX = world_p2.x - world_p1.x; const float DX = WORLD_P2.x - WORLD_P1.x;
const float DY = world_p2.y - world_p1.y; const float DY = WORLD_P2.y - WORLD_P1.y;
debris->centro = {.x = (world_p1.x + world_p2.x) / 2.0F, debris->centro = {.x = (WORLD_P1.x + WORLD_P2.x) / 2.0F,
.y = (world_p1.y + world_p2.y) / 2.0F}; .y = (WORLD_P1.y + WORLD_P2.y) / 2.0F};
debris->original_angle = std::atan2(DY, DX); debris->original_angle = std::atan2(DY, DX);
debris->original_half_length = std::sqrt((DX * DX) + (DY * DY)) / 2.0F; debris->original_half_length = std::sqrt((DX * DX) + (DY * DY)) / 2.0F;
debris->p1 = world_p1; debris->p1 = WORLD_P1;
debris->p2 = world_p2; debris->p2 = WORLD_P2;
// Direcció radial (desde el centro hacia el segment) // Direcció radial (desde el centro hacia el segment)
Vec2 direccio = computeExplosionDirection(world_p1, world_p2, centro); Vec2 direccio = computeExplosionDirection(WORLD_P1, WORLD_P2, centro);
// Velocidad inicial (base ± variació aleatòria + velocity heretada de l'objecte) // Velocidad inicial (base ± variació aleatòria + velocity heretada de l'objecte +
// velocitat de la bala escalada per BULLET_IMPULSE_FACTOR).
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_SPEED); 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; (bullet_impulse_velocity.x * Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR);
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y +
(bullet_impulse_velocity.y * Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR);
debris->acceleration = friction; debris->acceleration = friction;
// Rotación de trayectoria (con conversió a tangencial si excedeix cap) // Rotación de trayectoria (con conversió a tangencial si excedeix cap)
+14 -3
View File
@@ -47,6 +47,14 @@ namespace Effects {
// - lifetime: temps de vida del debris (s, per defecte TEMPS_VIDA = 2s) // - lifetime: temps de vida del debris (s, per defecte TEMPS_VIDA = 2s)
// - friction: desacceleració del debris (px/s², per defecte ACCELERACIO = -60) // - friction: desacceleració del debris (px/s², per defecte ACCELERACIO = -60)
// - segment_multiplier: nombre de còpies per segment (per defecte 1 = sense duplicar) // - segment_multiplier: nombre de còpies per segment (per defecte 1 = sense duplicar)
// - bullet_impulse_velocity: velocitat de la bala que ha causat l'impacte (px/s,
// per defecte 0). S'aplica a cada fragment escalada per
// Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR, independent de
// velocitat_objecte. Permet que els trossos "salten amb la força de la bala"
// encara que el cos sigui pesat i amb prou feines es mogui.
// - piece_scale: multiplicador de la longitud de cada fragment al spawn
// (per defecte 1.0). Útil per a debris "parcial" d'impactes no letals
// en enemics HP>1 (trossos petits, com d'esquerda).
void explode(const std::shared_ptr<Graphics::Shape>& shape, void explode(const std::shared_ptr<Graphics::Shape>& shape,
const Vec2& centro, const Vec2& centro,
float angle, float angle,
@@ -56,11 +64,14 @@ namespace Effects {
const Vec2& velocitat_objecte = {.x = 0.0F, .y = 0.0F}, const Vec2& velocitat_objecte = {.x = 0.0F, .y = 0.0F},
float velocitat_angular = 0.0F, float velocitat_angular = 0.0F,
float factor_herencia_visual = 0.0F, float factor_herencia_visual = 0.0F,
const std::string& sound = Defaults::Sound::EXPLOSION, // sound: nom del so d'explosió. Cadena buida ("") = explosió silenciosa.
const std::string& sound = Defaults::Sound::ENEMY_EXPLOSION,
SDL_Color color = {0, 0, 0, 0}, // alpha==0 → fragmentos usan oscilador global SDL_Color color = {0, 0, 0, 0}, // alpha==0 → fragmentos usan oscilador global
float lifetime = Defaults::Physics::Debris::TEMPS_VIDA, float lifetime = Defaults::Physics::Debris::TEMPS_VIDA,
float friction = Defaults::Physics::Debris::ACCELERACIO, float friction = Defaults::Physics::Debris::ACCELERACIO,
int segment_multiplier = 1); int segment_multiplier = 1,
const Vec2& bullet_impulse_velocity = {.x = 0.0F, .y = 0.0F},
float piece_scale = 1.0F);
// Actualitzar todos los fragments active // Actualitzar todos los fragments active
void update(float delta_time); void update(float delta_time);
@@ -97,7 +108,7 @@ namespace Effects {
-> std::vector<std::pair<Vec2, Vec2>>; -> std::vector<std::pair<Vec2, Vec2>>;
// Inicialitza un debris en un slot lliure i el deixa actiu. Retorna // Inicialitza un debris en un slot lliure i el deixa actiu. Retorna
// false si el pool está ple (la cridadora ha d'aturar el bucle). // false si el pool está ple (la cridadora ha d'aturar el bucle).
auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction) -> bool; auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction, const Vec2& bullet_impulse_velocity, float piece_scale) -> bool;
static void applyAngularVelocity(Debris& debris, const Vec2& direccio, float velocitat_angular); static void applyAngularVelocity(Debris& debris, const Vec2& direccio, float velocitat_angular);
static void applyVisualRotation(Debris& debris, float velocitat_angular, float factor_herencia_visual); static void applyVisualRotation(Debris& debris, float velocitat_angular, float factor_herencia_visual);
}; };
+2 -2
View File
@@ -33,9 +33,9 @@ namespace Effects {
TrailManager::TrailManager(Rendering::Renderer* renderer) TrailManager::TrailManager(Rendering::Renderer* renderer)
: renderer_(renderer), : renderer_(renderer),
star_shape_(Graphics::ShapeLoader::load("star.shp")) { star_shape_(Graphics::ShapeLoader::load("effect/starfield.shp")) {
if (!star_shape_ || !star_shape_->isValid()) { if (!star_shape_ || !star_shape_->isValid()) {
std::cerr << "[TrailManager] Warning: no s'ha pogut load star.shp\n"; std::cerr << "[TrailManager] Warning: no s'ha pogut load effect/starfield.shp\n";
} }
for (auto& particle : pool_) { for (auto& particle : pool_) {
particle.active = false; particle.active = false;
+43 -34
View File
@@ -14,38 +14,39 @@
#include "core/rendering/shape_renderer.hpp" #include "core/rendering/shape_renderer.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/constants.hpp" #include "game/constants.hpp"
#include "game/entities/bullet_config.hpp"
#include "game/entities/bullet_registry.hpp"
Bullet::Bullet(Rendering::Renderer* renderer) Bullet::Bullet(Rendering::Renderer* renderer)
: Entity(renderer) { : Entity(renderer),
// Brightness específico para balas config_(&BulletRegistry::get()) {
brightness_ = Defaults::Brightness::BALA; brightness_ = Defaults::Brightness::BALA;
// Configuración del cuerpo físico. // Cinemàtiques pures: no col·lisionen al PhysicsWorld (body_.radius = 0).
// Las balas son cinemáticas: no colisionan con otros bodies ni paredes. // El gameplay (GameScene) gestiona els hits via checkCollisionSwept i la
// El gameplay (GameScene) gestiona los hits con check_collision y la // sortida del PLAYAREA.
// salida del PLAYAREA. Por eso radius=0 en el world (no participa en body_.setMass(config_->physics.mass);
// resolveBodyCollisions ni resolveBoundsCollisions). body_.radius = 0.0F;
body_.setMass(0.5F); // Ligera (no afecta a nadie, pero por consistencia) body_.restitution = config_->physics.restitution;
body_.radius = 0.0F; // Sin colisión física (cinemática pura) body_.linear_damping = config_->physics.linear_damping;
body_.restitution = 0.0F; // Irrelevante (no rebota) body_.angular_damping = config_->physics.angular_damping;
body_.linear_damping = 0.0F; // Sin fricción (movimiento rectilíneo uniforme)
body_.angular_damping = 0.0F;
// Cargar shape compartida desde archivo shape_ = Graphics::ShapeLoader::load(config_->shape.path);
shape_ = Graphics::ShapeLoader::load("bullet.shp");
if (!shape_ || !shape_->isValid()) { if (!shape_ || !shape_->isValid()) {
std::cerr << "[Bullet] Error: no s'ha pogut load bullet.shp" << '\n'; std::cerr << "[Bullet] Error: no s'ha pogut carregar " << config_->shape.path << '\n';
} }
// Radi de col·lisió derivat del cercle circumscrit de la shape.
const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F;
collision_radius_ = BOUNDING * config_->shape.scale * config_->shape.collision_factor;
} }
void Bullet::init() { void Bullet::init() {
// 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}; prev_position_ = {.x = 0.0F, .y = 0.0F};
angle_ = 0.0F; angle_ = 0.0F;
// Reset del cuerpo físico
body_.position = Vec2{}; body_.position = Vec2{};
body_.velocity = Vec2{}; body_.velocity = Vec2{};
body_.angle = 0.0F; body_.angle = 0.0F;
@@ -53,29 +54,43 @@ void Bullet::init() {
body_.clearAccumulators(); body_.clearAccumulators();
} }
void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id) { void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed, const BulletConfig* cfg) {
// Activar bullet
is_active_ = true; is_active_ = true;
// Almacenar propietario (0=P1, 1=P2)
owner_id_ = owner_id; owner_id_ = owner_id;
// Posición y orientación iniciales = ship // Si no es passa cfg, restaurem al config per defecte (BulletRegistry::get):
// els slots són reutilitzables i una bala que abans ha estat disparada amb
// una variant (p.ex. bullet_long d'enemic) ha de tornar al bullet.shp del
// player quan aquest la reutilitza.
const BulletConfig* effective = (cfg != nullptr) ? cfg : &BulletRegistry::get();
if (effective != config_) {
config_ = effective;
shape_ = Graphics::ShapeLoader::load(config_->shape.path);
if (!shape_ || !shape_->isValid()) {
std::cerr << "[Bullet] Error: no s'ha pogut carregar " << config_->shape.path << '\n';
}
const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F;
collision_radius_ = BOUNDING * config_->shape.scale * config_->shape.collision_factor;
body_.setMass(config_->physics.mass);
body_.restitution = config_->physics.restitution;
body_.linear_damping = config_->physics.linear_damping;
body_.angular_damping = config_->physics.angular_damping;
}
center_ = position; center_ = position;
prev_position_ = position; // Al spawn no hi ha moviment encara: swept degenera a punt-cercle prev_position_ = position; // spawn: swept degenera a punt-cercle
angle_ = angle; angle_ = angle;
// Sincronizar el body físico: posición + velocidad cartesiana // Sincronizar el body físic: posició + velocitat cartesiana.
// angle - PI/2 porque angle=0 apunta hacia arriba (eje Y negativo SDL) // angle - PI/2 perquè angle=0 apunta cap amunt (eje Y negatiu SDL).
body_.position = position; body_.position = position;
body_.angle = angle; body_.angle = angle;
const float DIR_X = std::cos(angle - (Constants::PI / 2.0F)); const float DIR_X = std::cos(angle - (Constants::PI / 2.0F));
const float DIR_Y = std::sin(angle - (Constants::PI / 2.0F)); const float DIR_Y = std::sin(angle - (Constants::PI / 2.0F));
body_.velocity = Vec2{.x = DIR_X * Defaults::Game::BULLET_SPEED, .y = DIR_Y * Defaults::Game::BULLET_SPEED}; body_.velocity = Vec2{.x = DIR_X * bullet_speed, .y = DIR_Y * bullet_speed};
body_.angular_velocity = 0.0F; body_.angular_velocity = 0.0F;
body_.clearAccumulators(); body_.clearAccumulators();
// Reproducir sonido de disparo láser
Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME); Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME);
} }
@@ -87,24 +102,18 @@ void Bullet::update(float /*delta_time*/) {
} }
void Bullet::postUpdate(float /*delta_time*/) { void Bullet::postUpdate(float /*delta_time*/) {
// 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_; prev_position_ = center_;
center_ = body_.position; center_ = body_.position;
// angle_ no cambia (las balas no rotan visualmente).
} }
void Bullet::desactivar() { void Bullet::desactivar() {
is_active_ = false; is_active_ = false;
// Detener el cuerpo físico para que no acumule deriva mientras inactiva.
body_.velocity = Vec2{}; body_.velocity = Vec2{};
body_.angular_velocity = 0.0F; body_.angular_velocity = 0.0F;
} }
void Bullet::draw() const { void Bullet::draw() const {
if (is_active_ && shape_) { if (is_active_ && shape_) {
// Les bales roten segons l'angle de trayectòria (estático tras disparo) Rendering::renderShape(renderer_, shape_, center_, angle_, config_->shape.scale, 1.0F, brightness_, config_->colors.normal);
Rendering::renderShape(renderer_, shape_, center_, angle_, 1.0F, 1.0F, brightness_, Defaults::Palette::BULLET);
} }
} }
+17 -11
View File
@@ -6,10 +6,12 @@
#include <cstdint> #include <cstdint>
#include "core/defaults.hpp"
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/types.hpp" #include "core/types.hpp"
// Forward declaration — la definició completa s'inclou només al .cpp.
struct BulletConfig;
class Bullet : public Entities::Entity { class Bullet : public Entities::Entity {
public: public:
Bullet() Bullet()
@@ -17,7 +19,11 @@ class Bullet : public Entities::Entity {
explicit Bullet(Rendering::Renderer* renderer); explicit Bullet(Rendering::Renderer* renderer);
void init() override; void init() override;
void fire(const Vec2& position, float angle, uint8_t owner_id); // cfg = nullptr → manté el config actual (per defecte: BulletRegistry::get()).
// cfg != nullptr → substitueix el config per a aquesta bala (recarrega
// shape, recalcula collision_radius_, mass, etc.). Útil per a bales
// d'enemic amb shape pròpia.
void fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed, const BulletConfig* cfg = nullptr);
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;
@@ -25,25 +31,25 @@ class Bullet : public Entities::Entity {
// Override: Interfaz de Entity // Override: Interfaz de Entity
[[nodiscard]] auto isActive() const -> bool override { return is_active_; } [[nodiscard]] auto isActive() const -> bool override { return is_active_; }
// Override: Interfaz de colisión (gameplay-level: PLAYAREA bounds-check) // Override: Interfaz de colisión (radi derivat al ctor des del shape).
[[nodiscard]] auto getCollisionRadius() const -> float override { [[nodiscard]] auto getCollisionRadius() const -> float override { return collision_radius_; }
return Defaults::Entities::BULLET_RADIUS;
}
[[nodiscard]] auto isCollidable() const -> bool override { [[nodiscard]] auto isCollidable() const -> bool override {
return is_active_; return is_active_;
} }
// Configuració associada (vàlida des del ctor — la bala l'agafa del BulletRegistry).
[[nodiscard]] auto getConfig() const -> const BulletConfig& { return *config_; }
// 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_; }
// Posició al final del frame anterior, per a CCD segment-vs-cercle.
[[nodiscard]] auto getPrevPosition() const -> const Vec2& { return prev_position_; } [[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_). const BulletConfig* config_{nullptr}; // apunta al BulletRegistry; vàlid post-ctor
// Inicializados en la declaración para que tanto el ctor por defecto como el que toma renderer float collision_radius_{0.0F}; // derivat: shape.bounding_radius × scale × collision_factor
// 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
Vec2 prev_position_{}; // Posició al final del frame anterior (per a swept collision) Vec2 prev_position_{}; // posició al final del frame anterior (swept CCD)
}; };
+80
View File
@@ -0,0 +1,80 @@
// bullet_config.cpp - Implementació del parser de BulletConfig
// © 2026 JailDesigner
#include "game/entities/bullet_config.hpp"
#include <cstdint>
#include <exception>
#include <iostream>
#include <string>
namespace {
auto parseColor(const fkyaml::node& node, SDL_Color& out) -> bool {
if (!node.is_sequence() || node.size() != 3) {
return false;
}
const auto R = node[0].get_value<uint32_t>();
const auto G = node[1].get_value<uint32_t>();
const auto B = node[2].get_value<uint32_t>();
out = SDL_Color{
.r = static_cast<uint8_t>(R),
.g = static_cast<uint8_t>(G),
.b = static_cast<uint8_t>(B),
.a = 255};
return true;
}
auto parseShape(const fkyaml::node& node, BulletConfig::ShapeCfg& out) -> bool {
if (!node.contains("shape") || !node["shape"].contains("path")) {
std::cerr << "[BulletConfig] Error: falta 'shape.path'\n";
return false;
}
const auto& s = node["shape"];
out.path = s["path"].get_value<std::string>();
out.scale = s.contains("scale") ? s["scale"].get_value<float>() : 1.0F;
out.collision_factor = s.contains("collision_factor")
? s["collision_factor"].get_value<float>()
: 1.0F;
return true;
}
auto parsePhysics(const fkyaml::node& node, BulletConfig::PhysicsCfg& out) -> bool {
if (!node.contains("physics")) {
std::cerr << "[BulletConfig] Error: falta 'physics'\n";
return false;
}
const auto& p = node["physics"];
out.mass = p["mass"].get_value<float>();
out.restitution = p["restitution"].get_value<float>();
out.linear_damping = p["linear_damping"].get_value<float>();
out.angular_damping = p["angular_damping"].get_value<float>();
out.impact_momentum_factor = p["impact_momentum_factor"].get_value<float>();
return true;
}
auto parseColors(const fkyaml::node& node, BulletConfig::ColorsCfg& out) -> bool {
if (!node.contains("colors") || !parseColor(node["colors"]["normal"], out.normal)) {
std::cerr << "[BulletConfig] Error: 'colors.normal' no és [r,g,b]\n";
return false;
}
return true;
}
} // namespace
auto BulletConfig::fromYaml(const fkyaml::node& node) -> std::optional<BulletConfig> {
try {
BulletConfig cfg;
cfg.name = node.contains("name") ? node["name"].get_value<std::string>() : "bullet";
if (!parseShape(node, cfg.shape)) { return std::nullopt; }
if (!parsePhysics(node, cfg.physics)) { return std::nullopt; }
if (!parseColors(node, cfg.colors)) { return std::nullopt; }
return cfg;
} catch (const std::exception& e) {
std::cerr << "[BulletConfig] Excepció parsejant: " << e.what() << '\n';
return std::nullopt;
}
}
+41
View File
@@ -0,0 +1,41 @@
// bullet_config.hpp - Configuració de la bala carregada des de YAML
// © 2026 JailDesigner
//
// Paral·lel a Player/EnemyConfig. Una sola instància a tot el joc (per ara);
// es comparteix entre totes les bales actives via BulletRegistry.
#pragma once
#include <SDL3/SDL.h>
#include <optional>
#include <string>
#include "external/fkyaml_node.hpp"
struct BulletConfig {
struct ShapeCfg {
std::string path;
float scale;
float collision_factor;
};
struct PhysicsCfg {
float mass;
float restitution;
float linear_damping;
float angular_damping;
float impact_momentum_factor; // factor de transferència bullet→enemic
};
struct ColorsCfg {
SDL_Color normal;
};
std::string name;
ShapeCfg shape;
PhysicsCfg physics;
ColorsCfg colors;
static auto fromYaml(const fkyaml::node& node) -> std::optional<BulletConfig>;
};
+56
View File
@@ -0,0 +1,56 @@
// bullet_registry.cpp - Implementació del registre de bales
// © 2026 JailDesigner
#include "game/entities/bullet_registry.hpp"
#include <cstdlib>
#include <iostream>
#include "core/entities/entity_loader.hpp"
std::unordered_map<std::string, BulletConfig> BulletRegistry::configs;
bool BulletRegistry::loaded = false;
namespace {
// Tria comú: carrega el YAML d'un name, parseja a BulletConfig i el guarda
// al map. Retorna punter al config inserit, o nullptr si falla.
auto loadInto(std::unordered_map<std::string, BulletConfig>& configs, const std::string& name) -> const BulletConfig* {
auto yaml = Entities::EntityLoader::load(name);
if (!yaml) {
std::cerr << "[BulletRegistry] Error: no s'ha pogut carregar " << name << ".yaml\n";
return nullptr;
}
auto cfg = BulletConfig::fromYaml(*yaml);
if (!cfg) {
std::cerr << "[BulletRegistry] Error: format invàlid a " << name << ".yaml\n";
return nullptr;
}
auto [it, _] = configs.insert_or_assign(name, *cfg);
return &it->second;
}
} // namespace
auto BulletRegistry::load() -> bool {
if (loadInto(configs, "bullet") == nullptr) {
return false;
}
loaded = true;
std::cout << "[BulletRegistry] Configuració de bala 'bullet' carregada.\n";
return true;
}
auto BulletRegistry::get() -> const BulletConfig& {
if (!loaded) {
std::cerr << "[BulletRegistry] FATAL: get() abans de load()\n";
std::exit(EXIT_FAILURE);
}
return configs.at("bullet");
}
auto BulletRegistry::get(const std::string& name) -> const BulletConfig* {
auto it = configs.find(name);
if (it != configs.end()) {
return &it->second;
}
return loadInto(configs, name);
}
+34
View File
@@ -0,0 +1,34 @@
// bullet_registry.hpp - Registre estàtic de configuracions de bala
// © 2026 JailDesigner
//
// Diverses configuracions per nom (data/entities/<name>/<name>.yaml). El
// config "bullet" es manté com a default històric (player) i es carrega via
// load(). Els altres (ex. "bullet_long" per a bales d'enemic) es carreguen
// peresosament la primera vegada que algú els demana per nom.
#pragma once
#include <string>
#include <unordered_map>
#include "game/entities/bullet_config.hpp"
class BulletRegistry {
public:
BulletRegistry() = delete;
// Carrega data/entities/bullet/bullet.yaml com a default. Retorna false si falla.
static auto load() -> bool;
// Accés a la configuració per defecte ("bullet"). Avorta amb log fatal si
// load() no s'ha cridat o ha fallat. Mantingut per a backwards compat del Bullet ctor.
static auto get() -> const BulletConfig&;
// Accés a una configuració per nom. Lazy-load: si no està al map, intenta
// carregar data/entities/<name>/<name>.yaml. Retorna nullptr si no es pot.
static auto get(const std::string& name) -> const BulletConfig*;
private:
static std::unordered_map<std::string, BulletConfig> configs;
static bool loaded;
};
+99 -250
View File
@@ -14,6 +14,8 @@
#include "core/rendering/shape_renderer.hpp" #include "core/rendering/shape_renderer.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/constants.hpp" #include "game/constants.hpp"
#include "game/entities/enemy_config.hpp"
#include "game/entities/enemy_registry.hpp"
namespace { namespace {
@@ -26,100 +28,70 @@ namespace {
}; };
} }
// Recupera el "ángulo equivalente" de un body en movimiento (para zigzag). // Random float [0..1).
// Si está parado, devuelve 0. auto randFloat01() -> float {
auto velocityToAngle(const Vec2& velocity) -> float { return static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
if (velocity.lengthSquared() < 0.0001F) { }
return 0.0F;
} // Random float [min..max).
// El movimiento (vx, vy) corresponde a angle - PI/2; invertimos. auto randRange(float min, float max) -> float {
return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F); return min + (randFloat01() * (max - min));
} }
} // namespace } // namespace
Enemy::Enemy(Rendering::Renderer* renderer) Enemy::Enemy(Rendering::Renderer* renderer)
: Entity(renderer), : Entity(renderer) {
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. // Body queda amb defaults inocus (radius=0 = no col·lisiona) fins
// init() ajusta velocidad y masa según el tipo (Pentagon/Quadrat/Molinillo). // que init() apliqui la configuració del tipus carregada via Registry.
body_.setMass(Defaults::Enemies::Body::DEFAULT_MASS); body_.radius = 0.0F;
body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo)
body_.restitution = Defaults::Enemies::Body::RESTITUTION;
body_.linear_damping = Defaults::Enemies::Body::LINEAR_DAMPING;
body_.angular_damping = Defaults::Enemies::Body::ANGULAR_DAMPING;
} }
void Enemy::init(EnemyType type, const Vec2* ship_pos) { void Enemy::init(EnemyType type, const Vec2* ship_pos) {
type_ = type; type_ = type;
config_ = &EnemyRegistry::get(type);
const EnemyConfig& cfg = *config_;
const char* shape_file = nullptr; ai_state_ = EnemyAiState{};
float base_speed = 0.0F; ai_state_.tracking_strength = cfg.ai.movement.tracking_strength;
float rotation_delta_min = 0.0F;
float rotation_delta_max = 0.0F;
float type_mass = Defaults::Enemies::Body::DEFAULT_MASS;
switch (type_) { // Timers paral·lels a tick: random [0, interval) per evitar que tots els
case EnemyType::PENTAGON: // enemics del mateix tipus disparin sincronitzats al spawn.
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE; ai_tick_timers_.resize(cfg.ai.tick.size());
base_speed = Defaults::Enemies::Pentagon::SPEED; for (std::size_t i = 0; i < cfg.ai.tick.size(); ++i) {
rotation_delta_min = Defaults::Enemies::Pentagon::ROTATION_DELTA_MIN; ai_tick_timers_[i] = randFloat01() * cfg.ai.tick[i].interval;
rotation_delta_max = Defaults::Enemies::Pentagon::ROTATION_DELTA_MAX;
type_mass = Defaults::Enemies::Pentagon::MASS;
break;
case EnemyType::SQUARE:
shape_file = Defaults::Enemies::Square::SHAPE_FILE;
base_speed = Defaults::Enemies::Square::SPEED;
rotation_delta_min = Defaults::Enemies::Square::ROTATION_DELTA_MIN;
rotation_delta_max = Defaults::Enemies::Square::ROTATION_DELTA_MAX;
type_mass = Defaults::Enemies::Square::MASS;
tracking_timer_ = 0.0F;
break;
case EnemyType::PINWHEEL:
shape_file = Defaults::Enemies::Pinwheel::SHAPE_FILE;
base_speed = Defaults::Enemies::Pinwheel::SPEED;
rotation_delta_min = Defaults::Enemies::Pinwheel::ROTATION_DELTA_MIN;
rotation_delta_max = Defaults::Enemies::Pinwheel::ROTATION_DELTA_MAX;
type_mass = Defaults::Enemies::Pinwheel::MASS;
break;
default:
std::cerr << "[Enemy] Error: tipo desconocido ("
<< static_cast<int>(type_) << "), usando PENTAGON\n";
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
base_speed = Defaults::Enemies::Pentagon::SPEED;
rotation_delta_min = Defaults::Enemies::Pentagon::ROTATION_DELTA_MIN;
rotation_delta_max = Defaults::Enemies::Pentagon::ROTATION_DELTA_MAX;
break;
} }
body_.setMass(type_mass); shape_ = Graphics::ShapeLoader::load(cfg.shape.path);
body_.radius = Defaults::Entities::ENEMY_RADIUS;
// Cargar shape
shape_ = Graphics::ShapeLoader::load(shape_file);
if (!shape_ || !shape_->isValid()) { if (!shape_ || !shape_->isValid()) {
std::cerr << "[Enemy] Error: no se ha podido cargar " << shape_file << '\n'; std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n';
} }
// Posición aleatoria con comprobación de seguridad // Radi de col·lisió derivat del cercle circumscrit de la shape.
const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F;
collision_radius_ = BOUNDING * cfg.shape.scale * cfg.shape.collision_factor;
body_.setMass(cfg.physics.mass);
body_.radius = collision_radius_;
body_.restitution = cfg.physics.restitution;
body_.linear_damping = cfg.physics.linear_damping;
body_.angular_damping = cfg.physics.angular_damping;
// Posició aleatòria amb comprovació de safety_distance.
float min_x; float min_x;
float max_x; float max_x;
float min_y; float min_y;
float max_y; float max_y;
Constants::getSafePlayAreaBounds(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y); Constants::getSafePlayAreaBounds(collision_radius_, min_x, max_x, min_y, max_y);
if (ship_pos != nullptr) { if (ship_pos != nullptr) {
bool found_safe_position = false; bool found_safe_position = false;
for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) { for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) {
float candidate_x; float candidate_x;
float candidate_y; float candidate_y;
if (attemptSafeSpawn(*ship_pos, candidate_x, candidate_y)) { if (attemptSafeSpawn(*ship_pos, collision_radius_, cfg.spawn.safety_distance, candidate_x, candidate_y)) {
center_.x = candidate_x; center_.x = candidate_x;
center_.y = candidate_y; center_.y = candidate_y;
found_safe_position = true; found_safe_position = true;
@@ -141,33 +113,28 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
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));
} }
// Dirección inicial aleatoria, velocidad escalar según tipo const float ANGLE_INICIAL = static_cast<float>(std::rand() % 360) * Constants::PI / 180.0F;
const float ANGLE_INICIAL = (std::rand() % 360) * Constants::PI / 180.0F; setVelocityFromAngle(ANGLE_INICIAL, cfg.physics.speed);
setVelocityFromAngle(ANGLE_INICIAL, base_speed);
// Sincronizar body_ con posición inicial
body_.position = center_; body_.position = center_;
body_.angle = 0.0F; body_.angle = 0.0F;
body_.angular_velocity = 0.0F; body_.angular_velocity = 0.0F;
body_.clearAccumulators(); body_.clearAccumulators();
// Rotación visual aleatoria (independiente del body) // Rotació visual aleatòria dins del rang del tipus
const float ROTATION_DELTA_RANGE = rotation_delta_max - rotation_delta_min; rotation_delta_ = randRange(cfg.physics.rotation_delta_min, cfg.physics.rotation_delta_max);
rotation_delta_ = rotation_delta_min + ((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * ROTATION_DELTA_RANGE);
rotation_ = 0.0F; rotation_ = 0.0F;
// Estado de animación
animation_ = EnemyAnimation(); animation_ = EnemyAnimation();
animation_.rotation_delta_base = rotation_delta_; animation_.rotation_delta_base = rotation_delta_;
animation_.rotation_delta_target = rotation_delta_; animation_.rotation_delta_target = rotation_delta_;
animation_.rotation_delta_t = 1.0F; animation_.rotation_delta_t = 1.0F;
// Invulnerabilidad post-spawn invulnerability_timer_ = cfg.spawn.invulnerability_duration;
invulnerability_timer_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; brightness_ = cfg.spawn.invulnerability_brightness_start;
brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;
// Timer para próximo cambio de dirección (Pentagon) health_ = cfg.health;
direction_change_timer_ = 0.0F; flash_timer_ = 0.0F;
is_active_ = true; is_active_ = true;
} }
@@ -177,8 +144,6 @@ void Enemy::update(float delta_time) {
return; return;
} }
// Decremento de timer "herido"; al cruzar 0 marca expiración para que el
// system layer dispare la explosión diferida.
wound_expired_this_frame_ = false; wound_expired_this_frame_ = false;
if (wounded_timer_ > 0.0F) { if (wounded_timer_ > 0.0F) {
wounded_timer_ -= delta_time; wounded_timer_ -= delta_time;
@@ -188,45 +153,33 @@ void Enemy::update(float delta_time) {
} }
} }
// Decremento de invulnerabilidad + LERP de brightness if (flash_timer_ > 0.0F) {
flash_timer_ -= delta_time;
flash_timer_ = std::max(flash_timer_, 0.0F);
}
if (invulnerability_timer_ > 0.0F) { if (invulnerability_timer_ > 0.0F) {
invulnerability_timer_ -= delta_time; invulnerability_timer_ -= delta_time;
invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F); invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F);
const float T_INV = invulnerability_timer_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; const float T_INV = invulnerability_timer_ / config_->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; const float START = config_->spawn.invulnerability_brightness_start;
constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_END; const float END = config_->spawn.invulnerability_brightness_end;
brightness_ = START + ((END - START) * SMOOTH_T); brightness_ = START + ((END - START) * SMOOTH_T);
} }
// Comportamiento por tipo (ajusta body_.velocity, NO mueve posición). // El moviment es delega a Systems::EnemyAi::tick, invocat des de l'scene
// Skip cuando está herido: el enemy és un "cos mort" inert, només // ABANS d'aquest update (manté l'ordre: AI escriu velocity/rotation_delta,
// respon a la inèrcia del impulse rebut i a les col·lisions físiques. // després animation pot modular rotation_delta via rotation_accel).
if (!isWounded()) {
switch (type_) {
case EnemyType::PENTAGON:
behaviorPentagon(delta_time);
break;
case EnemyType::SQUARE:
behaviorSquare(delta_time);
break;
case EnemyType::PINWHEEL:
behaviorPinwheel(delta_time);
break;
}
}
// Animaciones (palpitación + rotación acelerada)
updateAnimation(delta_time); updateAnimation(delta_time);
// Rotación visual (decoración, no afecta movimiento)
rotation_ += rotation_delta_ * 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.
if (is_active_) { if (is_active_) {
center_ = body_.position; center_ = body_.position;
} }
@@ -236,48 +189,42 @@ void Enemy::draw() const {
if (!is_active_ || !shape_) { if (!is_active_ || !shape_) {
return; return;
} }
const float SCALE = computeCurrentScale(); const float SCALE = config_->shape.scale * computeCurrentScale();
SDL_Color color{}; SDL_Color color = config_->colors.normal;
switch (type_) {
case EnemyType::PENTAGON:
color = Defaults::Palette::PENTAGON;
break;
case EnemyType::SQUARE:
color = Defaults::Palette::SQUARE;
break;
case EnemyType::PINWHEEL:
color = Defaults::Palette::PINWHEEL;
break;
}
// Parpadeo dorado mientras está herido: alterna color de tipo ↔ dorado
// a Wounded::BLINK_HZ usando el timer (fmod sobre el periodo).
if (wounded_timer_ > 0.0F) { if (wounded_timer_ > 0.0F) {
const float CYCLE = 1.0F / Defaults::Enemies::Wounded::BLINK_HZ; const float CYCLE = 1.0F / config_->wounded.blink_hz;
const float T = std::fmod(wounded_timer_, CYCLE); const float T = std::fmod(wounded_timer_, CYCLE);
if (T < (CYCLE / 2.0F)) { if (T < (CYCLE / 2.0F)) {
color = Defaults::Palette::WOUNDED; color = config_->colors.wounded;
} }
} }
Rendering::renderShape(renderer_, shape_, center_, rotation_, SCALE, 1.0F, brightness_, color); // Flash d'impacte parcial (HP>1): força el color a blanc i el brillo a
// 1.0 durant la finestra de flash. Té prioritat sobre el blink wounded.
float effective_brightness = brightness_;
if (flash_timer_ > 0.0F) {
color = SDL_Color{.r = 255, .g = 255, .b = 255, .a = 255};
effective_brightness = 1.0F;
}
Rendering::renderShape(renderer_, shape_, center_, rotation_, SCALE, 1.0F, effective_brightness, color);
} }
void Enemy::destroy() { void Enemy::destroy() {
is_active_ = 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;
wounded_timer_ = 0.0F; wounded_timer_ = 0.0F;
wound_expired_this_frame_ = false; wound_expired_this_frame_ = false;
last_hit_by_ = 0xFF; last_hit_by_ = 0xFF;
flash_timer_ = 0.0F;
} }
void Enemy::hurt(uint8_t shooter_id) { void Enemy::hurt(uint8_t shooter_id) {
wounded_timer_ = Defaults::Enemies::Wounded::DURATION; wounded_timer_ = config_->wounded.duration;
last_hit_by_ = shooter_id; last_hit_by_ = shooter_id;
// 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) {
@@ -285,13 +232,11 @@ void Enemy::applyImpulse(const Vec2& impulse) {
} }
void Enemy::setVelocity(float speed) { void Enemy::setVelocity(float speed) {
// Mantener la dirección actual del body, cambiar solo la magnitud.
const float CURRENT_SPEED = body_.velocity.length(); const float CURRENT_SPEED = body_.velocity.length();
if (CURRENT_SPEED > 0.0F) { if (CURRENT_SPEED > 0.0F) {
body_.velocity = body_.velocity * (speed / CURRENT_SPEED); body_.velocity = body_.velocity * (speed / CURRENT_SPEED);
} else { } else {
// Sin dirección actual: usar ángulo aleatorio const float A = static_cast<float>(std::rand() % 360) * Constants::PI / 180.0F;
const float A = (std::rand() % 360) * Constants::PI / 180.0F;
setVelocityFromAngle(A, speed); setVelocityFromAngle(A, speed);
} }
} }
@@ -300,109 +245,32 @@ void Enemy::setVelocityFromAngle(float angle_movement, float speed) {
body_.velocity = angleToDirection(angle_movement) * speed; body_.velocity = angleToDirection(angle_movement) * speed;
} }
// PENTAGON: zigzag esquivador. Cambios de dirección periódicos (probabilísticos)
// en lugar de detectar paredes; el rebote contra muros lo hace PhysicsWorld
// con restitution=1.0.
void Enemy::behaviorPentagon(float delta_time) {
direction_change_timer_ += delta_time;
// Probabilidad de zigzag por segundo (calibrada para sensación equivalente
// a la versión vieja que disparaba en cada toque de pared).
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
if (RAND_VAL < Defaults::Enemies::Pentagon::ZIGZAG_PROB_PER_SECOND * delta_time) {
const float CURRENT_ANGLE = velocityToAngle(body_.velocity);
const float DELTA = (static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) *
Defaults::Enemies::Pentagon::ANGLE_CHANGE_MAX;
const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA);
const float SPEED = body_.velocity.length();
setVelocityFromAngle(NEW_ANGLE, SPEED);
direction_change_timer_ = 0.0F;
}
}
// SQUARE: tracking discreto cada TRACKING_INTERVAL. Ajusta dirección
// hacia el ship mezclando con tracking_strength_.
void Enemy::behaviorSquare(float delta_time) {
tracking_timer_ += delta_time;
if (tracking_timer_ >= Defaults::Enemies::Square::TRACKING_INTERVAL && ship_position_ != nullptr) {
tracking_timer_ = 0.0F;
const Vec2 TO_SHIP = *ship_position_ - center_;
const float DIST = TO_SHIP.length();
if (DIST > 0.0F) {
const Vec2 DESIRED_DIR = TO_SHIP / DIST;
const float SPEED = body_.velocity.length();
const Vec2 DESIRED_VEL = DESIRED_DIR * SPEED;
// Mezcla LERP: velocidad actual con la deseada según tracking_strength_.
body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) +
(DESIRED_VEL * tracking_strength_);
// Renormalizar a la velocidad escalar original
const float NEW_SPEED = body_.velocity.length();
if (NEW_SPEED > 0.0F) {
body_.velocity = body_.velocity * (SPEED / NEW_SPEED);
}
}
}
}
// PINWHEEL: movimiento recto + boost de rotación visual cerca del ship.
// Sin tracking — solo cambios de dirección raros (igual que Pentagon pero
// con probabilidad mucho menor).
void Enemy::behaviorPinwheel(float /*delta_time*/) {
// Boost de rotación visual por proximidad al ship
if (ship_position_ != nullptr) {
const Vec2 TO_SHIP = *ship_position_ - center_;
const float DIST = TO_SHIP.length();
if (DIST < Defaults::Enemies::Pinwheel::PROXIMITY_DISTANCE) {
rotation_delta_ = animation_.rotation_delta_base * Defaults::Enemies::Pinwheel::ROTATION_DELTA_PROXIMITY_MULTIPLIER;
} else {
rotation_delta_ = animation_.rotation_delta_base;
}
}
// Movimiento lineal puro: el world se encarga de integrar y rebotar.
}
void Enemy::updateAnimation(float delta_time) { void Enemy::updateAnimation(float delta_time) {
updatePulse(delta_time); updatePulse(delta_time);
updateRotationAcceleration(delta_time); updateRotationAcceleration(delta_time);
} }
void Enemy::updatePulse(float delta_time) { void Enemy::updatePulse(float delta_time) {
const auto& cfg = config_->animation.pulse;
if (animation_.pulse_active) { if (animation_.pulse_active) {
animation_.pulse_phase += 2.0F * Constants::PI * animation_.pulse_frequency * delta_time; animation_.pulse_phase += 2.0F * Constants::PI * animation_.pulse_frequency * delta_time;
animation_.pulse_time_remaining -= delta_time; animation_.pulse_time_remaining -= delta_time;
if (animation_.pulse_time_remaining <= 0.0F) { if (animation_.pulse_time_remaining <= 0.0F) {
animation_.pulse_active = false; animation_.pulse_active = false;
} }
} else { return;
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX); }
const float TRIGGER_PROB = Defaults::Enemies::Animation::PULSE_TRIGGER_PROB * delta_time; if (randFloat01() < cfg.trigger_prob_per_second * delta_time) {
if (RAND_VAL < TRIGGER_PROB) { animation_.pulse_active = true;
animation_.pulse_active = true; animation_.pulse_phase = 0.0F;
animation_.pulse_phase = 0.0F; animation_.pulse_frequency = randRange(cfg.frequency_min, cfg.frequency_max);
animation_.pulse_amplitude = randRange(cfg.amplitude_min, cfg.amplitude_max);
const float FREQ_RANGE = Defaults::Enemies::Animation::PULSE_FREQ_MAX - animation_.pulse_time_remaining = randRange(cfg.duration_min, cfg.duration_max);
Defaults::Enemies::Animation::PULSE_FREQ_MIN;
animation_.pulse_frequency = Defaults::Enemies::Animation::PULSE_FREQ_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * FREQ_RANGE);
const float AMP_RANGE = Defaults::Enemies::Animation::PULSE_AMPLITUD_MAX -
Defaults::Enemies::Animation::PULSE_AMPLITUD_MIN;
animation_.pulse_amplitude = Defaults::Enemies::Animation::PULSE_AMPLITUD_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * AMP_RANGE);
const float DUR_RANGE = Defaults::Enemies::Animation::PULSE_DURATION_MAX -
Defaults::Enemies::Animation::PULSE_DURATION_MIN;
animation_.pulse_time_remaining = Defaults::Enemies::Animation::PULSE_DURATION_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE);
}
} }
} }
void Enemy::updateRotationAcceleration(float delta_time) { void Enemy::updateRotationAcceleration(float delta_time) {
const auto& cfg = config_->animation.rotation_accel;
if (animation_.rotation_delta_t < 1.0F) { if (animation_.rotation_delta_t < 1.0F) {
animation_.rotation_delta_t += delta_time / animation_.rotation_delta_duration; animation_.rotation_delta_t += delta_time / animation_.rotation_delta_duration;
if (animation_.rotation_delta_t >= 1.0F) { if (animation_.rotation_delta_t >= 1.0F) {
@@ -416,34 +284,24 @@ void Enemy::updateRotationAcceleration(float delta_time) {
const float TARGET = animation_.rotation_delta_target; const float TARGET = animation_.rotation_delta_target;
rotation_delta_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T); rotation_delta_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T);
} }
} else { return;
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX); }
const float TRIGGER_PROB = Defaults::Enemies::Animation::ROTATION_ACCEL_TRIGGER_PROB * delta_time; if (randFloat01() < cfg.trigger_prob_per_second * delta_time) {
if (RAND_VAL < TRIGGER_PROB) { animation_.rotation_delta_t = 0.0F;
animation_.rotation_delta_t = 0.0F; const float MULTIPLIER = randRange(cfg.multiplier_min, cfg.multiplier_max);
animation_.rotation_delta_target = animation_.rotation_delta_base * MULTIPLIER;
const float MULT_RANGE = Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MAX - animation_.rotation_delta_duration = randRange(cfg.duration_min, cfg.duration_max);
Defaults::Enemies::Animation::ROTATION_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);
animation_.rotation_delta_target = animation_.rotation_delta_base * MULTIPLIER;
const float DUR_RANGE = Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MAX -
Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MIN;
animation_.rotation_delta_duration = Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE);
}
} }
} }
auto Enemy::computeCurrentScale() const -> float { auto Enemy::computeCurrentScale() const -> float {
float scale = 1.0F; float scale = 1.0F;
if (invulnerability_timer_ > 0.0F) { if (invulnerability_timer_ > 0.0F) {
const float T_INV = invulnerability_timer_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; const float T_INV = invulnerability_timer_ / config_->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; const float START = config_->spawn.invulnerability_scale_start;
constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_END; const float END = config_->spawn.invulnerability_scale_end;
scale = START + ((END - START) * SMOOTH_T); scale = START + ((END - START) * SMOOTH_T);
} else if (animation_.pulse_active) { } else if (animation_.pulse_active) {
scale += animation_.pulse_amplitude * std::sin(animation_.pulse_phase); scale += animation_.pulse_amplitude * std::sin(animation_.pulse_phase);
@@ -452,16 +310,7 @@ auto Enemy::computeCurrentScale() const -> float {
} }
auto Enemy::getBaseVelocity() const -> float { auto Enemy::getBaseVelocity() const -> float {
switch (type_) { return EnemyRegistry::get(type_).physics.speed;
case EnemyType::PENTAGON:
return Defaults::Enemies::Pentagon::SPEED;
case EnemyType::SQUARE:
return Defaults::Enemies::Square::SPEED;
case EnemyType::PINWHEEL:
return Defaults::Enemies::Pinwheel::SPEED;
default:
return Defaults::Enemies::Pentagon::SPEED;
}
} }
auto Enemy::getBaseRotation() const -> float { auto Enemy::getBaseRotation() const -> float {
@@ -470,16 +319,16 @@ auto Enemy::getBaseRotation() const -> float {
void Enemy::setTrackingStrength(float strength) { void Enemy::setTrackingStrength(float strength) {
if (type_ == EnemyType::SQUARE) { if (type_ == EnemyType::SQUARE) {
tracking_strength_ = strength; ai_state_.tracking_strength = strength;
} }
} }
auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool { auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float safety_distance, float& out_x, float& out_y) -> bool {
float min_x; float min_x;
float max_x; float max_x;
float min_y; float min_y;
float max_y; float max_y;
Constants::getSafePlayAreaBounds(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y); Constants::getSafePlayAreaBounds(collision_radius, min_x, max_x, min_y, max_y);
const int RANGE_X = static_cast<int>(max_x - min_x); const int RANGE_X = static_cast<int>(max_x - min_x);
const int RANGE_Y = static_cast<int>(max_y - min_y); const int RANGE_Y = static_cast<int>(max_y - min_y);
@@ -489,5 +338,5 @@ auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -
const float DX = out_x - ship_pos.x; const float DX = out_x - ship_pos.x;
const float DY = out_y - ship_pos.y; const float DY = out_y - ship_pos.y;
const float DISTANCE = std::sqrt((DX * DX) + (DY * DY)); const float DISTANCE = std::sqrt((DX * DX) + (DY * DY));
return DISTANCE >= Defaults::Enemies::Spawn::SAFETY_DISTANCE; return DISTANCE >= safety_distance;
} }
+77 -29
View File
@@ -4,19 +4,29 @@
#pragma once #pragma once
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <array>
#include <cstdint> #include <cstdint>
#include <vector>
#include "core/defaults.hpp" #include "core/defaults/enemies.hpp"
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/entities/enemy_ai.hpp"
class Ship;
// 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)
SQUARE = 1, // Square perseguidor (tracks ship) SQUARE = 1, // Square perseguidor (tracks ship)
PINWHEEL = 2 // Molinillo agresivo (rápido, girando) PINWHEEL = 2, // Molinillo agresivo (rápido, girando)
STAR = 3, // Estrella de 5 puntes (clone visual de Pentagon, comportament zigzag)
ORB = 4, // Orb gegant tough (HP=10, chase lent — primer enemic HP>1)
}; };
// Forward declaration — EnemyConfig viu a enemy_config.hpp i s'inclou només a enemy.cpp.
struct EnemyConfig;
// 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)
@@ -48,10 +58,8 @@ class Enemy : public Entities::Entity {
// Override: Interfaz de Entity // Override: Interfaz de Entity
[[nodiscard]] auto isActive() const -> bool override { return is_active_; } [[nodiscard]] auto isActive() const -> bool override { return is_active_; }
// Override: Interfaz de colisión // Override: Interfaz de colisión. El radi ve del config carregat per tipus.
[[nodiscard]] auto getCollisionRadius() const -> float override { [[nodiscard]] auto getCollisionRadius() const -> float override { return collision_radius_; }
return Defaults::Entities::ENEMY_RADIUS;
}
// Mentre fa spawn (invulnerable) segueix col·lisionant: les bales el // 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 // poden abatre i el cos físic rebota amb la nau. El damage a la nau
// segueix filtrat per `isInvulnerable()` al detectShipEnemy. // segueix filtrat per `isInvulnerable()` al detectShipEnemy.
@@ -65,9 +73,22 @@ class Enemy : public Entities::Entity {
// Getters // Getters
[[nodiscard]] auto getRotationDelta() const -> float { return rotation_delta_; } [[nodiscard]] auto getRotationDelta() const -> float { return rotation_delta_; }
[[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; } [[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; }
// Configuració activa (carregada al darrer init()). Vàlida mentre l'enemic
// ha estat inicialitzat almenys un cop; abans és nullptr.
[[nodiscard]] auto getConfig() const -> const EnemyConfig& { return *config_; }
// Set ship position reference for tracking behavior // Referències als 2 ships per a AI de tracking/proximity/chase/flee.
void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; } // nullptr = ship inexistent al match. El sistema d'IA filtra per ship->isActive().
void setShips(const Ship* p1, const Ship* p2) { ships_ = {p1, p2}; }
[[nodiscard]] auto getShips() const -> const std::array<const Ship*, 2>& { return ships_; }
// Accessors per al sistema d'IA (Systems::EnemyAi).
[[nodiscard]] auto getAiState() -> EnemyAiState& { return ai_state_; }
[[nodiscard]] auto getAiTickTimers() -> std::vector<float>& { return ai_tick_timers_; }
[[nodiscard]] auto getRotationBase() const -> float { return animation_.rotation_delta_base; }
void setRotationDelta(float rot) { rotation_delta_ = rot; }
// Public: el sistema d'IA reorienta la velocitat des d'un angle.
void setVelocityFromAngle(float angle_movement, float speed);
// Stage system API (base stats) // Stage system API (base stats)
[[nodiscard]] auto getBaseVelocity() const -> float; [[nodiscard]] auto getBaseVelocity() const -> float;
@@ -97,46 +118,73 @@ class Enemy : public Entities::Entity {
void consumeWoundExpired() { wound_expired_this_frame_ = false; } void consumeWoundExpired() { wound_expired_this_frame_ = false; }
[[nodiscard]] auto getLastHitBy() const -> uint8_t { return last_hit_by_; } [[nodiscard]] auto getLastHitBy() const -> uint8_t { return last_hit_by_; }
// Salut: decrementada per l'acció DECREASE_HEALTH al dispatcher d'events.
// Quan arriba a 0 o menys, el dispatcher dispara ON_NO_HEALTH (que
// típicament encadena SET_HURT o DESTROY al YAML). last_hit_by s'actualitza
// al decrement perquè la mort posterior atribueixi correctament el kill.
[[nodiscard]] auto getHealth() const -> int { return health_; }
void decrementHealth(uint8_t shooter_id = 0xFF) {
--health_;
last_hit_by_ = shooter_id;
}
// Flash visual: brief impacto-feedback quan rep un hit no letal (HP>1).
// Disparat per l'acció FLASH; el render alça la lluminositat mentre dura.
void triggerFlash() { flash_timer_ = Defaults::Enemies::Visual::FLASH_DURATION; }
// Aplica un impulso (cambio inmediato de velocidad mass-aware) al cuerpo físico. // Aplica un impulso (cambio inmediato de velocidad mass-aware) al cuerpo físico.
void applyImpulse(const Vec2& impulse); void applyImpulse(const Vec2& impulse);
private: private:
// Miembros específicos (heredados: renderer_, shape_, center_, angle_, brightness_, body_). // Configuració carregada per tipus (apunta a una entrada de EnemyRegistry).
// Inicializados en la declaración: el ctor por defecto deja al enemy en estado "inactivo // nullptr abans del primer init(); per això getConfig() només és vàlid post-init.
// como pentágono", coherente con lo que harán init() o el ctor con renderer al activarlo. const EnemyConfig* config_{nullptr};
float rotation_delta_{0.0F}; // Velocidad angular visual (rad/s) — solo decoración, separada de body_.angular_velocity
float rotation_{0.0F}; // Rotación visual acumulada (no afecta movimiento) // Cache local del radi (per evitar dereferenciar config_ a getCollisionRadius);
// s'actualitza a init() segons el tipus.
float collision_radius_{0.0F};
float rotation_delta_{0.0F}; // Velocidad angular visual (rad/s)
float rotation_{0.0F}; // Rotación visual acumulada
bool is_active_{false}; bool is_active_{false};
EnemyType type_{EnemyType::PENTAGON}; EnemyType type_{EnemyType::PENTAGON};
EnemyAnimation animation_; EnemyAnimation animation_;
// Comportamiento type-specific // Estat per-instància que la primitiva de moviment manté entre frames.
float tracking_timer_{0.0F}; // Quadrat: tiempo desde último update de dirección EnemyAiState ai_state_;
const Vec2* ship_position_{nullptr}; // Puntero a posición de la nave (para tracking)
float tracking_strength_{0.0F}; // Quadrat: intensidad de tracking (0.0-1.5), default 0.5 // Timers paral·lels a config_->ai.tick: timers_[i] és el temps restant
float direction_change_timer_{0.0F}; // Pentagon: tiempo para próximo cambio de dirección // (en segons) fins a la pròxima execució de l'acció i. Re-dimensionat a
// init() segons la mida de config_->ai.tick.
std::vector<float> ai_tick_timers_;
// Referències als 2 ships per a AI de tracking/proximity/chase/flee.
std::array<const Ship*, 2> ships_{nullptr, nullptr};
// Invulnerabilidad post-spawn // Invulnerabilidad post-spawn
float invulnerability_timer_{0.0F}; float invulnerability_timer_{0.0F};
// Estado "herido": timer cuenta atrás; al cruzar 0 se marca expiración. // Estado "herido"
float wounded_timer_{0.0F}; float wounded_timer_{0.0F};
bool wound_expired_this_frame_{false}; bool wound_expired_this_frame_{false};
uint8_t last_hit_by_{0xFF}; // 0xFF = sin atribución uint8_t last_hit_by_{0xFF};
// Salut per-instància. Reseteja a config_->health a init(); el dispatcher
// d'events la decrementa via DECREASE_HEALTH i dispara ON_NO_HEALTH quan
// creua zero. Permet enemics tough (HP>1) sense canvis al motor.
int health_{1};
// Flash visual temporitzat per a feedback d'impacte parcial (HP>1).
// L'acció FLASH el reseteja a FLASH_DURATION; draw() alça la lluminositat
// mentre dura, i update() el decrementa.
float flash_timer_{0.0F};
// Métodos privados // Métodos privados
void updateAnimation(float delta_time); void updateAnimation(float delta_time);
void updatePulse(float delta_time); void updatePulse(float delta_time);
void updateRotationAcceleration(float delta_time); void updateRotationAcceleration(float delta_time);
void behaviorPentagon(float delta_time);
void behaviorSquare(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. // Static: passa els paràmetres com a args per no acoblar a *this.
static auto attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool; static auto attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float safety_distance, float& out_x, float& out_y) -> bool;
// Helper: setear body_.velocity desde un ángulo y magnitud.
// angle_movement=0 apunta hacia arriba (eje Y negativo SDL).
void setVelocityFromAngle(float angle_movement, float speed);
}; };
+84
View File
@@ -0,0 +1,84 @@
// enemy_ai.hpp - Sistema declaratiu d'IA per a enemics
// © 2026 JailDesigner
//
// Cada enemic declara al seu YAML quin movement primitiu fa servir i, opcional-
// ment, una llista d'accions periòdiques (tick). El motor només dispatcha; el
// comportament viu a les dades. Patró paral·lel al d'events declaratius
// (enemy_event.hpp).
#pragma once
#include <cstdint>
#include <string>
#include <vector>
// Primitiva de moviment activa per a un enemic. Substitueix el switch
// hardcoded sobre EnemyType.
enum class MovementType : uint8_t {
ZIGZAG, // Canvi de direcció probabilístic agressiu (Pentagon/Star)
TRACKING, // LERP discret cap al ship cada N segons (Square)
RECTILINEAR_PROXIMITY, // Rectilini + boost rotació visual prop del ship (Pinwheel)
WANDER, // Canvi de direcció probabilístic suau, sense target
CHASE, // Steering continu cap al ship més proper
FLEE, // Steering continu allunyant-se del ship més proper
};
// Accions que s'executen periòdicament (un timer per acció). Futur (Fase C):
// SHOOT amb aim_mode/jitter/bullet config.
enum class AiActionType : uint8_t {
SHOOT,
};
enum class AimMode : uint8_t {
RANDOM, // Angle uniformement aleatori
AIMED, // atan2(nearest_ship - center) + soroll gaussià (jitter_rad)
};
// Camps de tots els movements; només el subset rellevant per al type actiu
// s'usa. Els altres queden a 0.0F. Mateixa filosofia que la BehaviorCfg
// llegacy però amb el type explícit dins.
struct MovementConfig {
MovementType type{MovementType::ZIGZAG};
// ZIGZAG i WANDER (canvi de direcció probabilístic; comparteixen camps).
float angle_change_max{0.0F};
float zigzag_prob_per_second{0.0F};
// TRACKING
float tracking_strength{0.0F};
float tracking_interval{0.0F};
// RECTILINEAR_PROXIMITY
float rotation_proximity_multiplier{0.0F};
float proximity_distance{0.0F};
// CHASE / FLEE: força del steering per segon (LERP velocity ↔ direcció ideal).
// 1.0 = en ~1s la velocitat queda totalment realineada cap al target.
float chase_strength{0.0F};
float flee_strength{0.0F};
};
// Acció periòdica. interval = segons entre disparades; el dispatcher manté un
// timer per acció (paral·lel a aquesta llista) i dispara quan arriba a 0.
struct AiTickAction {
AiActionType type{AiActionType::SHOOT};
float interval{1.0F};
AimMode aim_mode{AimMode::RANDOM};
float jitter_rad{0.0F};
float bullet_speed{200.0F}; // px/s; la magnitud la decideix l'enemic, no el bullet config
std::string bullet_config_name; // bullet config a usar (lazy-load des de BulletRegistry)
};
struct EnemyAiConfig {
MovementConfig movement;
std::vector<AiTickAction> tick;
};
// Estat per-instància que la primitiva de moviment manté entre frames (timers
// d'interval, contadors de canvi de direcció...). Es viu dins de Enemy i el
// sistema d'IA hi escriu via getAiState().
struct EnemyAiState {
float direction_change_timer{0.0F}; // ZIGZAG
float tracking_timer{0.0F}; // TRACKING
float tracking_strength{0.0F}; // TRACKING (cau de cfg, mutable per dificultat)
};
+480
View File
@@ -0,0 +1,480 @@
// enemy_config.cpp - Implementació del parser de EnemyConfig
// © 2026 JailDesigner
#include "game/entities/enemy_config.hpp"
#include <cstdint>
#include <exception>
#include <iostream>
#include <string>
namespace {
auto parseColor(const fkyaml::node& node, SDL_Color& out) -> bool {
if (!node.is_sequence() || node.size() != 3) {
return false;
}
const auto R = node[0].get_value<uint32_t>();
const auto G = node[1].get_value<uint32_t>();
const auto B = node[2].get_value<uint32_t>();
out = SDL_Color{
.r = static_cast<uint8_t>(R),
.g = static_cast<uint8_t>(G),
.b = static_cast<uint8_t>(B),
.a = 255};
return true;
}
auto aiTypeFromString(const std::string& s) -> std::optional<EnemyType> {
if (s == "pentagon") { return EnemyType::PENTAGON; }
if (s == "square") { return EnemyType::SQUARE; }
if (s == "pinwheel") { return EnemyType::PINWHEEL; }
if (s == "star") { return EnemyType::STAR; }
if (s == "orb") { return EnemyType::ORB; }
return std::nullopt;
}
// Cada parseXxx valida + omple la sub-struct corresponent. Retornen false
// amb log si falta un camp requerit. Separar-los baixa la complexitat
// cognitiva del fromYaml() principal.
auto parseAiType(const fkyaml::node& node, EnemyType expected, const std::string& name, EnemyType& out) -> bool {
if (!node.contains("ai_type")) {
std::cerr << "[EnemyConfig] Error: falta 'ai_type' a " << name << '\n';
return false;
}
const auto AI_STR = node["ai_type"].get_value<std::string>();
const auto PARSED = aiTypeFromString(AI_STR);
if (!PARSED) {
std::cerr << "[EnemyConfig] Error: ai_type desconegut '" << AI_STR << "' a " << name << '\n';
return false;
}
if (*PARSED != expected) {
std::cerr << "[EnemyConfig] Error: ai_type '" << AI_STR
<< "' no coincideix amb el tipus esperat (per directori) a " << name << '\n';
return false;
}
out = *PARSED;
return true;
}
auto parseShape(const fkyaml::node& node, const std::string& name, EnemyConfig::ShapeCfg& out) -> bool {
if (!node.contains("shape") || !node["shape"].contains("path")) {
std::cerr << "[EnemyConfig] Error: falta 'shape.path' a " << name << '\n';
return false;
}
const auto& shape = node["shape"];
out.path = shape["path"].get_value<std::string>();
out.scale = shape.contains("scale") ? shape["scale"].get_value<float>() : 1.0F;
out.collision_factor = shape.contains("collision_factor")
? shape["collision_factor"].get_value<float>()
: 1.0F;
return true;
}
auto parsePhysics(const fkyaml::node& node, const std::string& name, EnemyConfig::PhysicsCfg& out) -> bool {
if (!node.contains("physics")) {
std::cerr << "[EnemyConfig] Error: falta 'physics' a " << name << '\n';
return false;
}
const auto& p = node["physics"];
out.mass = p["mass"].get_value<float>();
out.speed = p["speed"].get_value<float>();
out.rotation_delta_min = p["rotation_delta_min"].get_value<float>();
out.rotation_delta_max = p["rotation_delta_max"].get_value<float>();
out.restitution = p["restitution"].get_value<float>();
out.linear_damping = p["linear_damping"].get_value<float>();
out.angular_damping = p["angular_damping"].get_value<float>();
return true;
}
auto parseAnimation(const fkyaml::node& node, const std::string& name, EnemyConfig::AnimationCfg& out) -> bool {
if (!node.contains("animation") ||
!node["animation"].contains("pulse") ||
!node["animation"].contains("rotation_accel")) {
std::cerr << "[EnemyConfig] Error: falta 'animation.pulse' o 'animation.rotation_accel' a " << name << '\n';
return false;
}
const auto& p = node["animation"]["pulse"];
out.pulse.trigger_prob_per_second = p["trigger_prob_per_second"].get_value<float>();
out.pulse.duration_min = p["duration_min"].get_value<float>();
out.pulse.duration_max = p["duration_max"].get_value<float>();
out.pulse.amplitude_min = p["amplitude_min"].get_value<float>();
out.pulse.amplitude_max = p["amplitude_max"].get_value<float>();
out.pulse.frequency_min = p["frequency_min"].get_value<float>();
out.pulse.frequency_max = p["frequency_max"].get_value<float>();
const auto& r = node["animation"]["rotation_accel"];
out.rotation_accel.trigger_prob_per_second = r["trigger_prob_per_second"].get_value<float>();
out.rotation_accel.duration_min = r["duration_min"].get_value<float>();
out.rotation_accel.duration_max = r["duration_max"].get_value<float>();
out.rotation_accel.multiplier_min = r["multiplier_min"].get_value<float>();
out.rotation_accel.multiplier_max = r["multiplier_max"].get_value<float>();
return true;
}
auto parseWounded(const fkyaml::node& node, const std::string& name, EnemyConfig::WoundedCfg& out) -> bool {
if (!node.contains("wounded")) {
std::cerr << "[EnemyConfig] Error: falta 'wounded' a " << name << '\n';
return false;
}
const auto& w = node["wounded"];
out.duration = w["duration"].get_value<float>();
out.blink_hz = w["blink_hz"].get_value<float>();
return true;
}
auto parseSpawn(const fkyaml::node& node, const std::string& name, EnemyConfig::SpawnCfg& out) -> bool {
if (!node.contains("spawn")) {
std::cerr << "[EnemyConfig] Error: falta 'spawn' a " << name << '\n';
return false;
}
const auto& s = node["spawn"];
out.invulnerability_duration = s["invulnerability_duration"].get_value<float>();
out.invulnerability_brightness_start = s["invulnerability_brightness_start"].get_value<float>();
out.invulnerability_brightness_end = s["invulnerability_brightness_end"].get_value<float>();
out.invulnerability_scale_start = s["invulnerability_scale_start"].get_value<float>();
out.invulnerability_scale_end = s["invulnerability_scale_end"].get_value<float>();
out.safety_distance = s["safety_distance"].get_value<float>();
return true;
}
// Tots els camps de behavior són opcionals; només l'AI corresponent els consumeix.
void parseBehavior(const fkyaml::node& node, EnemyConfig::BehaviorCfg& out) {
if (!node.contains("behavior")) {
return;
}
const auto& b = node["behavior"];
const auto READ_OPT = [&b](const char* key, float& dst) {
if (b.contains(key)) {
dst = b[key].get_value<float>();
}
};
READ_OPT("zigzag_prob_per_second", out.zigzag_prob_per_second);
READ_OPT("angle_change_max", out.angle_change_max);
READ_OPT("tracking_strength", out.tracking_strength);
READ_OPT("tracking_interval", out.tracking_interval);
READ_OPT("rotation_proximity_multiplier", out.rotation_proximity_multiplier);
READ_OPT("proximity_distance", out.proximity_distance);
}
auto parseColors(const fkyaml::node& node, const std::string& name, EnemyConfig::ColorsCfg& out) -> bool {
if (!node.contains("colors") ||
!parseColor(node["colors"]["normal"], out.normal) ||
!parseColor(node["colors"]["wounded"], out.wounded)) {
std::cerr << "[EnemyConfig] Error: 'colors.normal' / 'colors.wounded' no són [r,g,b] a "
<< name << '\n';
return false;
}
return true;
}
auto parseScore(const fkyaml::node& node, const std::string& name, int& out) -> bool {
if (!node.contains("score")) {
std::cerr << "[EnemyConfig] Error: falta 'score' a " << name << '\n';
return false;
}
out = node["score"].get_value<int>();
return true;
}
// health és opcional: si el YAML no l'inclou, el default {1} de l'struct
// ja cobreix el comportament de tots els enemics actuals (1 hit → mort).
void parseHealth(const fkyaml::node& node, int& out) {
if (node.contains("health")) {
out = node["health"].get_value<int>();
}
}
// Forward-decl: aimModeFromString viu més avall (junt amb la resta de
// helpers d'AI) però parseActionList el necessita per al payload de
// FIRE_BULLET. Evita reordenar tot el bloc.
auto aimModeFromString(const std::string& s) -> std::optional<AimMode>;
auto actionTypeFromString(const std::string& s) -> std::optional<EnemyActionType> {
if (s == "set_hurt") { return EnemyActionType::SET_HURT; }
if (s == "destroy") { return EnemyActionType::DESTROY; }
if (s == "add_score") { return EnemyActionType::ADD_SCORE; }
if (s == "create_debris") { return EnemyActionType::CREATE_DEBRIS; }
if (s == "create_debris_partial") { return EnemyActionType::CREATE_DEBRIS_PARTIAL; }
if (s == "create_fireworks") { return EnemyActionType::CREATE_FIREWORKS; }
if (s == "create_fireworks_small") { return EnemyActionType::CREATE_FIREWORKS_SMALL; }
if (s == "apply_impulse") { return EnemyActionType::APPLY_IMPULSE; }
if (s == "decrease_health") { return EnemyActionType::DECREASE_HEALTH; }
if (s == "flash") { return EnemyActionType::FLASH; }
if (s == "fire_bullet") { return EnemyActionType::FIRE_BULLET; }
return std::nullopt;
}
auto parseActionList(const fkyaml::node& list_node, const std::string& enemy_name, const char* event_name, std::vector<EnemyAction>& out) -> bool {
if (!list_node.is_sequence()) {
std::cerr << "[EnemyConfig] Error: '" << event_name << "' ha de ser una llista a "
<< enemy_name << '\n';
return false;
}
for (const auto& item : list_node) {
if (!item.contains("action")) {
std::cerr << "[EnemyConfig] Error: entrada sense 'action' a " << event_name
<< " (" << enemy_name << ")\n";
return false;
}
const auto STR = item["action"].get_value<std::string>();
const auto PARSED = actionTypeFromString(STR);
if (!PARSED) {
std::cerr << "[EnemyConfig] Error: acció desconeguda '" << STR << "' a "
<< event_name << " (" << enemy_name << ")\n";
return false;
}
EnemyAction action;
action.type = *PARSED;
// Payload de FIRE_BULLET. Camps opcionals; els defaults són els del struct.
if (action.type == EnemyActionType::FIRE_BULLET) {
if (item.contains("bullet")) {
action.bullet_config_name = item["bullet"].get_value<std::string>();
}
if (item.contains("bullet_speed")) {
action.bullet_speed = item["bullet_speed"].get_value<float>();
}
if (item.contains("aim_mode")) {
const auto AIM_STR = item["aim_mode"].get_value<std::string>();
const auto AIM = aimModeFromString(AIM_STR);
if (!AIM) {
std::cerr << "[EnemyConfig] Error: aim_mode desconegut '" << AIM_STR
<< "' a " << event_name << " (" << enemy_name << ")\n";
return false;
}
action.aim_mode = *AIM;
}
if (item.contains("jitter_rad")) {
action.jitter_rad = item["jitter_rad"].get_value<float>();
}
}
out.push_back(action);
}
return true;
}
// Defaults: replica el flux hardcoded actual (set_hurt → destroy → score+debris+fireworks).
// Construïm via mutació per esquivar warnings de designated-init parcial sobre
// l'EnemyAction (que té payload de FIRE_BULLET no rellevant ací).
void fillLegacyDefaults(EnemyEventConfig& events) {
const auto MAKE = [](EnemyActionType type) {
EnemyAction a;
a.type = type;
return a;
};
events.on_hit = {MAKE(EnemyActionType::SET_HURT)};
events.on_hurt_end = {MAKE(EnemyActionType::DESTROY)};
events.on_destroy = {
MAKE(EnemyActionType::ADD_SCORE),
MAKE(EnemyActionType::CREATE_DEBRIS),
MAKE(EnemyActionType::CREATE_FIREWORKS),
};
}
auto movementTypeFromString(const std::string& s) -> std::optional<MovementType> {
if (s == "zigzag") { return MovementType::ZIGZAG; }
if (s == "tracking") { return MovementType::TRACKING; }
if (s == "rectilinear_proximity") { return MovementType::RECTILINEAR_PROXIMITY; }
if (s == "wander") { return MovementType::WANDER; }
if (s == "chase") { return MovementType::CHASE; }
if (s == "flee") { return MovementType::FLEE; }
return std::nullopt;
}
auto aiActionTypeFromString(const std::string& s) -> std::optional<AiActionType> {
if (s == "shoot") { return AiActionType::SHOOT; }
return std::nullopt;
}
auto aimModeFromString(const std::string& s) -> std::optional<AimMode> {
if (s == "random") { return AimMode::RANDOM; }
if (s == "aimed") { return AimMode::AIMED; }
return std::nullopt;
}
auto parseMovement(const fkyaml::node& mv_node, const std::string& enemy_name, MovementConfig& out) -> bool {
if (!mv_node.contains("type")) {
std::cerr << "[EnemyConfig] Error: falta 'ai.movement.type' a " << enemy_name << '\n';
return false;
}
const auto TYPE_STR = mv_node["type"].get_value<std::string>();
const auto PARSED = movementTypeFromString(TYPE_STR);
if (!PARSED) {
std::cerr << "[EnemyConfig] Error: movement type desconegut '" << TYPE_STR
<< "' a " << enemy_name << '\n';
return false;
}
out.type = *PARSED;
const auto READ_OPT = [&mv_node](const char* key, float& dst) {
if (mv_node.contains(key)) {
dst = mv_node[key].get_value<float>();
}
};
READ_OPT("angle_change_max", out.angle_change_max);
READ_OPT("zigzag_prob_per_second", out.zigzag_prob_per_second);
READ_OPT("tracking_strength", out.tracking_strength);
READ_OPT("tracking_interval", out.tracking_interval);
READ_OPT("rotation_proximity_multiplier", out.rotation_proximity_multiplier);
READ_OPT("proximity_distance", out.proximity_distance);
READ_OPT("chase_strength", out.chase_strength);
READ_OPT("flee_strength", out.flee_strength);
return true;
}
auto parseTickList(const fkyaml::node& list_node, const std::string& enemy_name, std::vector<AiTickAction>& out) -> bool {
if (!list_node.is_sequence()) {
std::cerr << "[EnemyConfig] Error: 'ai.tick' ha de ser una llista a " << enemy_name << '\n';
return false;
}
for (const auto& item : list_node) {
if (!item.contains("action")) {
std::cerr << "[EnemyConfig] Error: entrada sense 'action' a ai.tick ("
<< enemy_name << ")\n";
return false;
}
const auto STR = item["action"].get_value<std::string>();
const auto PARSED = aiActionTypeFromString(STR);
if (!PARSED) {
std::cerr << "[EnemyConfig] Error: acció d'ai desconeguda '" << STR
<< "' a " << enemy_name << '\n';
return false;
}
AiTickAction action;
action.type = *PARSED;
if (item.contains("interval")) {
action.interval = item["interval"].get_value<float>();
}
if (item.contains("aim_mode")) {
const auto AIM_STR = item["aim_mode"].get_value<std::string>();
const auto AIM = aimModeFromString(AIM_STR);
if (!AIM) {
std::cerr << "[EnemyConfig] Error: aim_mode desconegut '" << AIM_STR
<< "' a ai.tick (" << enemy_name << ")\n";
return false;
}
action.aim_mode = *AIM;
}
if (item.contains("jitter_rad")) {
action.jitter_rad = item["jitter_rad"].get_value<float>();
}
if (item.contains("bullet_speed")) {
action.bullet_speed = item["bullet_speed"].get_value<float>();
}
if (item.contains("bullet")) {
action.bullet_config_name = item["bullet"].get_value<std::string>();
}
out.push_back(action);
}
return true;
}
// Migració progressiva: si el YAML no porta secció `ai:`, derivem el
// movement a partir de l'ai_type i copiem els paràmetres de la BehaviorCfg
// ja parsejada. Comportament idèntic al hardcoded actual.
void fillLegacyAiDefaults(EnemyType ai_type, const EnemyConfig::BehaviorCfg& legacy, EnemyAiConfig& out) {
switch (ai_type) {
case EnemyType::PENTAGON:
case EnemyType::STAR:
out.movement.type = MovementType::ZIGZAG;
out.movement.angle_change_max = legacy.angle_change_max;
out.movement.zigzag_prob_per_second = legacy.zigzag_prob_per_second;
break;
case EnemyType::SQUARE:
out.movement.type = MovementType::TRACKING;
out.movement.tracking_strength = legacy.tracking_strength;
out.movement.tracking_interval = legacy.tracking_interval;
break;
case EnemyType::PINWHEEL:
out.movement.type = MovementType::RECTILINEAR_PROXIMITY;
out.movement.rotation_proximity_multiplier = legacy.rotation_proximity_multiplier;
out.movement.proximity_distance = legacy.proximity_distance;
break;
case EnemyType::ORB:
// Sense legacy fallback: el YAML de l'orb ha de definir
// ai.movement explícitament. Default chase lent perquè el switch
// siga exhaustiu i no falli si algú omet el bloc ai.
out.movement.type = MovementType::CHASE;
out.movement.chase_strength = 0.3F;
break;
}
}
auto parseAi(const fkyaml::node& node, const std::string& name, EnemyType ai_type, const EnemyConfig::BehaviorCfg& legacy, EnemyAiConfig& out) -> bool {
if (!node.contains("ai")) {
fillLegacyAiDefaults(ai_type, legacy, out);
return true;
}
const auto& ai = node["ai"];
if (!ai.contains("movement")) {
std::cerr << "[EnemyConfig] Error: falta 'ai.movement' a " << name << '\n';
return false;
}
if (!parseMovement(ai["movement"], name, out.movement)) {
return false;
}
if (ai.contains("tick") && !parseTickList(ai["tick"], name, out.tick)) {
return false;
}
return true;
}
auto parseEvents(const fkyaml::node& node, const std::string& name, EnemyEventConfig& out) -> bool {
if (!node.contains("events")) {
fillLegacyDefaults(out);
return true;
}
const auto& e = node["events"];
if (e.contains("on_hit") && !parseActionList(e["on_hit"], name, "on_hit", out.on_hit)) {
return false;
}
if (e.contains("on_no_health") &&
!parseActionList(e["on_no_health"], name, "on_no_health", out.on_no_health)) {
return false;
}
if (e.contains("on_hurt_end") &&
!parseActionList(e["on_hurt_end"], name, "on_hurt_end", out.on_hurt_end)) {
return false;
}
if (e.contains("on_destroy") &&
!parseActionList(e["on_destroy"], name, "on_destroy", out.on_destroy)) {
return false;
}
// Validació: destroy no pot aparèixer dins on_destroy (recursió infinita).
for (const auto& a : out.on_destroy) {
if (a.type == EnemyActionType::DESTROY) {
std::cerr << "[EnemyConfig] Error: 'destroy' no pot aparèixer dins 'on_destroy' a "
<< name << " (recursió infinita)\n";
return false;
}
}
return true;
}
} // namespace
auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
-> std::optional<EnemyConfig> {
try {
EnemyConfig cfg;
cfg.name = node.contains("name") ? node["name"].get_value<std::string>() : "enemy";
if (!parseAiType(node, expected_ai_type, cfg.name, cfg.ai_type)) { return std::nullopt; }
if (!parseShape(node, cfg.name, cfg.shape)) { return std::nullopt; }
if (!parsePhysics(node, cfg.name, cfg.physics)) { return std::nullopt; }
parseBehavior(node, cfg.behavior);
if (!parseAnimation(node, cfg.name, cfg.animation)) { return std::nullopt; }
if (!parseWounded(node, cfg.name, cfg.wounded)) { return std::nullopt; }
if (!parseSpawn(node, cfg.name, cfg.spawn)) { return std::nullopt; }
if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; }
if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; }
parseHealth(node, cfg.health);
if (!parseEvents(node, cfg.name, cfg.events)) { return std::nullopt; }
if (!parseAi(node, cfg.name, cfg.ai_type, cfg.behavior, cfg.ai)) { return std::nullopt; }
return cfg;
} catch (const std::exception& e) {
std::cerr << "[EnemyConfig] Excepció parsejant: " << e.what() << '\n';
return std::nullopt;
}
}
+114
View File
@@ -0,0 +1,114 @@
// enemy_config.hpp - Configuració d'un tipus d'enemic carregada des de YAML
// © 2026 JailDesigner
//
// Una instància per tipus (Pentagon/Square/Pinwheel), carregada un cop al
// startup per EnemyRegistry i compartida entre totes les instàncies d'aquell
// tipus. Estructura paral·lela a PlayerConfig.
#pragma once
#include <SDL3/SDL.h>
#include <optional>
#include <string>
#include "external/fkyaml_node.hpp"
#include "game/entities/enemy.hpp" // EnemyType
#include "game/entities/enemy_ai.hpp"
#include "game/entities/enemy_event.hpp"
struct EnemyConfig {
struct ShapeCfg {
std::string path;
float scale; // multiplicador visual + hitbox sobre la mida nativa del .shp
float collision_factor; // ajust opcional del hitbox respecte el cercle circumscrit (default 1.0)
};
struct PhysicsCfg {
float mass;
float speed;
float rotation_delta_min;
float rotation_delta_max;
float restitution; // rebot contra parets (1.0 = perfectament elàstic)
float linear_damping; // fricció lineal (s^-1)
float angular_damping;
};
// Camps específics de cada AI. Els no aplicables a un tipus queden a 0.0F
// i no s'usen — el dispatch viu a Enemy::behaviorXxx.
struct BehaviorCfg {
// Pentagon
float zigzag_prob_per_second{0.0F};
float angle_change_max{0.0F};
// Square
float tracking_strength{0.0F};
float tracking_interval{0.0F};
// Pinwheel
float rotation_proximity_multiplier{0.0F};
float proximity_distance{0.0F};
};
// Animacions decoratives. Compartides estructuralment entre tots els tipus
// però amb valors propis per personalitzar la "personalitat" visual de cada un.
struct AnimationCfg {
struct PulseCfg {
float trigger_prob_per_second; // probabilitat per segon d'iniciar un pulse
float duration_min;
float duration_max;
float amplitude_min; // amplitud d'escala (±)
float amplitude_max;
float frequency_min; // Hz
float frequency_max;
};
struct RotationAccelCfg {
float trigger_prob_per_second;
float duration_min; // segons de transició al nou speed
float duration_max;
float multiplier_min; // multiplicador sobre rotation_delta_base
float multiplier_max;
};
PulseCfg pulse;
RotationAccelCfg rotation_accel;
};
struct WoundedCfg {
float duration; // segons en estat ferit abans d'explotar
float blink_hz; // freqüència del parpelleig color normal ↔ wounded
};
struct SpawnCfg {
float invulnerability_duration; // segons d'invulnerabilitat post-spawn
float invulnerability_brightness_start; // brightness inicial (corba LERP)
float invulnerability_brightness_end; // brightness final
float invulnerability_scale_start; // escala inicial (corba LERP, 0 = invisible)
float invulnerability_scale_end; // escala final (1 = mida nativa)
float safety_distance; // px mínim respecte al player al spawn
};
struct ColorsCfg {
SDL_Color normal;
SDL_Color wounded;
};
std::string name;
EnemyType ai_type;
ShapeCfg shape;
PhysicsCfg physics;
BehaviorCfg behavior;
AnimationCfg animation;
WoundedCfg wounded;
SpawnCfg spawn;
ColorsCfg colors;
int score;
// Salut inicial: per defecte 1 (un balazo → on_no_health). Els YAMLs poden
// pujar-lo (p.ex. 10 per a un enemic tough). El sistema d'events és qui
// decideix què passa quan la salut arriba a 0 via on_no_health.
int health{1};
EnemyEventConfig events;
EnemyAiConfig ai;
// Parseja un descriptor d'enemic. expected_ai_type valida que ai_type del
// YAML coincideix amb el tipus que el caller espera (segons el directori).
static auto fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
-> std::optional<EnemyConfig>;
};
+67
View File
@@ -0,0 +1,67 @@
// enemy_event.hpp - Sistema declaratiu d'events i accions per a enemics
// © 2026 JailDesigner
//
// Cada enemic descriu al seu YAML què passa quan rep un event (on_hit,
// on_hurt_end, on_destroy) com a llista d'accions. El motor només dispatcha;
// el comportament viu a les dades.
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "game/entities/enemy_ai.hpp" // AimMode
enum class EnemyEventType : uint8_t {
ON_HIT, // Impactat per una bala
ON_NO_HEALTH, // health ha arribat a 0 o menys aquest frame (via DECREASE_HEALTH)
ON_HURT_END, // Timer wounded ha expirat aquest frame
ON_DESTROY, // L'acció destroy s'està executant (efectes col·laterals)
};
enum class EnemyActionType : uint8_t {
SET_HURT, // Entra estat wounded (o destrueix si ja era wounded)
DESTROY, // Dispara on_destroy + desactiva físicament
ADD_SCORE, // Suma config.score al shooter + floating score
CREATE_DEBRIS, // Explosió de debris amb herència de velocitat
CREATE_DEBRIS_PARTIAL, // Debris de xip parcial (trossos a escala 0.3, per hits HP>1)
CREATE_FIREWORKS, // Burst radial de firework
CREATE_FIREWORKS_SMALL, // Burst petit (pocs punts, poca velocitat) — feedback per hit
APPLY_IMPULSE, // Aplica l'impuls de la bala impactant
DECREASE_HEALTH, // Decrementa health_; si <=0, dispatcha ON_NO_HEALTH
FLASH, // Flash visual breu (feedback per impacte parcial)
FIRE_BULLET, // Dispara una bala (config per nom) dirigida o aleatòria
};
struct EnemyAction {
EnemyActionType type;
// Payload de FIRE_BULLET (ignorat per altres tipus). Paral·lel a AiTickAction
// perquè un futur refactor pugui compartir doShoot/doFireBullet si val la pena.
std::string bullet_config_name;
float bullet_speed{200.0F};
AimMode aim_mode{AimMode::RANDOM};
float jitter_rad{0.0F};
};
struct EnemyEventConfig {
std::vector<EnemyAction> on_hit;
std::vector<EnemyAction> on_no_health;
std::vector<EnemyAction> on_hurt_end;
std::vector<EnemyAction> on_destroy;
[[nodiscard]] auto getActions(EnemyEventType event) const -> const std::vector<EnemyAction>& {
switch (event) {
case EnemyEventType::ON_HIT:
return on_hit;
case EnemyEventType::ON_NO_HEALTH:
return on_no_health;
case EnemyEventType::ON_HURT_END:
return on_hurt_end;
case EnemyEventType::ON_DESTROY:
return on_destroy;
}
return on_hit; // unreachable
}
};
+70
View File
@@ -0,0 +1,70 @@
// enemy_registry.cpp - Implementació del registre estàtic d'enemics
// © 2026 JailDesigner
#include "game/entities/enemy_registry.hpp"
#include <cstdlib>
#include <iostream>
#include <string>
#include "core/entities/entity_loader.hpp"
EnemyConfig EnemyRegistry::pentagon_config;
EnemyConfig EnemyRegistry::square_config;
EnemyConfig EnemyRegistry::pinwheel_config;
EnemyConfig EnemyRegistry::star_config;
EnemyConfig EnemyRegistry::orb_config;
bool EnemyRegistry::loaded = false;
namespace {
auto loadOne(const std::string& name, EnemyType expected_type, EnemyConfig& out) -> bool {
auto yaml = Entities::EntityLoader::load(name);
if (!yaml) {
std::cerr << "[EnemyRegistry] Error: no s'ha pogut carregar " << name << ".yaml\n";
return false;
}
auto cfg = EnemyConfig::fromYaml(*yaml, expected_type);
if (!cfg) {
std::cerr << "[EnemyRegistry] Error: format invàlid a " << name << ".yaml\n";
return false;
}
out = *cfg;
return true;
}
} // namespace
auto EnemyRegistry::loadAll() -> bool {
const bool OK = loadOne("pentagon", EnemyType::PENTAGON, pentagon_config) &&
loadOne("square", EnemyType::SQUARE, square_config) &&
loadOne("pinwheel", EnemyType::PINWHEEL, pinwheel_config) &&
loadOne("star", EnemyType::STAR, star_config) &&
loadOne("orb", EnemyType::ORB, orb_config);
loaded = OK;
if (OK) {
std::cout << "[EnemyRegistry] 5 configuracions d'enemic carregades.\n";
}
return OK;
}
auto EnemyRegistry::get(EnemyType type) -> const EnemyConfig& {
if (!loaded) {
std::cerr << "[EnemyRegistry] FATAL: get() abans de loadAll()\n";
std::exit(EXIT_FAILURE);
}
switch (type) {
case EnemyType::PENTAGON:
return pentagon_config;
case EnemyType::SQUARE:
return square_config;
case EnemyType::PINWHEEL:
return pinwheel_config;
case EnemyType::STAR:
return star_config;
case EnemyType::ORB:
return orb_config;
}
std::cerr << "[EnemyRegistry] FATAL: tipus desconegut\n";
std::exit(EXIT_FAILURE);
}
+32
View File
@@ -0,0 +1,32 @@
// enemy_registry.hpp - Registre estàtic de configuracions d'enemics per tipus
// © 2026 JailDesigner
//
// Carrega els 3 fitxers YAML (pentagon, square, pinwheel) un cop al startup
// i exposa el lookup per EnemyType. Pensat per a ser invocat des de
// GameScene; si la càrrega falla, el caller decideix avortar.
#pragma once
#include "game/entities/enemy.hpp"
#include "game/entities/enemy_config.hpp"
class EnemyRegistry {
public:
EnemyRegistry() = delete; // tot estàtic
// Carrega els 3 descriptors. Retorna true si tots tres s'han carregat
// i parsejat correctament. Cridar abans del primer get().
static auto loadAll() -> bool;
// Lookup. Cal haver cridat loadAll() abans. Si el tipus no s'ha carregat
// (loadAll fallida o no cridada), avorta amb log fatal.
static auto get(EnemyType type) -> const EnemyConfig&;
private:
static EnemyConfig pentagon_config;
static EnemyConfig square_config;
static EnemyConfig pinwheel_config;
static EnemyConfig star_config;
static EnemyConfig orb_config;
static bool loaded;
};

Some files were not shown because too many files have changed in this diff Show More