afig guia d'arquitectura del projecte
This commit is contained in:
@@ -0,0 +1,642 @@
|
||||
# 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;
|
||||
**F1–F11** = 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 F1–F12 / 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.*
|
||||
Reference in New Issue
Block a user