# Arquitectura de **AEE Arcade** > Guía de orientación para un desarrollador nuevo en el proyecto. > > Cada afirmación de este documento está anclada a código real: se cita el > fichero (y, cuando ayuda, la función o el número de línea) que la respalda. > Donde no he encontrado algo, lo digo explícitamente en lugar de inventarlo. > > El proyecto es *"Aventures en Egipte ARCADE"* (AEE), un juego 2D escrito en > C++ sobre **SDL3**. Los comentarios del código están en valenciano/catalán; > este documento está en castellano por petición. --- ## Índice 1. [Visión general](#1-visión-general) 2. [Punto de entrada y bucle principal](#2-punto-de-entrada-y-bucle-principal) 3. [Gestión de escenas](#3-gestión-de-escenas) 4. [Renderizado: de la lógica al píxel](#4-renderizado-de-la-lógica-al-píxel) 5. [Entrada](#5-entrada) 6. [Comunicación entre módulos](#6-comunicación-entre-módulos) 7. [Lógica del juego](#7-lógica-del-juego) 8. [Entidades](#8-entidades) 9. [Mapa y niveles](#9-mapa-y-niveles) 10. [Recursos](#10-recursos) 11. [Configuración y constantes](#11-configuración-y-constantes) 12. [Modo demo / IA](#12-modo-demo--ia) 13. [Convenciones y patrones recurrentes](#13-convenciones-y-patrones-recurrentes) 14. [Guía de navegación: "si quieres tocar X, mira Y"](#14-guía-de-navegación-si-quieres-tocar-x-mira-y) --- ## 1. Visión general AEE Arcade está organizado en **dos capas claramente separadas** dentro de `source/`: - **`source/core/`** — el motor genérico, sin conocimiento del juego concreto: sistema/arranque, rendering, audio, input, recursos, escena base, locale y UI base. - **`source/game/`** — el juego concreto: escenas (LOGO, TITLE, INTRO, GAME), entidades (momias, sarcófagos, puertas…), mapa, progresión de niveles, editor (solo Debug) y HUD. La separación no es solo de carpetas: el código de `core/` nunca incluye cabeceras de `game/`. La conexión va siempre de `game/` hacia `core/`, salvo algún *hook* genérico (p.ej. el overlay del menú, ver §6). Filosofía dominante: **data-driven**. Casi todo el comportamiento (geometría de salas, plantillas de entidades, timings de transición, controles, paletas, shaders, idioma…) se carga desde ficheros **YAML** en `data/`, con valores de *fallback* compilados en `source/utils/defaults.hpp`. El parser es [fkYAML](https://github.com/fktn-k/fkYAML), vendorizado en `source/external/fkyaml_node.hpp`. ```mermaid graph TD SDL[SDL3 callbacks · main.cpp] --> DIR[Director · core/system] DIR -->|update / render| SC[Scene activa] DIR --> EVT[EventBuffer] EVT --> MENU[ServiceMenu] & GI["GlobalInputs (F1–F12)"] & GP[GamepadManager] SC --> GAME[GameScene · game/scenes] GAME --> EM[EntityManager] --> BUS[EventBus] GAME --> TM[Tilemap] & MAP[Map] & LM[LevelManager] GAME --> PL[Players] -->|polling teclado/mando| INPUT[core/input] EM --> ENT["Entity + concrete/*"] GAME -->|blit ARGB| SURF[Surface] SURF --> CANVAS["Canvas 320×240 ARGB (CPU)"] CANVAS --> SCREEN[Screen] --> BACKEND["SDL3 GPU + shaders / fallback SDL_Renderer"] RES[Resource::Cache] -.-> SURF & AUDIO[Audio] ``` Tamaño aproximado: ~188 ficheros C++ y ~30.000 líneas en `source/` (excluyendo `external/`). **Dos ideas-fuerza que conviene interiorizar desde el principio**, porque condicionan todo lo demás: 1. **El render es 100% por CPU sobre un único canvas ARGB de 320×240.** La GPU solo escala ese canvas a la ventana y aplica post-procesado (CRT). Ver §4. 2. **La paleta es por-`Surface`, pero el canvas final es ARGB de 24 bits.** No es un juego de paleta global limitada: cada GIF lleva su propia paleta y se compone sobre un framebuffer de color completo (`source/core/rendering/surface.hpp:13`). --- ## 2. Punto de entrada y bucle principal ### 2.1. No hay `while` propio: SDL conduce el bucle `source/main.cpp` define `SDL_MAIN_USE_CALLBACKS` (línea 1). Eso significa que **no existe un game loop escrito a mano**: es SDL quien llama a cuatro funciones de *callback*: | Callback | Qué hace en este proyecto | |---|---| | `SDL_AppInit` | Guarda `argv` (para reinicio en caliente), inicia el logger y **crea el `Director`** (`main.cpp:13`). | | `SDL_AppEvent` | Empuja cada `SDL_Event` a `EventBuffer::events` (`main.cpp:31`). | | `SDL_AppIterate` | Ejecuta **un frame**: `director->iterate()` (`main.cpp:37`). | | `SDL_AppQuit` | Destruye el `Director` y, si se pidió, hace `execv` para reiniciar (`main.cpp:42`). | El `Director` se entrega a SDL como puntero crudo (`appstate`) y se reabsorbe en `SDL_AppQuit` con un `unique_ptr` para limpieza RAII ordenada. ### 2.2. El `Director`: orquestador del frame `source/core/system/director.hpp` / `.cpp` es el corazón del sistema. **Constructor (`director.cpp:47`)** — inicializa *todo* en un orden cuidadoso y documentado en comentarios. Resumen del orden: 1. `SDL_Init(0)` (sin subsistemas todavía). 2. Sistema de recursos: localiza `resources.pack` junto al binario; en `_DEBUG` activa *fallback* a disco apuntando al árbol fuente (`AEE_SOURCE_DIR`) para editar YAML/GIF sin re-empaquetar; en Release el pack es obligatorio (`director.cpp:52`). 3. `createSystemFolder()` — carpeta de configuración por plataforma (ver §11). 4. `GamepadManager::get().init()` **antes** de cargar opciones (las opciones re-vinculan mandos a jugadores; `director.cpp:86`). 5. Carga de `config.yaml`, `gamepad_configs.yaml`, `gameplay.yaml`, presets de shaders (`postfx.yaml`, `crtpi.yaml`). 6. `Screen::get().configure(...)` — crea ventana + backend GPU. 7. `Locale`, `KeyConfig`, `UiConfig`, `Lighting`, `EditorConfig` (Debug). 8. Pre-carga de recursos de UI: paleta, fuentes, sombra compartida, pistas de música, sonidos de menú. 9. `Audio::init(...)`. 10. Construcción de los *overlays* compartidos (`Notifier`, `RenderInfo`, `ServiceMenu`) y de las `Surface`/`Text` compartidas entre escenas. 11. Registro del *overlay hook* del menú en `Screen` (`director.cpp:291`). 12. Elección de la escena inicial (ver §3.3) y `last_ticks_ms_ = SDL_GetTicks()`. **Destructor (`director.cpp:336`)** — el orden de liberación es crítico y está fuertemente comentado: primero la escena, luego el audio del `Cache` (mientras SDL sigue vivo), luego `Audio`, `Screen` y por fin `SDL_Quit()`. Se hace explícitamente (no vía destructores estáticos `atexit`) para evitar crashes en `vkDestroyInstance` cuando el runtime descarga librerías antes de tiempo. ### 2.3. Gestión del tiempo y orden update/render El "frame" vive en `Director::iterate()` (`director.cpp:509`). **No hay timestep fijo**: se usa el delta real entre frames. ```cpp const std::uint64_t NOW_MS = SDL_GetTicks(); int delta_ms = static_cast(NOW_MS - last_ticks_ms_); delta_ms = std::max(delta_ms, 0); // nunca negativo last_ticks_ms_ = NOW_MS; ``` Orden exacto de un frame (`director.cpp:509`–`549`): ```mermaid sequenceDiagram participant SDL participant Dir as Director::iterate participant Scene SDL->>Dir: SDL_AppIterate Note over Dir: Mouse::handleEvent(ev) para todos los eventos Dir->>Dir: processGamepadEvents() (hot-plug + botón SERVICE) Dir->>Dir: processMenuInput() (ServiceMenu consume teclas) Dir->>Dir: processEvents() (detecta SDL_EVENT_QUIT) Dir->>Dir: delta_ms = now - last Dir->>Dir: Audio::update() Dir->>Dir: Mouse::updateCursorVisibility() Dir->>Dir: ServiceMenu::update(delta_ms) Dir->>Scene: update(delta_ms) Dir->>Scene: render() Dir->>Dir: switchSceneIfRequested() Dir->>Dir: EventBuffer::events.clear() ``` El orden de procesamiento de eventos es deliberado y está comentado en `director.cpp:514`: gamepad → menú → quit, para que un `SDL_EVENT_QUIT` inyectado por el menú ("Salir del juego") se vea en el mismo frame antes de limpiar el buffer. El bucle termina (`SDL_APP_SUCCESS`) cuando la escena se reinicia a `nullptr` (quit) o cuando `current_scene_->wantsQuit()` es `true`. --- ## 3. Gestión de escenas ### 3.1. La base `Scene` `source/core/scene/scene.hpp` es minimalista: ```cpp class Scene { public: virtual void update(int delta_ms) = 0; virtual void render() = 0; auto wantsQuit() const -> bool { return wants_quit_; } auto takeExitRequest() -> std::any { return std::exchange(exit_request_, {}); } protected: bool wants_quit_{false}; std::any exit_request_; }; ``` El detalle clave: la petición de transición viaja dentro de un **`std::any`**. Así, `core/scene/scene.hpp` **no necesita conocer los tipos del juego**. La escena deposita un `ExitRequest` (tipo definido en `game/`) y el `Director` hace el `std::any_cast` cuando le toca. ### 3.2. El flujo de escenas: descentralizado al pedir, centralizado al construir `source/game/scenes/scene_flow.hpp` define el contrato: - `enum class SceneId { LOGO, TITLE, INTRO, GAME }`. - Structs de parámetros por escena (`LogoParams`, `TitleParams`, `IntroParams`, `GameParams`) reunidos en un `std::variant SceneParams`. - `struct ExitRequest { SceneId next; SceneParams params; }`. Una escena **no construye la siguiente**: solo declara a dónde quiere ir. El único punto que sabe cómo instanciar cada escena concreta es `Director::buildScene()` (`director.cpp:484`), que tiene a mano todos los recursos compartidos pre-cargados en el constructor. ```mermaid graph LR A[Escena activa] -->|deposita ExitRequest en std::any| B[Director::switchSceneIfRequested] B -->|std::any_cast| C[Director::buildScene] C -->|make_unique| D[Nueva escena] ``` Esto desacopla las escenas entre sí: ninguna `#include` a otra. Para **añadir una escena nueva** (documentado en `scene_flow.hpp:14`): (1) una entrada en el `enum`, (2) un struct `*Params` si necesita datos, (3) un `case` en `buildScene`. > Nota de migración: en `buildScene` se ve que algunas escenas todavía reciben > recursos directamente del `Director` por el constructor en vez de pasar por > el canal `ExitRequest`. Es un estado intermedio, comentado en > `director.cpp:484`. ### 3.3. Flujo del juego y punto de arranque El flujo "completo" que ve el jugador final es **LOGO → TITLE → INTRO → GAME**. - **Release**: siempre arranca en `LOGO` (`director.cpp:314`). - **Debug**: lee `debug.yaml` de la carpeta de sistema; si no existe, lo crea con `start_scene: GAME` para no tragarse los placeholders en el día a día (`director.cpp:301`). `debug.yaml` también puede fijar `starting_players` y `starting_level` (`director.cpp:304`). Las escenas concretas viven en `source/game/scenes/`: `logo_scene`, `title_scene`, `intro_scene`, `game_scene`. La transición entre salas dentro de GAME **no** es un cambio de escena: la `GameScene` persiste y recarga la sala internamente (ver §7). --- ## 4. Renderizado: de la lógica al píxel Esta es la sección más importante para entender el proyecto, porque el render es poco convencional: **todo el dibujo lo hace la CPU sobre un buffer de píxeles**, y la GPU solo interviene al final. ### 4.1. El canvas: un único framebuffer ARGB en RAM - La resolución lógica del juego es **320×240** (`source/utils/defines.hpp:10`, `GameCanvas::WIDTH/HEIGHT`). - `Screen` mantiene `std::vector canvas_` — un array plano de píxeles ARGB8888 (`screen.hpp:143`). - El acceso de los blitters no va por `Screen::get()` directamente, sino por `Rendering::activeCanvas()` (`source/core/rendering/canvas.hpp:38`), para no acoplarse al ciclo de vida de `Screen`. También existe un sistema de **clipping** apilable (`pushClip`/`popClip`, `canvas.hpp:46`). El bit alfa es siempre opaco: `Rendering::OPAQUE_ALPHA = 0xFF000000` (`canvas.hpp:15`). El canal alfa no se usa para transparencia del frame final — la transparencia de sprites se resuelve por *color index transparente* durante el blit (ver §4.2). ### 4.2. `Surface`: sprites paletizados → píxeles ARGB `source/core/rendering/surface.hpp` / `.cpp` es el blitter base. - Una `Surface` guarda los píxeles como **índices de 8 bits** (`Uint8`) más una **paleta propia** de 256 colores ARGB (`Palette = std::array`). - Se carga desde un GIF (`Surface(const std::string&)`, `surface.cpp:38`): decodifica píxeles y paleta con `GIF::Gif` (`core/rendering/gif.*`). - El índice 0 es transparente por convención de todos los GIF del proyecto (`surface.hpp:78`); los tilesets que usen otro índice llaman a `setTransparentColor()`. El núcleo del dibujo es `renderWithColorReplace()` (`surface.cpp:90`). Para cada píxel del rectángulo fuente: 1. Comprueba el *clip* y descarta fuera de él. 2. Lee el índice fuente; si es el transparente, **salta** (no escribe). 3. Aplica `flip` horizontal/vertical reindexando la coordenada fuente. 4. Opcionalmente reemplaza un índice por otro (`source_color → target_color`), usado por `Text` para recolorear glifos (la "tinta" se guarda con un único índice). 5. Escribe `palette_[remapped]` en el canvas. `render()` (`surface.cpp:82`) es un caso particular de lo anterior (delega con `source==target==transparent`, evitando duplicar el bucle). > Hay además una `SubPalette` (`std::array`, identidad por defecto > vía `std::iota`) que permite remapear índices antes de la paleta; se usa para > efectos como el parpadeo de inmunidad del Player. Sobre `Surface` se construyen abstracciones de mayor nivel en `core/rendering/sprite/`: - `Sprite` → `AnimatedSprite` → `MovingSprite` (herencia). - `AnimatedSprite` reproduce tiras de fotogramas; su parser está en `animated_sprite_parser.cpp`. **El `Player` hereda de `AnimatedSprite`** (`player.hpp:18`). ### 4.3. El orden de pintado de un frame de juego `GameScene::render()` (`source/game/scenes/game_scene.cpp:1682`) es la referencia canónica de cómo se compone una imagen. Orden exacto: 1. **Fondo**: `Screen::fillRect` con el color de fondo de la paleta UI. 2. **Tilemap**: `tilemap_.render()` — copia de la textura prerenderizada (ver §9.2). Las celdas de sarcófago quedan transparentes. 3. **(Editor Debug)**: si está activo, pinta entidades congeladas y vuelve. 4. **Capa de pintado** (`PaintLayer`): el "rastro" del jugador por el suelo. 5. **Sombras**: `p->renderShadow()` de cada jugador en el aire — por debajo de todos los actores. 6. **Z-sort de actores**: entidades + jugadores se fusionan en un vector y se ordenan con `std::ranges::stable_sort` por **`getFeetY()` ascendente** (`game_scene.cpp:1755`). Quien tiene los pies más arriba se pinta antes (más al fondo). Esto da la falsa perspectiva 2D. 7. **Pasada de iluminación**: `applyLightingPass()` (ver §4.4). 8. **Fade de sala** (`renderRoomFade`): negro entre actores y HUD. 9. **HUD**: `hud_->render()`. 10. **Texto de transición de sala** y **rótulo de game over**. 11. **Fade final de game over**. 12. `Notifier::render()` y `RenderInfo::render()` (overlays de debug). 13. **`Screen::get().present()`**. El criterio recurrente está comentado: la UI (HUD, notificaciones, menú) se pinta **después** de los fades para quedar siempre legible. ### 4.4. Iluminación por CPU `source/core/rendering/lighting.hpp` / `.cpp`. Es una pasada que **modifica cada píxel del canvas** después de pintar el mundo. Dos modos seleccionables por sala (`lighting.mode` en el YAML): - **SUBTRACTIVE** (histórico, por defecto): `out = src × max(room_light, max_emitter_intensity)`. Los emisores "rompen" una capa oscura. El color del emisor se ignora; solo cuenta la intensidad. - **ADDITIVE**: `out = saturate(src × room_light + Σ emitter_color × intensity)`. Los emisores suman halo cromático. Cada emisor (jugadores, antorchas, momias con `light_radius > 0`) proyecta un círculo radial de 3 anillos configurables en `data/config/lighting.yaml`. Las antorchas pueden tener *flicker* (`light_flicker`), con parámetros resueltos por entidad (`Entity::getFlickerParams`, `entity.hpp:306`). ### 4.5. `Screen::present()` y el backend GPU `Screen::present()` (`source/core/rendering/screen.cpp:306`): 1. Llama al **overlay hook** (menú de servicio) para que pinte *sobre* el canvas antes de subirlo (`screen.cpp:310`). 2. Actualiza el contador de FPS. 3. Si hay **backend GPU** (`shader_backend_`): `uploadPixels(canvas)` + `render()`. Este es el camino por defecto. 4. Si no (fallback): `SDL_UpdateTexture` de una textura STREAMING + `SDL_RenderTexture` + `SDL_RenderPresent`. El backend GPU vive en `source/core/rendering/sdl3gpu/` (SDL3 GPU API + shaders SPIR-V compilados). En `configure()` (`screen.cpp:108`) se intenta el path GPU primero; si falla, hay **fallback automático a `SDL_Renderer`**. El escalado a la ventana usa modos *letterbox / integer / stretch / overscan* (`Defaults::ScalingMode`). **Post-procesado**: shaders en `data/shaders/` (`postfx.frag`, `postfx.vert`, `crtpi_frag.glsl`). El efecto CRT y similares se aplican aquí, en GPU, sobre la textura del canvas. El juego no sabe nada de esto: solo escribe píxeles en el canvas. Los presets se gestionan desde `Options` y se ciclan con hotkeys (§5). ```mermaid graph TD LOG["Lógica del juego (game/)"] -->|Surface::render| CV["Canvas ARGB 320×240 (CPU)"] HUD[HUD / Notifier / RenderInfo] --> CV LIGHT[Lighting pass CPU] --> CV MENU[ServiceMenu overlay] -->|hook en present| CV CV -->|uploadPixels| GPU["ShaderBackend (SDL3 GPU)"] GPU -->|shader postfx/crtpi| WIN[Ventana] CV -.fallback.-> SDLR[SDL_Renderer] --> WIN ``` > **Resumen honesto**: la GPU en este proyecto **no dibuja geometría del > juego**. Solo recibe un mapa de bits ya compuesto y lo escala + filtra. Todo > el "render" del juego es manipulación de un array de `uint32_t` en RAM. --- ## 5. Entrada Hay **dos vías de entrada totalmente distintas**, y entender la diferencia evita mucha confusión: ### 5.1. Vía por eventos (`EventBuffer`) Los `SDL_Event` se acumulan en `EventBuffer::events` (`source/core/system/event_buffer.hpp`) durante `SDL_AppEvent`. Los consumen, en orden, dentro de `Director::iterate()`: - **`Mouse`** (`core/input/mouse.*`): solo reacciona a `MOUSE_MOTION` para reaparecer el cursor (auto-ocultación). - **Gamepad hot-plug y botón SERVICE** (`processGamepadEvents`, `director.cpp:417`): ADDED/REMOVED van a `GamepadManager`; el botón SERVICE (por defecto BACK, remapeable) abre/cierra el menú. - **Menú de servicio** (`processMenuInput`, `director.cpp:446`): con el menú abierto consume todas las teclas salvo F1–F12 y ESC. - **`GlobalInputs`** (`core/input/global_inputs.cpp:190`): hotkeys de sistema F1–F12 + ESC. Itera `EventBuffer` buscando `KEY_DOWN`. Las acciones se leen de `KeyConfig` (`data/config/input.yaml`): | Acción | Efecto | |---|---| | `dec_zoom` / `inc_zoom` | cambia zoom de ventana | | `fullscreen` | alterna pantalla completa | | `toggle_vsync` | alterna VSync | | `screenshot` | guarda captura | | `toggle_shader` / `next_shader` / `next_preset` | post-FX | | `next_scaling_mode` | cicla letterbox/integer/… | | `toggle_render_info` | cicla overlay de debug OFF→TOP→BOTTOM | | `exit` (ESC) | **salida en dos pasos** (ver abajo) | La salida con ESC es en dos pulsaciones: la primera muestra una notificación de confirmación y arma un temporizador; la segunda, dentro de la ventana, activa `wantsQuit()` (`global_inputs.cpp:176`, `global_inputs.hpp:1`). ### 5.2. Vía por *polling* (la del gameplay) El **`Player` lee su input directamente cada frame**, no por eventos: - Teclado: `SDL_GetKeyboardState(nullptr)` (`player.cpp:629`, `764`, `784`). Cada `Player` tiene su propio `Controls` (struct de `SDL_Scancode`, `player.hpp:23`). Por defecto P1 usa flechas + M/N (`Defaults::KeysGame`, `defaults.hpp:75`) y P2 usa WASD (`Defaults::KeysGameP2`). - Mando: `GamepadManager::get().getForPlayer(id)` + `GamepadBindings::isPressed(pad, btn)` (`player.cpp:769`). Esto encaja con que el movimiento se calcula en `Player::tick(dt)` durante el bloque de mundo de `GameScene::update` (§7), donde importa el estado *actual* de las teclas, no la secuencia de eventos. ### 5.3. Detección puntual de pulsaciones en escena Para "¿se ha pulsado START este frame?" hay un helper compartido, `Input::Detect` (`source/game/scenes/start_input.hpp`): - `pressedStart(player_id)` — combina eventos de teclado y botón de mando. Lo usan `TitleScene` (empezar partida) y `GameScene` (*join in progress*). - `pressedActionButton(player_id)` — salto o ataque, usado para saltar el *countdown* de CONTINUE?. ### 5.4. Mandos `core/input/`: - `gamepad_manager.*` — descubre, abre/cierra mandos y asigna mando↔jugador. - `gamepad_bindings.*` — vinculaciones por dispositivo `(name, path)`, persistidas en `gamepad_configs.yaml` (fichero aparte de `config.yaml` porque es por hardware, no por perfil; `director.cpp:93`). - `define_buttons.*`, `key_config.*` — catálogo de botones y mapeo de scancodes de hotkeys. --- ## 6. Comunicación entre módulos El proyecto combina tres mecanismos, cada uno para su propósito: ### 6.1. `EventBus` — pub/sub tipado dentro de un nivel `source/game/entities/event_bus.hpp`. Hay **un `EventBus` por mundo**, propiedad del `EntityManager` (`entity_manager.hpp:199`). Las entidades reciben un puntero vía `Entity::setEventBus`. - El *payload* es un **`std::variant`** tipado (`event_bus.hpp:98`), no `void*` ni `std::any`: `int`, `std::string`, `Entity*`, `TilePos`, `PaintedTile`, `WorldPos`, `WhipSpawnPayload`, `PlayerStatsPayload`. - API: `subscribe(name, handler) → SubId`, `unsubscribe(id)`, `publish(Event)` (+ sobrecargas ergonómicas por tipo de payload). Eventos reales que circulan (vistos en el código): `sarcophagus_opened`, `level_complete`, `player_hit`, `player_killed`, `player_disabled`, `player_jumped`, `player_landed`, `on_tile_entered`, `tile_painted`, `whip_spawn`, `player_stats_changed`. Ejemplo de flujo desacoplado: el `Player` no conoce ni `WhipAttack` ni `EntityManager`; al atacar publica `whip_spawn` con un `WhipSpawnPayload`, y `GameScene` lo escucha y crea la entidad (`event_bus.hpp:66`). Otro: cuando el `Player` muta vidas/score/tesoros, publica `player_stats_changed` con un *snapshot completo*; el HUD lo consume como estado absoluto (no delta). Así cualquier vía que cambie stats se refleja sin que el caller recuerde notificar al HUD (`event_bus.hpp:28`). ### 6.2. Llamadas directas e inyección de dependencias Donde la relación es estructural y estable, se usa llamada directa o inyección por *setter*: p.ej. `Entity::setEntityManager` (una momia consulta el mundo para no caer en un `Hole`, `entity.hpp:180`), o `Map::setEntityCollider` (el `EntityManager` registra un predicado de colisión para muros dinámicos, `map.hpp:171`). ### 6.3. *Hooks* / callbacks para invertir dependencias `core/` no conoce `game/`, así que cuando `core/` necesita algo del juego se inyecta un `std::function`: - `Screen::setOverlayRenderer(callback)` — el `Director` registra una lambda que pinta el `ServiceMenu` sobre el canvas en `present()`. Así `Screen` puede poner el menú por encima de cualquier escena sin incluir `game/ui` (`screen.hpp:81`, `director.cpp:291`). ### 6.4. Singletons Varios subsistemas son **Meyers singletons** (`get()`): `Screen`, `Resource::Cache`, `GamepadManager`, `GamepadBindings`, `Locale`, `UiConfig`, `Lighting`, `LevelManager`, `ServiceMenu`, `Notifier`, `RenderInfo`. Ver §13 para el matiz de su ciclo de vida. --- ## 7. Lógica del juego El grueso vive en `source/game/scenes/game_scene.cpp` (la clase más grande del proyecto, ~1800 líneas). `GameScene` coordina mapa, entidades, jugadores, HUD, y **dos máquinas de estado**. ### 7.1. FSM de sala — `RoomFsm` `source/game/scenes/room_fsm.hpp`. Lógica **pura** (no toca SDL/Audio; solo `Locale` para formatear texto), extraída para ser testeable. ``` ROOM_INTRO → PLAYING → ROOM_OUTRO → (siguiente sala) ROOM_INTRO ``` Cada estado tiene sub-fases con timings cargados de `data/scenes/game/game.yaml` (`room_fsm.hpp:33`). Por ejemplo, `ROOM_INTRO` tiene: `fade_in → text_delay → text_enter → text_hold → text_exit → music_tail`. La fase `music_tail` es una "cola silenciosa" calculada para que el *timeline* visual termine exactamente cuando drena el `.ogg` de música — **no hay callback de fin de música**; la transición la dispara `GameScene::updateRoomTransition` cuando la FSM consume la última fase (`room_fsm.hpp:27`). La función central es `RoomFsm::advance(current, transition, cfg, delta_ms)` → devuelve el estado resultante; si cambia, el caller resetea la transición. ### 7.2. FSM de game over — `GameOverFsm` `source/game/scenes/game_over_fsm.*`. Sub-FSM independiente (`CONTINUE? → countdown → GAME OVER → FADE_OUT → DONE`) que se superpone a la `RoomFsm` y puede dispararse durante `PLAYING` cuando llegan a 0 los jugadores en partida. Al terminar pide la transición a `LOGO` (`game_scene.hpp:280`–`333`). El *countdown* puede saltarse pulsando acción (`handleCountdownSkip`). ### 7.3. FSM del `Player` `source/game/entities/player.hpp:66` define `enum class State`: `GROUNDED, AIRBORNE, LANDING_STUN, HURT, ATTACKING, EXITED, DISABLED, DYING`. Cada estado define qué input acepta y cómo se mueve/colisiona. Detalles relevantes: - El **salto** usa `jump_z_` (altura): saltar por encima de la `height_` de una entidad permite esquivar momias/rocas (`entity.hpp:219`). - `DISABLED` = slot inactivo (P2 en single-player, o todos durante transiciones); `EXITED` = ha salido por la puerta; `DYING` = animación de muerte + delay antes de respawn. - El array `players_` **siempre tiene 2 entradas** (P1, P2); el slot inactivo queda `DISABLED` y puede entrar a media partida ("join in progress") pulsando START (`game_scene.hpp:87`, `handleJoinInput`). ### 7.4. El bloque de mundo: orden de update Dentro de `GameScene::update` (`game_scene.cpp:1519`), el mundo solo avanza si **no** está abierto el menú ni el editor y `delta_ms > 0`. El pipeline de movimiento es de **tres pasadas** para intercalar la resolución jugador↔jugador (`game_scene.cpp:1629`): 1. `player->tick(DT)` — cada jugador calcula su velocidad intencionada (teclado + mando + *facing*). 2. `resolvePlayerVsPlayer(...)` — anula velocidades en choque frontal o las escala a `push_factor_` (empuje). 3. `player->applyMovement(DT)` — en orden "empujado primero". Después: `entity_manager_.update(DT)` (tras los jugadores, para que un `on_tile_entered` emitido este frame se procese ya) y `checkPlayerCollisions(...)`. ### 7.5. Reglas del juego - **Sarcófagos** (`concrete/sarcophagus.hpp`): el jugador debe "pintar" (recorrer) todo el perímetro de tiles del sarcófago para abrirlo. FSM: `CLOSED → OPENING → OPEN`. Al abrirse publica `sarcophagus_opened` y reparte recompensa (`points`/`treasures`) al jugador atribuido. - **Puerta** (`concrete/door.hpp`): FSM `CLOSED → OPENING → OPEN`. El `EntityManager` la abre automáticamente cuando el nº de sarcófagos abiertos iguala el total (`entity_manager.hpp:21`). Al abrirse libera el pasadizo (`Map::door_open = true`). Cruzarla dispara `Player::exit()` → `player_exited` → `level_complete` → OUTRO. - **Pintado del suelo** (`game/map/paint_layer.*`): escucha `on_tile_entered`, filtra pintabilidad (`Map::isPaintable`) y re-publica `tile_painted` para los sarcófagos (`game_scene.hpp:219`). - **Hazards**: momias, rocas rodantes (`rolling_rock`), trampas de suelo/pared (`floor_trap`, `wall_trap`), agujeros (`hole`). Los letales publican `player_killed`. --- ## 8. Entidades ### 8.1. `Entity`: composición, no herencia de sprite `source/game/entities/entity.hpp`. Decisión de diseño explícita (`entity.hpp:34`): **`Entity` NO hereda de `AnimatedSprite`**. La animación es un *componente opcional* (`std::optional anim_`), porque no todas las entidades la necesitan (trampas invisibles, triggers) y para no imponer semántica de movimiento continuo a actores estáticos. `Entity` reúne lo común: posición en tiles y píxeles, caja de colisión opcional (`CollisionBox`), flags de comportamiento (`collides_`, `blocks_movement_`, `triggers_on_overlap_`, `is_floor_decal_`, `dead_`), parámetros de luz, y un conjunto de **hooks polimórficos**: | Hook | Para qué | |---|---| | `update(dt)` / `render()` | tick lógico / dibujo | | `onPlayerContact(Player*)` | el jugador toca la entidad (daño, salida…) | | `onWhipHit(WhipAttack*)` | recibe un latigazo (override en `Mummy`) | | `notifyTilePainted(col,row,id)` | un tile se pintó (lo usa `Sarcophagus`) | | `onJustSpawned()` | recién nacida (dispara animación EMERGING) | | `onRemoved()` | antes de borrarse (mantener invariantes) | | `shouldReceiveContact()` | filtro de si recibe contacto este frame | | `getFeetY()` | Y de los pies, para el z-sort (§4.3) | El polimorfismo evita "sacos" separados por tipo: el `EntityManager` itera todas y cada entidad decide qué hace (`entity_manager.hpp:78`). ### 8.2. `EntityManager`: contenedor y coordinador por nivel `source/game/entities/entity_manager.hpp`. **No es singleton**: una instancia por nivel, vive dentro de `GameScene` (`game_scene.hpp:80`). Responsabilidades: poseer las entidades (`std::vector>`), propagar `update`, contar sarcófagos abiertos y abrir la puerta, hacer *fan-out* de `notifyTilePainted`, comprobar colisiones jugador↔entidad, y cargar el nivel desde YAML (`loadFromLevel`). También gestiona una **cola de actores pendientes** (`pending_actors_`): los hazards dinámicos declarados en el YAML no se instancian de inmediato, sino que `spawnPendingActors()` los materializa en una sub-fase concreta del ROOM_INTRO con su cinemática de aparición (`entity_manager.hpp:130`). ### 8.3. `EntityFactory`: registro abierto a extensión `source/game/entities/entity_factory.hpp`. Factory por nombre con **auto-registro**: cada `concrete/*.cpp` declara su creador con la macro `REGISTER_ENTITY(name, Class)`, que genera un objeto estático cuyo constructor se ejecuta antes de `main` y se registra en la factory (`entity_factory.hpp:56`). > **Añadir un tipo de entidad = nueva clase en `concrete/` + una línea > `REGISTER_ENTITY`.** No se toca la factory ni el manager. Aviso importante recogido en el código: para que el registrador se ejecute, el `.cpp` debe enlazarse directamente al ejecutable (no oculto en una static lib sin `--whole-archive`); todos los `concrete/*.cpp` entran en `APP_SOURCES` (`entity_factory.hpp:53`). Tipos concretos existentes (`source/game/entities/concrete/`): `door`, `dust`, `floor_trap`, `hole`, `mummy`, `rolling_rock`, `sarcophagus`, `solid_props`, `torch`, `wall_trap`, `whip_attack`. ### 8.4. Jerarquía intermedia Entre `Entity` y las concretas hay clases intermedias que aportan semántica: `decoration.hpp`, `solid_decoration.hpp`, `interactable.hpp`, `hazard.hpp` (con `DynamicHazard` para actores con fase de aparición). `Sarcophagus` hereda de `Interactable`, por ejemplo. --- ## 9. Mapa y niveles ### 9.1. `Map`: geometría y colisión (un namespace, no una clase) `source/game/map/map.hpp`. **`Map` es un `namespace`**, no una clase, porque no hay estado de instancia: la geometría de la sala activa vive como variables `inline` del namespace (`map.hpp:32`). Se carga con `Map::loadFrom(yaml)` desde el bloque `geometry:` del YAML de la pirámide. Contiene la geometría de la sala (graella de sarcófagos, *pitch*, anchos de muro, playfield, posición de la puerta `door_col`) y las funciones de colisión: - `collides(box)` — ¿la caja choca con sarcófago, pasadizo de puerta cerrada o entidad sólida? (`map.hpp:164`). - `insidePlayfield(box)`, `isPaintable(col,row)`, `pixelToTile(px,py)`. Para no acoplar a todos al namespace global, existe `Map::Info` — un *snapshot* POD copiable de la geometría + punteros a funciones de colisión, que se inyecta a entidades (hoy el `Player`, decoupling COUP-02; `map.hpp:201`). ### 9.2. `Tilemap`: prerenderizado una vez `source/game/map/tilemap.hpp`. La cuadrícula de tiles 16×16 se **prerrenderiza una sola vez a un buffer ARGB propio** en el constructor; cada frame es una copia clipada al canvas (`tilemap.hpp:8`). Así no se recorre la cuadrícula cada frame. En Debug guarda además `data_` (la rejilla de índices) para el editor. ### 9.3. `LevelManager`: progresión `source/game/levels/level_manager.hpp`. Meyers singleton, **fuente de verdad de la progresión**. Carga `data/maps/levels.yaml` (lista de salas con `pyramid`, `room`, `file`). API: `currentPath()`, `advance()`, `restart()`, `setIndex()`, `setStartingPoint(pyramid, room)`. No hace *loop* por sí mismo: la política la decide `GameScene` (`level_manager.hpp:49`). Los mapas reales viven en `data/maps/p1/`…`p5/` (5 pirámides × ~6 salas), cada una con sus `rN.yaml` y un `pyramid.yaml` con la geometría. --- ## 10. Recursos ### 10.1. `Resource::Cache` `source/core/resources/resource_cache.hpp`. Meyers singleton que cachea por nombre: `Surface`, animaciones, paletas, música (`Ja::Music`) y sonidos (`Ja::Sound`). API `loadX(name, path)` + `getX(name)`. La búsqueda es lineal sobre vectores (suficiente para el tamaño actual). Tiene un `clearAudio()` que **el `Director` llama explícitamente** antes de destruir `Audio` y `SDL_Quit`, porque al ser Meyers el Cache se destruiría tarde, con los streams SDL ya invalidados (`resource_cache.hpp:43`). ### 10.2. Pack vs disco `source/core/resources/resource_pack.*` + `resource_helper.*`. Toda lectura de fichero pasa por `Resource::Helper::loadFile(path)`. En **Release** los datos salen de `resources.pack` (empaquetado por `tools/pack_resources/` en cada build). En **Debug** se activa *fallback* a disco apuntando al árbol fuente, de modo que editar un YAML o un GIF en `data/` se ve sin re-empaquetar (`director.cpp:52`). ### 10.3. Formatos de arte - **GIF** es el formato de sprites, tilesets y fuentes (`data/**/*.gif`), decodificado por `core/rendering/gif.*`. Cada GIF lleva su paleta. - **Fuentes**: pares `.fnt` + `.gif` (`data/fonts/`), consumidos por `Text` (`core/rendering/text.*`). - **Música**: `.ogg` decodificado con `stb_vorbis` (`source/external/`). - **Paletas sueltas**: `.pal` JASC o `.gif` (`palette_loader.*`, `data/palettes/`). --- ## 11. Configuración y constantes ### 11.1. La cadena YAML → `Options` → subsistema El patrón es uniforme: un YAML en `data/config/` (o en la carpeta de sistema del usuario) se carga, y si falta una clave cae al valor compilado en `source/utils/defaults.hpp`. `defaults.hpp` es el **catálogo central de constantes** organizado en sub-namespaces (`Window`, `Video`, `Audio`, `KeysGame`, `KeysGameP2`, `I18n`, `Paths`, `UI`, …). `source/game/options.*` + `options_apply.*` traducen esas opciones a structs tipados que los subsistemas consumen sin conocer YAML (p.ej. `Options::buildScreenConfig()` → `Screen::Config`). Esto mantiene `Screen`, `Audio`, etc. desacoplados de la capa de configuración. Ficheros de configuración por categoría: - `data/config/`: `ui.yaml`, `input.yaml`, `lighting.yaml`, `sounds.yaml`, `editor.yaml`. - En la carpeta de sistema del usuario: `config.yaml`, `gameplay.yaml`, `gamepad_configs.yaml`, `debug.yaml` (Debug), y `shaders/postfx.yaml`, `shaders/crtpi.yaml`. ### 11.2. Carpeta de sistema por plataforma `Director::createSystemFolder()` (`director.cpp:353`): - Windows: `%APPDATA%\jailgames\aee_arcade` - macOS: `~/Library/Application Support/jailgames/aee_arcade` - Linux: `$XDG_CONFIG_HOME/jailgames/aee_arcade` (por defecto `~/.config/jailgames/aee_arcade`) Las capturas de pantalla y los presets de shader cuelgan de ahí. ### 11.3. Localización `source/core/locale/locale.*` + `data/locale/{ca,en}.yaml`. Todo texto visible pasa por `Locale::get().text(clave)`; si falta el fichero o la clave, devuelve la propia clave como *fallback* (`director.cpp:124`). Idioma por defecto: `ca` (`defaults.hpp:41`). --- ## 12. Modo demo / IA **No existe un modo demo, *attract mode*, autoplay ni IA que controle a los jugadores.** Lo he buscado explícitamente y no lo hay. Aclaración para evitar malentendidos: la única aparición de la palabra "ai" en el código es un **bloque YAML `ai:`** en las plantillas de `mummy` y `rolling_rock`, que solo fija la **dirección inicial** del hazard: ```yaml ai: initial_direction: east # mummy.cpp:96, rolling_rock.cpp:41 ``` No es un sistema de IA: es un parámetro de spawn. El movimiento de las momias es lógica fija en `mummy.cpp` (patrullaje/persecución simple con consulta al mundo para no caer en agujeros), no un módulo de IA separado ni dirigido por el modo de juego. Si en el futuro se quisiera un *attract mode*, el punto natural de enganche sería el flujo de escenas (`scene_flow.hpp`) y/o sustituir el *polling* de input del `Player` por una fuente sintética, pero **hoy nada de eso está implementado**. --- ## 13. Convenciones y patrones recurrentes Patrones que verás una y otra vez (reconocerlos acelera mucho la lectura): - **Idioma de los comentarios**: valenciano/catalán. Los identificadores son en inglés; las constantes locales suelen ir en `MAYÚSCULAS`. - **Estilo C++ moderno**: `[[nodiscard]]` en casi todo getter, trailing return types (`auto f() -> T`), `enum class` con tipo subyacente explícito, `std::optional`/`std::variant` en vez de punteros nulos o `void*`. Hay `.clang-format` y `.clang-tidy` en la raíz. - **Meyers singleton con ciclo de vida manual**: los singletons que dependen de SDL (`Screen`, `Audio`, `Cache` para audio) exponen `get()` pero su `configure()`/`shutdown()` los llama explícitamente el `Director`, **no** el destructor estático. El motivo (crashes en `vkDestroyInstance` al descargar Vulkan/Wayland antes de tiempo) está documentado en `screen.hpp:28` y `director.cpp:336`. - **Inyección de dependencias por `std::function` / setters** para invertir la dirección `core/` ↛ `game/` (ver §6.3). - **Data-driven con fallback compilado**: nunca asumas que un valor es una constante de código sin más; suele venir de un YAML con default en `defaults.hpp`. - **`std::any` solo en la frontera `Scene`↔`Director`** para no acoplar la base de escenas a los tipos del juego (§3.1). - **Composición sobre herencia para entidades** (§8.1). - **Separación render-de-colisión**: el `Tilemap` que se ve y `Map::collides` que bloquea son cosas distintas (un sarcófago se dibuja como actor pero el `Map` lo trata como obstáculo). - **Bloques `#ifdef _DEBUG`**: el editor de tilemap (`game/editor/`), los saltos de sala (`[`/`]`), forzar apertura de puerta, y el arranque configurable por `debug.yaml` solo existen en builds Debug. - **Tests**: en `tests/` hay smoke tests y tests de FSM/animación (`test_scene_flow`, `test_game_fsm`, `test_whip_attack`, etc.) con *stubs* para aislar dependencias pesadas (audio, player). - **Política de commits y branches**: ver `docs/convencions.md` (mensajes cortos en valenciano, trabajo en ramas de feature, merge `--no-ff`). --- ## 14. Guía de navegación: "si quieres tocar X, mira Y" | Quiero… | Empieza por… | |---|---| | Entender el arranque y el orden de init | `source/core/system/director.cpp` (ctor + `iterate()`) | | Cambiar el orden update/render del frame | `Director::iterate()` (`director.cpp:509`) | | Añadir una escena | `source/game/scenes/scene_flow.hpp` + `Director::buildScene` | | Cómo se dibuja un sprite | `source/core/rendering/surface.cpp` (`renderWithColorReplace`) | | El orden de pintado del juego | `GameScene::render()` (`game_scene.cpp:1682`) | | El z-order / falsa perspectiva | `getFeetY()` + `stable_sort` en `game_scene.cpp:1755` | | Iluminación / antorchas | `core/rendering/lighting.*` + `data/config/lighting.yaml` | | Shaders / efecto CRT | `core/rendering/sdl3gpu/` + `data/shaders/` + `Options` | | Escalado de ventana / fullscreen | `core/rendering/screen.cpp` (`adjustWindowSize`, `setScalingMode`) | | Controles del jugador | `Player::Controls` (`player.hpp:23`) + `Player::tick` (`player.cpp`) | | Hotkeys de sistema (F1–F12, ESC) | `core/input/global_inputs.cpp` + `data/config/input.yaml` | | Menú de servicio (F9/SERVICE) | `source/game/ui/service_menu.*` | | Mandos / vinculaciones | `core/input/gamepad_manager.*`, `gamepad_bindings.*` | | Añadir un tipo de entidad | nueva clase en `game/entities/concrete/` + `REGISTER_ENTITY` | | Eventos entre entidades | `game/entities/event_bus.hpp` | | Lógica de una sala (intro/outro) | `RoomFsm` (`game/scenes/room_fsm.*`) + `data/scenes/game/game.yaml` | | Game over / continue | `GameOverFsm` (`game/scenes/game_over_fsm.*`) | | Sarcófagos / puerta / pintado | `concrete/sarcophagus.*`, `concrete/door.*`, `game/map/paint_layer.*` | | Geometría/colisión de la sala | `source/game/map/map.hpp` | | El tileset que se ve | `source/game/map/tilemap.*` + `data/tilesets/` | | Progresión de salas | `game/levels/level_manager.*` + `data/maps/levels.yaml` | | Editar mapas (Debug) | `source/game/editor/` | | Cargar un recurso | `core/resources/resource_cache.*` + `resource_helper.*` | | Empaquetar datos | `tools/pack_resources/` | | Valores por defecto / constantes | `source/utils/defaults.hpp` | | Textos / traducciones | `core/locale/locale.*` + `data/locale/*.yaml` | | HUD (vidas/score/tesoros) | `source/game/ui/hud.*` (escucha `player_stats_changed`) | | Audio (música/sfx) | `core/audio/` (`audio.*`, `jail_audio.*`) | --- *Documento generado a partir de la lectura directa del código en el commit actual de la rama `main`. Si algo aquí no cuadra con el código, el código manda: actualiza este documento.*