Files
coffee-crisis-ae/docs/ARQUITECTURA.md
T

643 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Arquitectura de **Coffee Crisis Arcade Edition**
> Guía de orientación para un desarrollador nuevo en el proyecto.
>
> Cada afirmación 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.
>
> **Coffee Crisis Arcade Edition** es un *shooter* arcade cooperativo de 2
> jugadores escrito en **C++20 sobre SDL3**: los jugadores defienden el café
> de globos gigantes. Apunta a Windows, Linux, macOS (Intel/Apple Silicon),
> Raspberry Pi, Anbernic y web (Emscripten). Los comentarios del código están
> en español/valenciano; este documento está en castellano.
---
## Índice
1. [Visión general](#1-visión-general)
2. [Punto de entrada y bucle principal](#2-punto-de-entrada-y-bucle-principal)
3. [Secciones y flujo de la aplicación](#3-secciones-y-flujo-de-la-aplicación)
4. [Renderizado: de la lógica al píxel](#4-renderizado-de-la-lógica-al-píxel)
5. [Entrada](#5-entrada)
6. [Lógica del juego: la clase `Game`](#6-lógica-del-juego-la-clase-game)
7. [Entidades y managers de gameplay](#7-entidades-y-managers-de-gameplay)
8. [Modo demo y attract mode](#8-modo-demo-y-attract-mode)
9. [Recursos](#9-recursos)
10. [Audio](#10-audio)
11. [Configuración, parámetros y constantes](#11-configuración-parámetros-y-constantes)
12. [Localización](#12-localización)
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
El árbol `source/` separa **motor** y **juego**:
- **`source/core/`** — motor genérico: `system` (arranque, secciones, demo,
eventos globales), `rendering` (+ `sdl3gpu`, `sprite`), `input`, `resources`,
`audio`, `locale`.
- **`source/game/`** — el juego concreto: `scenes` (las secciones), `gameplay`
(managers), `entities` (jugador, globos, balas, ítems…), `ui`, y `options`.
- **`source/utils/`** — `color`, `param`, `utils`, `defines`.
- **`source/external/`** — vendorizado: `nlohmann/json`, `fkyaml`, `stb_image`,
`stb_vorbis`.
Los `#include` son **absolutos respecto a `source/`** (p.ej.
`#include "core/audio/audio.hpp"`); CMake añade un único `-I.../source`
(ver `CLAUDE.md`). ~150 ficheros C++, ~32.000 líneas.
**Cuatro ideas-fuerza que conviene interiorizar:**
1. **El flujo de la aplicación se conduce con una variable global**
(`Section::name`), no con objetos de transición. El `Director` reacciona a
sus cambios (§3).
2. **El render usa texturas GPU vía `SDL_Renderer`** dibujadas sobre una
*render-target texture*, con post-procesado opcional vía un backend SDL3
GPU. No es un blitter de software (§4).
3. **El gameplay se organiza con managers/pools** (`BalloonManager`,
`BulletManager`, `StageManager`) coordinados por una clase `Game` muy grande
(§6, §7).
4. **Sí hay modo demo** (*attract mode*): es **reproducción de input grabado**,
no IA (§8).
```mermaid
graph TD
SDL[SDL3 callbacks · main.cpp] --> DIR[Director]
DIR -->|lee Section::name| ST{handleSectionTransition}
ST --> SEC["Section activa (Logo/Intro/Title/Game/…)"]
DIR --> GE[GlobalEvents] --> INP[Input] & SVC[ServiceMenu]
SEC --> GAME[Game]
GAME --> BM[BalloonManager] & BLM[BulletManager] & STG[StageManager]
GAME --> PL[Players] -->|Input::Action| INP
GAME --> DEMO["Demo (playback de input grabado)"] -.->|setInput| PL
GAME -->|SDL_RenderTexture| CV["game_canvas_ (render target)"]
CV -->|RenderReadPixels → uploadPixels| SB[ShaderBackend SDL3 GPU] --> WIN[Ventana]
RES["Resource / Asset"] -.-> GAME & SEC
```
---
## 2. Punto de entrada y bucle principal
### 2.1. SDL conduce el bucle (callbacks)
`source/main.cpp` define `SDL_MAIN_USE_CALLBACKS`: no hay `while` propio. SDL
llama a cuatro funciones, todas delegando en el `Director`:
```cpp
SDL_AppInit *appstate = new Director(argc, argv); // main.cpp:15
SDL_AppIterate Director::iterate(); // un frame
SDL_AppEvent Director::handleEvent(event); // un evento
SDL_AppQuit delete Director;
```
### 2.2. El `Director`
`source/core/system/director.{hpp,cpp}` es el orquestador. Mantiene **un
`unique_ptr` por cada sección** (`preload_`, `logo_`, `intro_`, `title_`,
`game_`, …) de los que **solo uno está vivo** a la vez (`director.hpp:60`).
**Constructor (`director.cpp:82`)**: fija la semilla aleatoria, crea la carpeta
de sistema (`jailgames/coffee_crisis_arcade_edition`), decide la sección
inicial según el build (`RECORDING` → GAME; `_DEBUG` → lee `debug.yaml`; Release
→ LOGO) y llama a `init()`.
**`init()` (`director.cpp:131`)** inicializa en orden: `Asset` (índice de
ficheros), sistema de recursos (`resources.pack`, con/ sin *fallback* a disco
según build), `Input`, `Options` (config.yaml, controllers.json, presets de
shaders), parámetros y *scores*, `Lang`, `Screen`, `Audio` y `Resource`.
### 2.3. Arranque NO bloqueante (PRELOAD incremental)
Un detalle importante: si el modo de carga es `PRELOAD`, **no se cargan todos
los recursos de golpe**. `init()` redirige el arranque a la sección `PRELOAD`
guardando el destino real en `Section::post_preload` (`director.cpp:191`):
```cpp
Section::post_preload = Section::name; // destino real
Section::name = Section::Name::PRELOAD; // mientras tanto, barra de progreso
Resource::get()->beginLoad();
```
Cada frame, `Director::iterate()` (`director.cpp:489`) llama a
`Resource::loadStep(50 /*ms*/)`: carga recursos hasta agotar un presupuesto de
50 ms por frame, manteniendo la ventana y el bucle vivos. Cuando termina,
`finishBoot()` inicializa lo que depende de los recursos (`ServiceMenu`,
`Notifier`, singletons de `Screen`) y salta a `post_preload`.
### 2.4. Orden del frame y gestión del tiempo
`Director::iterate()` (`director.cpp:489`):
```mermaid
sequenceDiagram
participant SDL
participant Dir as Director::iterate
participant Sec as Section activa
SDL->>Dir: SDL_AppIterate
alt boot_loading_
Dir->>Dir: Resource::loadStep(50ms) → finishBoot() al terminar
end
Dir->>Dir: handleSectionTransition() (destruye/crea sección)
Dir->>Sec: iterate() (la sección hace su propio update+render)
```
El tiempo es **time-based**: cada sección calcula su propio `delta_time`. En
`Game` es `calculateDeltaTime()` (`game.hpp:210`), usado en todos los `update`.
Los eventos llegan por separado vía `Director::handleEvent`
`GlobalEvents::handle` + reenvío a la sección activa (`director.cpp:539`).
### 2.5. Reinicio en caliente
Igual que su proyecto hermano, soporta reinicio real vía `execv`
(`Director::relaunch()`, `director.cpp:254`): la sección `RESET` (p.ej. tras
F10 o cambio de idioma) reemplaza el proceso por sí mismo; si no se puede
(Emscripten, `argv` inválido), cae a un `reset()` interno que recarga recursos y
vuelve a `LOGO`.
---
## 3. Secciones y flujo de la aplicación
### 3.1. Variables globales de estado
`source/core/system/section.hpp` define el estado del flujo como **variables
globales `inline`** en el namespace `Section`:
```cpp
inline Name name = Name::RESET; // sección a la que ir
inline Name post_preload = Name::LOGO; // destino tras PRELOAD
inline Options options = Options::NONE; // 1P/2P/BOTH, timeouts, etc.
inline AttractMode attract_mode = AttractMode::TITLE_TO_DEMO;
```
`Name` enumera: `RESET, PRELOAD, LOGO, INTRO, TITLE, GAME, HI_SCORE_TABLE,
GAME_DEMO, INSTRUCTIONS, CREDITS, QUIT`. Cualquier parte del código cambia el
flujo simplemente asignando `Section::name = ...`.
### 3.2. La transición de secciones
`Director::handleSectionTransition()` (`director.cpp:390`) se ejecuta cada
frame:
- Si `Section::name == RESET`: intenta `relaunch()`; si vuelve, hace el reset
interno.
- Si `Section::name == last_built_section_name_`: no hace nada (ya está viva).
- Si cambió: `resetActiveSection()` (libera todos los `unique_ptr`) y un
`switch` construye la nueva sección. Para `GAME` traduce `Section::options`
a `Player::Id` (1P/2P/BOTH); para `GAME_DEMO` construye `Game(..., DEMO_ON)`
con jugador aleatorio (`director.cpp:448`).
Cada sección (`source/game/scenes/`) es autónoma y expone `iterate()` +
`handleEvent()`. `Director::iterate` despacha al puntero vivo con una cadena de
`if/else if` (`director.cpp:517`).
```mermaid
graph LR
RESET --> PRELOAD --> LOGO --> INTRO --> TITLE
TITLE -->|jugar| GAME --> HI_SCORE_TABLE --> TITLE
TITLE -->|timeout / attract| GAME_DEMO --> TITLE
TITLE -->|attract alterno| LOGO
TITLE --> INSTRUCTIONS & CREDITS
```
### 3.3. Attract mode
El *attract mode* alterna entre mostrar la demo y volver al logo. En
`title.cpp:51` el Title, al construirse, mira `Section::attract_mode` y fija su
`next_section_` (a `GAME_DEMO` o `LOGO`), **invirtiendo el modo** para la
próxima vez. Cuando el Title agota su *timeout* sin input, salta a esa sección
(ver §8).
---
## 4. Renderizado: de la lógica al píxel
A diferencia de un blitter de software, aquí **los sprites son texturas GPU**
compuestas por `SDL_Renderer`, y el post-procesado se hace en un backend SDL3
GPU.
### 4.1. `Texture` y la jerarquía de sprites
- `source/core/rendering/texture.hpp``Texture` envuelve un `SDL_Texture*`,
con `loadFromFile`, `createBlank`, `setAsRenderTarget` y un `render(x, y,
clip, zoom, angle, …)`. Es la unidad de dibujo.
- `source/core/rendering/sprite/` — jerarquía sobre `Sprite`
(`sprite.hpp:13`), que guarda **varias texturas** (`textures_`), un
`sprite_clip_` y posición/zoom:
- `AnimatedSprite` — animación por fotogramas (clip que avanza).
- `MovingSprite` — con velocidad.
- `PathSprite` — sigue rutas precalculadas (`Path`).
- `SmartSprite` — sprite con lógica propia (p.ej. el café que salta al ser
golpeado).
- `CardSprite` — variante para "cartas"/paneles.
El `Player`, por ejemplo, compone un `AnimatedSprite player_sprite_` y un
`power_sprite_` para el aura (`player.hpp:250`).
### 4.2. Dos render-targets
Hay **dos texturas de destino** encadenadas:
1. **`Game::canvas_`** — textura de la *zona de juego*. `Game::fillCanvas()`
(`game.cpp`) fija el render-target a `canvas_` y dibuja, **en este orden**:
`background → smart_sprites → items → balloons → tabe → players → bullets →
path_sprites`. Ese orden es el z-order del playfield.
2. **`Screen::game_canvas_`** — textura de *pantalla completa* (ARGB8888 a la
resolución de juego; `screen.cpp:92`, `SDL_TEXTUREACCESS_TARGET`).
`Game::render()` hace `screen_->start()` (que pone el target en
`game_canvas_`), copia `canvas_` sobre él, y encima dibuja **scoreboard** y
los **fades** de entrada/salida; finalmente `screen_->render()`.
### 4.3. Post-procesado y presentación
`Screen::renderPresent()` (`screen.cpp:156`) decide cómo llega a la ventana:
- **Con backend GPU acelerado**: lee los píxeles de `game_canvas_` con
`SDL_RenderReadPixels` a un `pixel_buffer_` CPU, los sube al backend
(`shader_backend_->uploadPixels(...)`) y este los renderiza con el shader
activo (`shader_backend_->render()`).
- **Sin backend** (fallback): vuelca `game_canvas_` directamente a la ventana
con `SDL_RenderTexture`.
El backend vive en `source/core/rendering/sdl3gpu/` (SDL3 GPU API; shaders
SPIR-V compilados offline desde GLSL en `data/shaders/`, embebidos en cabeceras
`spv/`/`msl/`). Hay dos shaders seleccionables, **PostFX** y **CrtPi**, con
presets en `postfx.yaml`/`crtpi.yaml` (`screen.cpp:382`). El build `NO_SHADERS`
(Anbernic) desactiva todo el pipeline de shaders.
```mermaid
graph TD
OBJ["background, balloons, players, bullets…"] -->|SDL_RenderTexture| CANVAS["Game::canvas_ (zona de juego)"]
CANVAS -->|copiar| GC["Screen::game_canvas_ (pantalla)"]
SB2[scoreboard] --> GC
FADE[fades de entrada/salida] --> GC
GC -->|RenderReadPixels → uploadPixels| SHADER["ShaderBackend (PostFX/CrtPi)"]
SHADER --> WIN[Ventana]
GC -.fallback sin GPU.-> WIN
```
### 4.4. Efectos de pantalla
`Screen` integra efectos globales sobre la presentación: **shake**
(`ShakeEffect`, desplaza el rect), **flash** (`FlashEffect`, tinte temporal) y
**attenuate** (atenuación) — todos definidos como structs internos en
`screen.hpp:108``205`. Las transiciones entre estados usan
`source/core/rendering/fade.*` (cuadrícula de cuadros).
---
## 5. Entrada
### 5.1. `Input` singleton
`source/core/input/input.hpp`. Centraliza teclado y mandos bajo un enum de
**acciones** (`Input::Action`, alias de `InputAction`). El mapeo por defecto
vive en structs:
- **`Keyboard`** (`input.hpp:54`): flechas = mover; **Q/W/E** = disparar
izquierda/centro/derecha; Enter = START; F12 = SERVICE; P = PAUSE; ESC = EXIT;
**F1F11** = ventana/vídeo/audio/idioma/reset/info.
- **`Gamepad`** (`input.hpp:99`): cruceta = mover; WEST/NORTH/EAST = disparar;
START/ BACK = start/service. Cada mando es un `shared_ptr<Gamepad>` con su
propio `bindings`, nombre y *path*.
La consulta principal es `checkAction(action, repeat, check_keyboard, gamepad)`
(`input.hpp:168`). Hay además detección de ejes y triggers como botones, y
gestión de configuraciones de mando persistidas (`gamepad_config_manager`,
`controllers.json`).
### 5.2. Eventos globales y hotkeys
- `source/core/system/global_events.cpp``GlobalEvents::handle(event)` trata
lo común a cualquier sección: `SDL_EVENT_QUIT`, *resize*, render target reset,
hot-plug de mandos (con notificación tras `markStartupComplete`), el toggle
del menú de servicio y el ratón.
- `source/core/input/global_inputs.cpp` — hotkeys de sistema, despachadas con
un mapa acción→lambda (`global_inputs.cpp:187`): fullscreen, zoom ±, audio,
autofire, idioma (CHANGE_LANG → `Section::RESET`, reinicia), VSync, integer
scale, info; más PostFX/shader/preset. **F10/RESET** pone `Section::name =
RESET` (reinicio) y **ESC/EXIT** pone `QUIT`.
### 5.3. Cómo llega la entrada a un jugador
Dentro de `Game`, `checkInput()``handlePlayersInput()`
`handleNormalPlayerInput(player)` consulta `Input` y traduce a
`player->setInput(Input::Action)` y disparos (`handleFireInput`). El `Player`
sabe si usa teclado o un mando concreto (`uses_keyboard_`, `gamepad_`;
`player.hpp:217`). En modo demo, esta misma vía se alimenta de datos grabados
(§8).
---
## 6. Lógica del juego: la clase `Game`
`source/game/scenes/game.{hpp,cpp}` es la sección de gameplay y la clase más
grande del proyecto. Coordina jugadores, globos, balas, ítems, fases,
puntuación y efectos.
### 6.1. FSM de la partida
`Game::State` (`game.hpp:75`): `FADE_IN → ENTERING_PLAYER →
SHOWING_GET_READY_MESSAGE → PLAYING → COMPLETED / GAME_OVER`. `checkState()`
(`game.cpp`) detecta fin de juego (`stage_manager_->isGameCompleted()`) o game
over (`allPlayersAreGameOver()`); `setState()` hace la transición.
### 6.2. El frame de `Game`
```cpp
void Game::iterate() { // game.cpp
const float DELTA_TIME = calculateDeltaTime();
checkInput();
update(DELTA_TIME);
render();
}
void Game::update(float dt) {
screen_->update(dt);
Audio::update();
updateDemo(dt); // no-op si no es demo
updateGameStates(dt); // dispatch por State
fillCanvas(); // dibuja la zona de juego en canvas_
}
```
`updateGameStates` despacha a un `updateGameState<Estado>` por cada valor de la
FSM (`game.hpp:218`).
### 6.3. Sistemas que orquesta
`Game` posee (vía `unique_ptr`) los managers y objetos del nivel
(`game.hpp:136`):
- **`StageManager`** — progresión por "poder": cada fase necesita
`power_to_complete`; el juego se completa al acumular el poder total. También
define umbrales de **amenaza** (`menace`) por fase. Implementa `IStageInfo`
(`stage.hpp:51`), interfaz mínima que se inyecta a jugador y globos.
- **`BalloonManager`** — despliega formaciones (`deployRandomFormation`),
globos hijos al explotar uno, *power balls*, ajusta velocidad
(`Balloon::GAME_TEMPO`) y calcula la amenaza en pantalla (§7).
- **`BulletManager`** — pool de balas; las colisiones se resuelven con
**callbacks** que registra `Game` (`setBalloonCollisionCallback`,
`setTabeCollisionCallback`, `setOutOfBoundsCallback`; `bullet_manager.hpp:49`),
manteniendo la lógica de juego dentro de `Game`.
- **`Background`**, **`Fade` ×2**, **`Tabe`** (enemigo especial volador),
**`Scoreboard`**, **`PauseManager`**.
- Listas de `Item` (power-ups y puntos), `SmartSprite` y `PathSprite`.
### 6.4. Mecánicas destacadas
- **Ítems / power-ups** (`game.hpp:268`): café (toque extra),
máquina de café (power-up), *power ball*, reloj (= **detener el tiempo**,
`enableTimeStopItem`), e ítems de puntos. La probabilidad de soltar ítem la
decide `dropItem()` con *odds* configurables (`Helper`, `game.hpp:100`).
- **Sistema de amenaza** (`updateMenace`/`setMenace`): si la amenaza cae por
debajo de un umbral, se generan más globos.
- **Multijugador**: `players_` es un `vector<shared_ptr<Player>>` (1 o 2). Un
`players_draw_list_` separado mantiene el z-order de dibujo
(`buildPlayerDrawList`, `sendPlayerToBack`/`bringPlayerToFront`;
`game.hpp:338`).
- **Récords**: al perder con buena puntuación, el jugador entra en
`ENTERING_NAME` (`enter_name.*`, `manage_hiscore_table.*`).
---
## 7. Entidades y managers de gameplay
`source/game/entities/`:
- **`Player`** (`player.hpp`) — la entidad más compleja. Hereda de
`AnimatedSprite`. Tiene **tres ejes de estado** independientes:
`walking_state_`, `firing_state_` y `playing_state_` (`player.hpp:266`). El
disparo usa un **sistema de dos líneas**: una funcional (cooldown, `canFire`)
y otra visual (animaciones `NORMAL→AIMING→RECOILING→THREAT_POSE`,
`player.hpp:296`). Soporta power-up, invulnerabilidad con parpadeo, *continue*
y entrada de nombre. Puede controlarse por teclado o por un `Gamepad`
concreto.
- **`Balloon`** (`balloon.hpp`) — el enemigo básico; al explotar puede crear
globos hijos. `Balloon::GAME_TEMPO` define velocidades por progreso.
- **`Bullet`** (`bullet.hpp`) — proyectil con tipo (UP/LEFT/RIGHT) y color
(según power-up del jugador).
- **`Item`** — power-ups y objetos de puntos que caen.
- **`Explosions`** — efectos de explosión (lo posee `BalloonManager`).
- **`Tabe`** — enemigo/objeto especial volador con su propia lógica de impacto.
`source/game/gameplay/` contiene los **managers y sistemas** (no entidades en
sí): `BalloonManager`, `BulletManager`, `StageManager`, `Difficulty`,
`Scoreboard`, `balloon_formations` (lee `formations.txt`), `cooldown`,
`enter_name`, `manage_hiscore_table`, `game_logo`.
El patrón general aquí **no es un `EntityManager` polimórfico único** (como en
el proyecto hermano), sino **managers especializados por tipo** + listas
homogéneas en `Game`, con **callbacks** para cruzar responsabilidades sin
acoplar (caso de `BulletManager`).
---
## 8. Modo demo y attract mode
> **A diferencia de muchos juegos, aquí el modo demo SÍ existe — pero NO es
> IA.** Es **reproducción de input pregrabado**.
### 8.1. Formato de los datos
`source/core/system/demo.hpp`: cada fotograma de demo es un `DemoKeys` con seis
banderas (`left`, `right`, `no_input`, `fire`, `fire_left`, `fire_right`). Una
demo es un `vector<DemoKeys>` de `TOTAL_DEMO_DATA = 2000` fotogramas. Hay tres
ficheros: `data/demo/demo{1,2,3}.bin`, leídos por `loadDemoDataFromFile`
(`demo.cpp:11`).
### 8.2. Reproducción
Cuando `Game` se construye con `DEMO_ON` (sección `GAME_DEMO`), `initDemo()`
(`game.cpp`) carga todas las demos del `Asset` y **asigna una a cada jugador de
forma aleatoria** (`shuffle`), elige una fase de inicio al azar, da cafés
aleatorios, pone los marcadores en modo `DEMO` y silencia los globos. Luego cada
frame, `demoHandlePlayerInput()` lee el fotograma actual y lo inyecta por la
**misma vía de input que un humano**:
```cpp
// game.cpp — demoHandlePlayerInput
const auto& demo_data = demo_data_vec.at(demo_.index % demo_data_vec.size());
if (demo_data.left == 1) player->setInput(Input::Action::LEFT);
else if (demo_data.right == 1) player->setInput(Input::Action::RIGHT);
...
if (demo_data.fire == 1) handleFireInput(player, Bullet::Type::UP);
```
No hay toma de decisiones: el jugador "demo" repite exactamente las pulsaciones
grabadas. `demoHandlePassInput()` permite **salir** con cualquier botón,
volviendo a `TITLE` y dejando armado el siguiente attract (`TITLE_TO_DEMO`).
### 8.3. Attract mode
El Title alterna `TITLE_TO_DEMO``TITLE_TO_LOGO` (`title.cpp:51`): tras su
*timeout*, una vez lanza la demo y la siguiente vuelve al logo, en bucle de
atracción típico de recreativa.
### 8.4. Grabación y autoplay
- **`#ifdef RECORDING`** (`demo.cpp:43`, `game.hpp:349`): build especial que
arranca directamente en GAME y **graba** las pulsaciones a un `.bin` con
`saveDemoFile`. Así se generan las demos.
- **`autoplay`** (debug): en `_DEBUG`, `debug_config.autoplay` permite alimentar
datos de demo a los jugadores en una partida normal (`game.hpp:354`,
`initDemo`).
---
## 9. Recursos
### 9.1. `Resource` (singleton)
`source/core/resources/resource.hpp`. Cachea por nombre texturas, música,
sonidos, ficheros de texto/fuentes, animaciones y **datos de demo**
(`getDemoData`). Dos modos (`LoadingMode`):
- **`PRELOAD`** — carga incremental al arranque con `beginLoad()` +
`loadStep(budget_ms)` + `renderProgress()` (la sección Preload pinta la
barra). Es el modo por defecto.
- **`LAZY_LOAD`** — carga bajo demanda (seleccionable vía `debug.yaml`).
### 9.2. `Asset` y el pack
`source/core/resources/asset.*` mantiene el **índice de ficheros** a partir del
manifiesto `data/config/assets.txt` (`director.cpp:296`) y valida que no falte
ninguno (`Asset::check()`). La lectura física pasa por
`ResourceHelper::loadFile`, que sirve desde **`resources.pack`** y hace
*fallback* al filesystem en builds de desarrollo (en Release nativo es estricto:
solo el pack; `director.cpp:143`). `resource_pack.*` y `resource_loader.*`
implementan el formato del pack y la carga.
Formatos: imágenes vía `stb_image` + `gif` propio; audio `.ogg` vía
`stb_vorbis`; texto/JSON vía `nlohmann/json`; configs vía `fkyaml`.
---
## 10. Audio
`source/core/audio/audio.hpp``Audio` singleton sobre `jail_audio`
(primer-party, no librería externa) y `audio_adapter`. API: `playMusic(name,
loop, crossfade_ms)`, `playSound(name, Group)`, control de volumen por **grupos**
(`Group::GAME`, `INTERFACE`, …), `pause/resume/stop`, *crossfade* y *fade out*
(`audio.hpp:49`). `Audio::update()` se llama cada frame desde las secciones
(p.ej. `Game::update`). El build `NO_AUDIO` compila sin audio.
---
## 11. Configuración, parámetros y constantes
El proyecto tiene **tres capas de configuración** que conviene no confundir:
1. **`Options`** (`source/game/options.hpp`) — opciones persistentes del
usuario, agrupadas en structs (`Window`, `Video`, `Audio`, `Settings`,
`Gamepad`, `Keyboard`) más presets de shaders (`PostFXPreset`,
`CrtPiPreset`). Se guardan en `config.yaml` / `controllers.json` /
`postfx.yaml` / `crtpi.yaml`. Incluye `params_preset` ∈ {`classic`,
`arcade`, `red`} (`options.hpp:339`).
2. **`param` / `Param`** (`source/utils/param.hpp`) — parámetros de *gameplay y
layout* (dimensiones del juego, `play_area`, ajustes de globos, scoreboard,
título, fades, jugador, Tabe…). Se cargan de ficheros de texto
`param_320x240.txt` / `param_320x256.txt` / `classic.txt` con
`loadParamsFromFile` (`director.cpp:275`). El objeto global `param` lo leen
casi todos los subsistemas.
3. **Otros datos de juego**: `stages.txt` (fases, lo lee `StageManager`),
`formations.txt` (formaciones de globos), `assets.txt` (manifiesto).
Los **valores de fallback compilados** viven en
`source/core/system/defaults.hpp` (y `source/utils/defines.hpp` para constantes
base). En Debug, `debug.yaml` controla sección inicial, opciones, fase, modo de
carga, `autoplay` e `invincibility` (`director.cpp:318`).
### Builds condicionales (multiplataforma)
El juego se adapta por *defines* (ver `CLAUDE.md`): `WINDOWS_BUILD` /
`LINUX_BUILD` / `MACOS_BUILD`, `RELEASE_BUILD`, `MACOS_BUNDLE`, `ANBERNIC`
(handheld), `__EMSCRIPTEN__` (web), `NO_SHADERS`, `NO_AUDIO`, `RECORDING`,
`ARCADE`, `DEBUG`/`VERBOSE`. Aparecen sobre todo en `Director::init` y `Screen`.
---
## 12. Localización
`source/core/locale/lang.*``Lang` carga el idioma desde **JSON** en
`data/lang/`. `Lang::setLanguage(Options::settings.language)` se llama en
`init()` y en cada `reset()`. Cambiar idioma en caliente (F9 / `CHANGE_LANG`)
fuerza un `Section::RESET` (reinicio del proceso) para recargarlo todo
(`global_inputs.cpp`).
---
## 13. Convenciones y patrones recurrentes
- **Naming** (de `.clang-format`/`.clang-tidy`, resumido en `CLAUDE.md`):
clases `CamelCase`, métodos `camelBack`, variables/params `snake_case`,
miembros privados `snake_case_` (guion bajo final), constantes `UPPER_CASE`,
namespaces `CamelCase`, valores de enum `UPPER_CASE`. clang-tidy trata los
warnings como errores.
- **Singletons con init/destroy manual**: `Screen`, `Resource`, `Audio`,
`Input`, `Asset`, `ServiceMenu`, `Notifier`, `Lang`. Se crean/destruyen en
orden explícito en `Director::init` / `shutdownSubsystems` (`director.cpp:227`)
— no se confía en destructores estáticos.
- **Estado de flujo por variable global** (`Section::name`): patrón central; no
hay objetos de transición tipados.
- **Time-based en todo**: cada sistema recibe `delta_time`; las constantes de
tiempo se documentan como "N frames a 60fps → segundos" (p.ej.
`game.hpp:85`).
- **Callbacks para desacoplar**: `BulletManager` y `StageManager` reciben
`std::function` desde `Game` para no conocer la lógica de colisión/puntuación.
- **Interfaz mínima `IStageInfo`** (`stage_interface.hpp`): se inyecta a
jugador y globos para que consulten amenaza/poder sin ver todo el
`StageManager`.
- **Managers por tipo + listas homogéneas** en `Game`, en vez de un contenedor
de entidades polimórfico.
- **Comentarios** en español/valenciano; muchos `#include` llevan comentario
"// Para X" indicando qué símbolo justifican (estilo IWYU).
- **Multiplataforma por `#ifdef`** (ver §11). Conviene leerlos al tocar
arranque o render.
---
## 14. Guía de navegación: "si quieres tocar X, mira Y"
| Quiero… | Empieza por… |
|---|---|
| Entender el arranque | `source/core/system/director.cpp` (`init`, `iterate`) |
| Cambiar el flujo de pantallas | `source/core/system/section.hpp` + `handleSectionTransition` |
| Añadir/editar una pantalla | `source/game/scenes/` (crea `iterate()`+`handleEvent()`) y un `case` en `director.cpp` |
| La barra de carga / arranque lento | `Resource::beginLoad/loadStep` + `scenes/preload.*` |
| Cómo se dibuja todo | `Game::fillCanvas` + `Game::render` + `Screen::renderPresent` (`screen.cpp:156`) |
| Sprites / animaciones | `source/core/rendering/sprite/` + `texture.hpp` |
| Shaders / CRT / post-FX | `source/core/rendering/sdl3gpu/` + `data/shaders/` + `Options` presets |
| Efectos (shake/flash/fade) | `screen.hpp` (structs) + `core/rendering/fade.*` |
| Controles / mandos | `source/core/input/input.hpp` (`Keyboard`/`Gamepad`/`checkAction`) |
| Hotkeys F1F12 / reset | `source/core/input/global_inputs.cpp` |
| Eventos globales / hot-plug | `source/core/system/global_events.cpp` |
| Lógica de partida y estados | `source/game/scenes/game.cpp` (`updateGameState*`) |
| Globos y formaciones | `gameplay/balloon_manager.*` + `entities/balloon.*` + `formations.txt` |
| Balas y colisiones | `gameplay/bullet_manager.*` (callbacks) + `entities/bullet.*` |
| Fases / progresión / dificultad | `gameplay/stage.*` (`StageManager`) + `stages.txt` + `gameplay/difficulty.*` |
| El jugador (movimiento, disparo) | `entities/player.*` (sistema de disparo de dos líneas) |
| Ítems y power-ups | `entities/item.*` + `Game::dropItem/createItem` |
| Récords / entrada de nombre | `gameplay/manage_hiscore_table.*`, `gameplay/enter_name.*` |
| **Modo demo / attract** | `core/system/demo.*`, `Game::initDemo/demoHandle*`, `scenes/title.cpp` |
| Grabar nuevas demos | build con `RECORDING` + `Game::updateRecording` |
| Cargar un recurso | `core/resources/resource.hpp` + `asset.*` + `assets.txt` |
| Audio (música/SFX) | `core/audio/audio.hpp` |
| Opciones del usuario | `game/options.hpp` (+ `config.yaml`) |
| Parámetros de gameplay/layout | `utils/param.hpp` + `param_320x*.txt` |
| Valores por defecto | `core/system/defaults.hpp`, `utils/defines.hpp` |
| Idiomas | `core/locale/lang.*` + `data/lang/` |
| Menú de servicio / notificaciones | `game/ui/service_menu.*`, `game/ui/notifier.*` |
| Empaquetar datos | `tools/` (`pack_resources`) + `make resources.pack` |
---
*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.*