908 lines
41 KiB
Markdown
908 lines
41 KiB
Markdown
# 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<int>(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<std::uint32_t> 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<Uint32, 256>`).
|
||
- 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<Uint8,256>`, 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<AnimationComponent> 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<unique_ptr<Entity>>`),
|
||
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.*
|