diff --git a/DOCS/ARQUITECTURA.md b/DOCS/ARQUITECTURA.md new file mode 100644 index 0000000..30acb61 --- /dev/null +++ b/DOCS/ARQUITECTURA.md @@ -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
SDL_MAIN_USE_CALLBACKS"] + end + MAIN -->|posee| DIR["Director
(es el programa)"] + + subgraph core["source/core (motor)"] + SDLM["SDLManager
ventana + GPU"] + GE["GlobalEvents
F1-F7/F12/ESC/hotplug"] + INPUT["Input (singleton)"] + AUDIO["Audio (singleton)"] + RES["Resource::Loader / Pack"] + LOC["Locale (i18n)"] + OVL["Notifier · ServiceMenu
DebugOverlay · DefineInputs"] + end + + subgraph game["source/game (juego)"] + SCN["Scenes
Logo · Title · Game"] + ENT["Entities
Ship · Enemy · Bullet"] + SYS["Systems
Collision · EnemyAi · DemoPilot"] + STG["StageManager / WaveRunner"] + FX["Effects
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(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/` 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`. +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()
(acumula en CPU)"] --> B["Rendering::linea / renderShape"] + B --> C["GpuFrameRenderer::pushLine()
extruye quad → vertices_ / indices_"] + C -.repetido N veces.-> C + A --> D["SDLManager::present()
= GpuFrameRenderer::endFrame()"] + D --> E["flushBatch()
sube VBO/IBO, dibuja sobre OFFSCREEN"] + E --> F["bloomPass()
H: high-pass+blur → bloom_a
V: blur → bloom_b"] + F --> G["compositePass()
offscreen + bloom_b + flicker + fondo
→ 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=` 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//.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`, + `std::array` (15), `std::array` (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//` + [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. + +