# 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 Las fuentes GLSL viven en [shaders/](shaders/) (`line.vert/frag`, `bloom.frag`, `postfx.vert/frag`). Se compilan a SPIR-V (Linux/Windows) y MSL (macOS) y se **embeben** como cabeceras en el binario: ver [gpu/spv/](source/core/rendering/gpu/spv/) y [gpu/msl/](source/core/rendering/gpu/msl/). La selección por plataforma la hace el `GpuDevice`. No se cargan shaders de disco en runtime. > No verificado en detalle: los pormenores del pipeline de compilación de shaders > (qué target de CMake genera los `.spv.h`/`.msl.h`). Los headers embebidos existen > y se referencian desde las pipelines; el mecanismo exacto de generación habría que > confirmarlo en [CMakeLists.txt](CMakeLists.txt) y [tools/shaders/](tools/shaders/). --- ## 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. --- ## 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 - Las secciones de **render** ([§5](#5-renderizado-de-la-lógica-al-píxel)), **bucle** ([§3](#3-bucle-principal)), **escenas** ([§4](#4-sistema-de-escenas)), **eventos globales** ([§9](#9-comunicación-entre-módulos)), **GameScene/IA demo** ([§10](#10-lógica-del-juego)–[§11](#11-ia-del-modo-demo-attract)) se verificaron leyendo directamente los ficheros y firmas citados. - El **pipeline de compilación de shaders** (cómo se generan los `.spv.h`/`.msl.h`) no se ha trazado al detalle; los headers embebidos existen y se usan, pero el paso de build exacto queda por confirmar en [CMakeLists.txt](CMakeLists.txt) / [tools/shaders/](tools/shaders/). - El detalle interno de la **PhysicsWorld** (algoritmo de resolución de colisiones físicas) se ha descrito a alto nivel; para el detalle, ver [physics_world.cpp](source/core/physics/physics_world.cpp) y [collision.hpp](source/core/physics/collision.hpp).