diff --git a/docs/ARQUITECTURA.md b/docs/ARQUITECTURA.md new file mode 100644 index 0000000..fcdec7a --- /dev/null +++ b/docs/ARQUITECTURA.md @@ -0,0 +1,907 @@ +# 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.*