Compare commits

...

9 Commits

31 changed files with 819 additions and 149 deletions
+2 -2
View File
@@ -232,7 +232,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
# --- 4. CONFIGURACIÓN PLATAFORMAS Y COMPILADOR ---
# Configuración de flags de compilación
target_compile_options(${PROJECT_NAME} PRIVATE -Wall)
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:-Os -ffunction-sections -fdata-sections>)
# Definir _DEBUG en modo Debug
@@ -393,7 +393,7 @@ if(NOT EMSCRIPTEN)
source/core/resources/resource_pack.cpp
)
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
target_compile_options(pack_resources PRIVATE -Wall)
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
# Regeneració automàtica de resources.pack en cada build si canvia data/.
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
+642
View File
@@ -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;
**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.*
+5
View File
@@ -8,8 +8,13 @@
// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp).
// clang-format off
#undef STB_VORBIS_HEADER_ONLY
// stb_vorbis.c (codi de tercers) dispara -Wtautological-compare; el silenciem
// només per a aquesta inclusió sense afectar el nostre codi.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wtautological-compare"
// NOLINTNEXTLINE(bugprone-suspicious-include) — stb_vorbis és single-file: el TU principal inclou el .c per portar la implementació.
#include "external/stb_vorbis.c"
#pragma GCC diagnostic pop
// stb_vorbis.c filtra les macros L, C i R (i PLAYBACK_*) al TU. Les netegem
// perquè xocarien amb noms de paràmetres de plantilla en altres headers.
#undef L
+2 -2
View File
@@ -64,7 +64,7 @@ class GamepadConfigManager {
// Escribir al archivo
std::ofstream file(filename);
if (!file.is_open()) {
return false; // NOLINT(readability-simplify-boolean-expr)
return false;
}
file << j.dump(4); // Formato con indentación de 4 espacios
@@ -92,7 +92,7 @@ class GamepadConfigManager {
configs.clear();
if (!j.contains("gamepads") || !j["gamepads"].is_array()) {
return false; // NOLINT(readability-simplify-boolean-expr)
return false;
}
for (const auto& gamepad_json : j["gamepads"]) {
+2 -2
View File
@@ -20,11 +20,11 @@ class PauseManager {
// --- Operadores friend ---
friend auto operator|(Source a, Source b) -> Source {
return static_cast<Source>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b)); // NOLINT(readability-redundant-casting)
return static_cast<Source>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
}
friend auto operator&(Source a, Source b) -> Source {
return static_cast<Source>(static_cast<uint8_t>(a) & static_cast<uint8_t>(b)); // NOLINT(readability-redundant-casting)
return static_cast<Source>(static_cast<uint8_t>(a) & static_cast<uint8_t>(b));
}
friend auto operator~(Source a) -> uint8_t {
+4 -4
View File
@@ -83,11 +83,11 @@ void Background::initializeSprites() {
const float TOP_CLOUDS_Y = base_ - 165;
const float BOTTOM_CLOUDS_Y = base_ - 101;
top_clouds_sprite_a_ = std::make_unique<MovingSprite>(top_clouds_texture_, (SDL_FRect){.x = 0, .y = TOP_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(top_clouds_texture_->getHeight())});
top_clouds_sprite_b_ = std::make_unique<MovingSprite>(top_clouds_texture_, (SDL_FRect){.x = rect_.w, .y = TOP_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(top_clouds_texture_->getHeight())});
top_clouds_sprite_a_ = std::make_unique<MovingSprite>(top_clouds_texture_, SDL_FRect{.x = 0, .y = TOP_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(top_clouds_texture_->getHeight())});
top_clouds_sprite_b_ = std::make_unique<MovingSprite>(top_clouds_texture_, SDL_FRect{.x = rect_.w, .y = TOP_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(top_clouds_texture_->getHeight())});
bottom_clouds_sprite_a_ = std::make_unique<MovingSprite>(bottom_clouds_texture_, (SDL_FRect){.x = 0, .y = BOTTOM_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(bottom_clouds_texture_->getHeight())});
bottom_clouds_sprite_b_ = std::make_unique<MovingSprite>(bottom_clouds_texture_, (SDL_FRect){.x = rect_.w, .y = BOTTOM_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(bottom_clouds_texture_->getHeight())});
bottom_clouds_sprite_a_ = std::make_unique<MovingSprite>(bottom_clouds_texture_, SDL_FRect{.x = 0, .y = BOTTOM_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(bottom_clouds_texture_->getHeight())});
bottom_clouds_sprite_b_ = std::make_unique<MovingSprite>(bottom_clouds_texture_, SDL_FRect{.x = rect_.w, .y = BOTTOM_CLOUDS_Y, .w = rect_.w, .h = static_cast<float>(bottom_clouds_texture_->getHeight())});
buildings_sprite_ = std::make_unique<Sprite>(buildings_texture_);
gradient_sprite_ = std::make_unique<Sprite>(gradients_texture_, 0, 0, rect_.w, rect_.h);
+1 -1
View File
@@ -68,7 +68,7 @@ void Fade::render() {
}
// Actualiza las variables internas
void Fade::update(float delta_time) {
void Fade::update(float /*delta_time*/) {
switch (state_) {
case State::PRE:
updatePreState();
+1 -1
View File
@@ -141,7 +141,7 @@ namespace GIF {
}
// Procesa el Image Descriptor y retorna el vector de datos sin comprimir.
auto processImageDescriptor(const uint8_t *&buffer, const std::vector<RGB> &gct, int resolution_bits) -> std::vector<uint8_t> {
auto processImageDescriptor(const uint8_t *&buffer, const std::vector<RGB> & /*gct*/, int /*resolution_bits*/) -> std::vector<uint8_t> {
ImageDescriptor image_descriptor;
readBytes(buffer, &image_descriptor, sizeof(ImageDescriptor));
+1 -1
View File
@@ -218,7 +218,7 @@ void Screen::handleCanvasResized() {
// Registra los callbacks nativos de Emscripten que restauran el canvas cuando
// SDL3 no emite los events equivalentes. Fuera de Emscripten es un no-op.
void Screen::registerEmscriptenEventCallbacks() { // NOLINT(readability-convert-member-functions-to-static)
void Screen::registerEmscriptenEventCallbacks() {
#ifdef __EMSCRIPTEN__
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
@@ -429,7 +429,7 @@ namespace Rendering {
return shader;
}
auto SDL3GPUShader::createShaderSPIRV(SDL_GPUDevice* device, // NOLINT(readability-convert-member-functions-to-static)
auto SDL3GPUShader::createShaderSPIRV(SDL_GPUDevice* device,
const uint8_t* spv_code,
size_t spv_size,
const char* entrypoint,
@@ -72,7 +72,7 @@ namespace Rendering {
const std::string& fragment_source) -> bool override;
void render() override;
void setTextureSize(float width, float height) override {}
void setTextureSize(float /*width*/, float /*height*/) override {}
void cleanup() final;
void destroy();
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
@@ -117,8 +117,40 @@ namespace Rendering {
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
SDL_GPUSampler* sampler_ = nullptr;
PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_min = 0.15F, .scanline_strength = 0.7F, .screen_height = 192.0F, .pixel_scale = 1.0F, .chroma_max = 0.15F, .scan_dark_ratio = 0.333F, .scan_dark_floor = 0.42F, .scan_edge_soft = 1.0F};
CrtPiUniforms crtpi_uniforms_{.scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = 1, .enable_multisample = 1, .enable_gamma = 1};
PostFXUniforms uniforms_{
.vignette_strength = 0.6F,
.chroma_min = 0.15F,
.scanline_strength = 0.7F,
.screen_height = 192.0F,
.mask_strength = 0.0F,
.gamma_strength = 0.0F,
.curvature = 0.0F,
.bleeding = 0.0F,
.pixel_scale = 1.0F,
.time = 0.0F,
.flicker = 0.0F,
.chroma_max = 0.15F,
.scan_dark_ratio = 0.333F,
.scan_dark_floor = 0.42F,
.scan_edge_soft = 1.0F,
.pad3 = 0.0F};
CrtPiUniforms crtpi_uniforms_{
.scanline_weight = 6.0F,
.scanline_gap_brightness = 0.12F,
.bloom_factor = 3.5F,
.input_gamma = 2.4F,
.output_gamma = 2.2F,
.mask_brightness = 0.80F,
.curvature_x = 0.05F,
.curvature_y = 0.10F,
.mask_type = 2,
.enable_scanlines = 1,
.enable_multisample = 1,
.enable_gamma = 1,
.enable_curvature = 0,
.enable_sharper = 0,
.texture_width = 0.0F,
.texture_height = 0.0F};
ShaderType active_shader_ = ShaderType::POSTFX;
int game_width_ = 0;
+3 -3
View File
@@ -8,13 +8,13 @@
// Constructor
Sprite::Sprite(std::shared_ptr<Texture> texture, float pos_x, float pos_y, float width, float height)
: textures_{std::move(texture)},
pos_((SDL_FRect){.x = pos_x, .y = pos_y, .w = width, .h = height}),
sprite_clip_((SDL_FRect){.x = 0, .y = 0, .w = pos_.w, .h = pos_.h}) {}
pos_(SDL_FRect{.x = pos_x, .y = pos_y, .w = width, .h = height}),
sprite_clip_(SDL_FRect{.x = 0, .y = 0, .w = pos_.w, .h = pos_.h}) {}
Sprite::Sprite(std::shared_ptr<Texture> texture, SDL_FRect rect)
: textures_{std::move(texture)},
pos_(rect),
sprite_clip_((SDL_FRect){.x = 0, .y = 0, .w = pos_.w, .h = pos_.h}) {}
sprite_clip_(SDL_FRect{.x = 0, .y = 0, .w = pos_.w, .h = pos_.h}) {}
Sprite::Sprite(std::shared_ptr<Texture> texture)
: textures_{std::move(texture)},
+8 -8
View File
@@ -32,7 +32,7 @@ Text::Text(const std::shared_ptr<Texture>& texture, const std::string& text_file
}
// Crea los objetos
sprite_ = std::make_unique<Sprite>(texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
sprite_ = std::make_unique<Sprite>(texture, SDL_FRect{.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
// Inicializa variables
fixed_width_ = false;
@@ -50,7 +50,7 @@ Text::Text(const std::shared_ptr<Texture>& texture, const std::shared_ptr<Text::
}
// Crea los objetos
sprite_ = std::make_unique<Sprite>(texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
sprite_ = std::make_unique<Sprite>(texture, SDL_FRect{.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
// Inicializa variables
fixed_width_ = false;
@@ -71,8 +71,8 @@ Text::Text(const std::shared_ptr<Texture>& texture, const std::shared_ptr<Textur
}
// Crea los objetos
sprite_ = std::make_unique<Sprite>(texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
white_sprite_ = std::make_unique<Sprite>(white_texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
sprite_ = std::make_unique<Sprite>(texture, SDL_FRect{.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
white_sprite_ = std::make_unique<Sprite>(white_texture, SDL_FRect{.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
// Inicializa variables
fixed_width_ = false;
@@ -90,8 +90,8 @@ Text::Text(const std::shared_ptr<Texture>& texture, const std::shared_ptr<Textur
}
// Crea los objetos
sprite_ = std::make_unique<Sprite>(texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
white_sprite_ = std::make_unique<Sprite>(white_texture, (SDL_FRect){.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
sprite_ = std::make_unique<Sprite>(texture, SDL_FRect{.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
white_sprite_ = std::make_unique<Sprite>(white_texture, SDL_FRect{.x = 0, .y = 0, .w = static_cast<float>(box_width_), .h = static_cast<float>(box_height_)});
// Inicializa variables
fixed_width_ = false;
@@ -137,7 +137,7 @@ void Text::write2X(int x, int y, const std::string& text, int kerning, int lengt
}
// Escribe el texto en una textura
auto Text::writeToTexture(const std::string& text, int zoom, int kerning, int length) -> std::shared_ptr<Texture> {
auto Text::writeToTexture(const std::string& text, int zoom, int kerning, int /*length*/) -> std::shared_ptr<Texture> {
auto* renderer = Screen::get()->getRenderer();
auto texture = std::make_shared<Texture>(renderer);
auto width = Text::length(text, kerning) * zoom;
@@ -342,7 +342,7 @@ void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerni
}
// Escribe texto a partir de un TextStyle
void Text::writeStyle(int x, int y, const std::string& text, const Style& style, int length) {
void Text::writeStyle(int x, int y, const std::string& text, const Style& style, int /*length*/) {
writeDX(style.flags, x, y, text, style.kerning, style.text_color, style.shadow_distance, style.shadow_color);
}
+1 -7
View File
@@ -4,7 +4,6 @@
#include <SDL3/SDL.h> // Para SDL_LogError, SDL_LogCategory, Uint8, SDL_...
#include <cstdint> // Para uint32_t
#include <cstring> // Para memcpy
#include <fstream> // Para basic_ifstream, basic_istream, basic_ios
#include <iostream> // Para std::cout
#include <sstream> // Para basic_istringstream
@@ -259,12 +258,7 @@ auto Texture::loadSurface(const std::string& file_path) -> std::shared_ptr<Surfa
return nullptr;
}
// Si el constructor de Surface espera un std::shared_ptr<Uint8[]>:
size_t pixel_count = raw_pixels.size();
auto pixels = std::shared_ptr<Uint8[]>(new Uint8[pixel_count], std::default_delete<Uint8[]>()); // NOLINT(modernize-avoid-c-arrays)
std::memcpy(pixels.get(), raw_pixels.data(), pixel_count);
auto surface = std::make_shared<Surface>(w, h, pixels);
auto surface = std::make_shared<Surface>(w, h, std::move(raw_pixels));
// Actualizar las dimensiones
width_ = w;
+2 -2
View File
@@ -16,11 +16,11 @@ using Palette = std::array<Uint32, 256>;
// Definición de Surface para imágenes con paleta
struct Surface {
std::shared_ptr<Uint8[]> data; // NOLINT(modernize-avoid-c-arrays)
std::vector<Uint8> data;
Uint16 w, h;
// Constructor
Surface(Uint16 width, Uint16 height, std::shared_ptr<Uint8[]> pixels) // NOLINT(modernize-avoid-c-arrays)
Surface(Uint16 width, Uint16 height, std::vector<Uint8> pixels)
: data(std::move(pixels)),
w(width),
h(height) {}
+1 -1
View File
@@ -50,7 +50,7 @@ TiledBG::~TiledBG() {
// Rellena la textura con el contenido
void TiledBG::fillTexture() {
// Crea los objetos para pintar en la textura de fondo
auto tile = std::make_unique<Sprite>(Resource::get()->getTexture("title_bg_tile.png"), (SDL_FRect){.x = 0, .y = 0, .w = TILE_WIDTH, .h = TILE_HEIGHT});
auto tile = std::make_unique<Sprite>(Resource::get()->getTexture("title_bg_tile.png"), SDL_FRect{.x = 0, .y = 0, .w = TILE_WIDTH, .h = TILE_HEIGHT});
// Prepara para dibujar sobre la textura
auto* temp = SDL_GetRenderTarget(renderer_);
+1 -1
View File
@@ -155,7 +155,7 @@ auto ResourcePack::addFile(const std::string& filename, const std::string& filep
auto ResourcePack::addDirectory(const std::string& directory) -> bool {
if (!std::filesystem::exists(directory)) {
std::cerr << "Error: Directory does not exist: " << directory << '\n';
return false; // NOLINT(readability-simplify-boolean-expr)
return false;
}
return std::ranges::all_of(std::filesystem::recursive_directory_iterator(directory),
+1 -1
View File
@@ -50,7 +50,7 @@ namespace SystemShutdown {
return shutdownSystem(config);
}
auto shutdownSystem(const ShutdownConfig& config) -> ShutdownResult {
auto shutdownSystem([[maybe_unused]] const ShutdownConfig& config) -> ShutdownResult {
#ifdef _WIN32
// Windows: Usar CreateProcess
STARTUPINFOA si = {0};
+2 -2
View File
@@ -22,7 +22,7 @@ namespace SystemUtils {
// Función auxiliar para crear una carpeta individual
auto createSingleFolder(const std::string& path, int permissions) -> Result {
struct stat st = {.st_dev = 0};
struct stat st{};
// Verificar si ya existe
if (stat(path.c_str(), &st) == 0) {
@@ -131,7 +131,7 @@ namespace SystemUtils {
}
auto folderExists(const std::string& path) -> bool {
struct stat st = {.st_dev = 0};
struct stat st{};
return (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode));
}
+1 -1
View File
@@ -890,7 +890,7 @@ void Player::shiftColliders() {
}
// Pone las texturas del jugador
void Player::setPlayerTextures(const std::vector<std::shared_ptr<Texture>>& texture) { // NOLINT(readability-named-parameter)
void Player::setPlayerTextures(const std::vector<std::shared_ptr<Texture>>& texture) {
player_sprite_->setTexture(texture[0]);
power_sprite_->setTexture(texture[1]);
}
+1 -1
View File
@@ -132,7 +132,7 @@ class Player {
void setAnimation(float delta_time); // Establece la animación según el estado (time-based)
// --- Texturas y animaciones ---
void setPlayerTextures(const std::vector<std::shared_ptr<Texture>>& texture); // NOLINT(readability-avoid-const-params-in-decls) Cambia las texturas del jugador
void setPlayerTextures(const std::vector<std::shared_ptr<Texture>>& texture);
// --- Gameplay: Puntuación y power-ups ---
void addScore(int score, int lowest_hi_score_entry); // Añade puntos
+14 -4
View File
@@ -114,7 +114,9 @@ void BalloonManager::deployRandomFormation(int stage) {
.size = balloon.size,
.vel_x = balloon.vel_x,
.game_tempo = balloon_speed_,
.creation_counter = creation_time_enabled_ ? balloon.creation_counter : 0.0F};
.creation_counter = creation_time_enabled_ ? balloon.creation_counter : 0.0F,
.animation = {},
.sound = {}};
createBalloon(config);
}
@@ -135,7 +137,9 @@ void BalloonManager::deployFormation(int formation_id) {
.size = balloon.size,
.vel_x = balloon.vel_x,
.game_tempo = balloon_speed_,
.creation_counter = balloon.creation_counter};
.creation_counter = balloon.creation_counter,
.animation = {},
.sound = {}};
createBalloon(config);
}
}
@@ -151,7 +155,9 @@ void BalloonManager::deployFormation(int formation_id, float y) {
.size = balloon.size,
.vel_x = balloon.vel_x,
.game_tempo = balloon_speed_,
.creation_counter = balloon.creation_counter};
.creation_counter = balloon.creation_counter,
.animation = {},
.sound = {}};
createBalloon(config);
}
}
@@ -213,7 +219,9 @@ void BalloonManager::createChildBalloon(const std::shared_ptr<Balloon>& parent_b
.size = static_cast<Balloon::Size>(static_cast<int>(parent_balloon->getSize()) - 1),
.vel_x = direction == "LEFT" ? Balloon::VELX_NEGATIVE : Balloon::VELX_POSITIVE,
.game_tempo = balloon_speed_,
.creation_counter = 0};
.creation_counter = 0,
.animation = {},
.sound = {}};
// Crea el globo hijo
auto child_balloon = createBalloon(config);
@@ -267,6 +275,8 @@ void BalloonManager::createPowerBall() {
.texture = balloon_textures_.at(4),
.animation = balloon_animations_.at(4),
.sound = {
.bouncing_file = {},
.popping_file = {},
.bouncing_enabled = bouncing_sound_enabled_,
.poping_enabled = poping_sound_enabled_,
.enabled = sound_enabled_}};
+4 -4
View File
@@ -679,7 +679,7 @@ namespace Options {
// --- PRIMERA PASADA: Intenta asignar mandos basándose en la ruta guardada ---
void GamepadManager::assignGamepadsByPath(
const std::array<std::string, MAX_PLAYERS>& desired_paths,
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-named-parameter)
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads,
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) {
for (size_t i = 0; i < MAX_PLAYERS; ++i) {
const std::string& desired_path = desired_paths[i];
@@ -705,7 +705,7 @@ namespace Options {
// refrescamos el path guardado al del dispositivo físico actual.
void GamepadManager::assignGamepadsByName(
const std::array<std::string, MAX_PLAYERS>& desired_names,
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-named-parameter)
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads,
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) {
for (size_t i = 0; i < MAX_PLAYERS; ++i) {
if (gamepads_[i].instance != nullptr) {
@@ -731,7 +731,7 @@ namespace Options {
// --- TERCERA PASADA: Asigna los mandos físicos restantes a los jugadores libres ---
void GamepadManager::assignRemainingGamepads(
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-named-parameter)
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads,
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) {
for (size_t i = 0; i < MAX_PLAYERS; ++i) {
if (gamepads_[i].instance != nullptr) {
@@ -763,7 +763,7 @@ namespace Options {
auto GamepadManager::isGamepadAssigned(
const std::shared_ptr<Input::Gamepad>& physical_gamepad,
const std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) -> bool { // NOLINT(readability-named-parameter)
const std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) -> bool {
return std::ranges::any_of(assigned_instances,
[&physical_gamepad](const auto& assigned) -> auto {
return assigned == physical_gamepad;
+5 -5
View File
@@ -262,7 +262,7 @@ namespace Options {
void clearPlayers() { players_.clear(); } // Limpia la lista de jugadores
// Asigna el mando a un jugador
void assignTo(const Input::Gamepad& gamepad, Player::Id player_id) {
void assignTo(const Input::Gamepad& /*gamepad*/, Player::Id /*player_id*/) {
}
// Asigna los mandos físicos basándose en la configuración actual de nombres.
@@ -296,19 +296,19 @@ namespace Options {
void assignGamepadsByPath(
const std::array<std::string, MAX_PLAYERS>& desired_paths,
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-avoid-const-params-in-decls)
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads,
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances);
void assignGamepadsByName(
const std::array<std::string, MAX_PLAYERS>& desired_names,
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-avoid-const-params-in-decls)
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads,
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances);
void assignRemainingGamepads(
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads, // NOLINT(readability-avoid-const-params-in-decls)
const std::vector<std::shared_ptr<Input::Gamepad>>& physical_gamepads,
std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances);
void clearUnassignedGamepadSlots();
[[nodiscard]] static auto isGamepadAssigned(
const std::shared_ptr<Input::Gamepad>& physical_gamepad,
const std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) -> bool; // NOLINT(readability-avoid-const-params-in-decls)
const std::vector<std::shared_ptr<Input::Gamepad>>& assigned_instances) -> bool;
};
struct Keyboard {
+1 -1
View File
@@ -800,7 +800,7 @@ void Game::renderPathSprites() {
}
// Acciones a realizar cuando el jugador colisiona con un globo
void Game::handlePlayerCollision(std::shared_ptr<Player>& player, const std::shared_ptr<Balloon>& balloon) {
void Game::handlePlayerCollision(std::shared_ptr<Player>& player, const std::shared_ptr<Balloon>& /*balloon*/) {
if (!player->isPlaying() || player->isInvulnerable()) {
return; // Si no está jugando o tiene inmunidad, no hace nada
}
+1 -1
View File
@@ -84,7 +84,7 @@ void Instructions::iniSprites() {
// Inicializa los sprites
for (int i = 0; std::cmp_less(i, item_textures_.size()); ++i) {
auto sprite = std::make_unique<Sprite>(item_textures_[i], 0, 0, Item::WIDTH, Item::HEIGHT);
sprite->setPosition((SDL_FPoint){.x = sprite_pos_.x, .y = sprite_pos_.y + ((Item::HEIGHT + item_space_) * i)});
sprite->setPosition(SDL_FPoint{.x = sprite_pos_.x, .y = sprite_pos_.y + ((Item::HEIGHT + item_space_) * i)});
sprites_.push_back(std::move(sprite));
}
}
+2 -2
View File
@@ -39,11 +39,11 @@ class MenuOption {
[[nodiscard]] virtual auto getBehavior() const -> Behavior = 0;
[[nodiscard]] virtual auto getValueAsString() const -> std::string { return ""; }
virtual void adjustValue(bool adjust_up) {}
virtual void adjustValue(bool /*adjust_up*/) {}
[[nodiscard]] virtual auto getTargetGroup() const -> ServiceMenu::SettingsGroup { return ServiceMenu::SettingsGroup::MAIN; }
virtual void executeAction() {}
virtual auto getMaxValueWidth(Text* text_renderer) const -> int { return 0; } // Método virtual para que cada opción calcule el ancho de su valor más largo
virtual auto getMaxValueWidth(Text* /*text_renderer*/) const -> int { return 0; } // Método virtual para que cada opción calcule el ancho de su valor más largo
protected:
// --- Variables ---
+3 -3
View File
@@ -45,7 +45,7 @@ void MenuRenderer::ShowHideAnimation::stop() {
elapsed = 0.0F;
}
MenuRenderer::MenuRenderer(const ServiceMenu* menu_state, std::shared_ptr<Text> element_text, std::shared_ptr<Text> title_text)
MenuRenderer::MenuRenderer(const ServiceMenu* /*menu_state*/, std::shared_ptr<Text> element_text, std::shared_ptr<Text> title_text)
: element_text_(std::move(element_text)),
title_text_(std::move(title_text)) {
initializeMaxSizes();
@@ -145,7 +145,7 @@ void MenuRenderer::render(const ServiceMenu* menu_state) {
}
}
void MenuRenderer::update(const ServiceMenu* menu_state, float delta_time) {
void MenuRenderer::update(const ServiceMenu* /*menu_state*/, float delta_time) {
updateAnimations(delta_time);
if (visible_) {
@@ -380,7 +380,7 @@ void MenuRenderer::updatePosition() {
// Resto de métodos (sin cambios significativos)
void MenuRenderer::precalculateMenuWidths(const std::vector<std::unique_ptr<MenuOption>>& all_options, const ServiceMenu* menu_state) { // NOLINT(readability-named-parameter)
void MenuRenderer::precalculateMenuWidths(const std::vector<std::unique_ptr<MenuOption>>& all_options, const ServiceMenu* menu_state) {
std::ranges::fill(group_menu_widths_, ServiceMenu::MIN_WIDTH);
for (int group = 0; group < 5; ++group) {
auto sg = static_cast<ServiceMenu::SettingsGroup>(group);
+1 -1
View File
@@ -140,7 +140,7 @@ class MenuRenderer {
void updateSwapAnimation(float delta_time);
void updatePosition();
void precalculateMenuWidths(const std::vector<std::unique_ptr<MenuOption>>& all_options, const ServiceMenu* menu_state); // NOLINT(readability-avoid-const-params-in-decls)
void precalculateMenuWidths(const std::vector<std::unique_ptr<MenuOption>>& all_options, const ServiceMenu* menu_state);
[[nodiscard]] auto getMenuWidthForGroup(ServiceMenu::SettingsGroup group) const -> int;
[[nodiscard]] auto getAnimatedSelectedColor() const -> Color;
void updateColorCounter();
+1 -1
View File
@@ -259,7 +259,7 @@ void Notifier::show(std::vector<std::string> texts, int icon, const std::string&
// Dibuja el icono de la notificación
if (has_icons_ && icon >= 0 && texts.size() >= 2) {
auto sp = std::make_unique<Sprite>(icon_texture_, (SDL_FRect){.x = 0, .y = 0, .w = ICON_SIZE, .h = ICON_SIZE});
auto sp = std::make_unique<Sprite>(icon_texture_, SDL_FRect{.x = 0, .y = 0, .w = ICON_SIZE, .h = ICON_SIZE});
sp->setPosition({.x = PADDING_IN_H, .y = PADDING_IN_V, .w = ICON_SIZE, .h = ICON_SIZE});
sp->setSpriteClip(SDL_FRect{
.x = static_cast<float>(ICON_SIZE * (icon % 10)),
+70 -83
View File
@@ -1,109 +1,96 @@
#include "core/resources/resource_pack.hpp"
#include "../../build/version.h" // Para Version::APP_NAME
#include <iostream>
#include <filesystem>
#include <iostream>
#include <string>
void showHelp() {
std::cout << Version::APP_NAME << " - Resource Packer" << std::endl;
std::cout << "===============================================" << std::endl;
std::cout << "Usage: pack_resources [options] [input_dir] [output_file]" << std::endl;
std::cout << std::endl;
std::cout << "Options:" << std::endl;
std::cout << " --help Show this help message" << std::endl;
std::cout << " --list List contents of an existing pack file" << std::endl;
std::cout << std::endl;
std::cout << "Arguments:" << std::endl;
std::cout << " input_dir Directory to pack (default: data)" << std::endl;
std::cout << " output_file Pack file name (default: resources.pack)" << std::endl;
std::cout << std::endl;
std::cout << "Examples:" << std::endl;
std::cout << " pack_resources # Pack 'data' to 'resources.pack'" << std::endl;
std::cout << " pack_resources mydata # Pack 'mydata' to 'resources.pack'" << std::endl;
std::cout << " pack_resources data my.pack # Pack 'data' to 'my.pack'" << std::endl;
std::cout << " pack_resources --list my.pack # List contents of 'my.pack'" << std::endl;
}
#include "core/resources/resource_pack.hpp"
#include "../../build/version.h" // Version::APP_NAME
void listPackContents(const std::string& packFile) {
ResourcePack pack;
if (!pack.loadPack(packFile)) {
std::cerr << "Error: Cannot open pack file: " << packFile << std::endl;
return;
namespace {
void showHelp() {
std::cout << Version::APP_NAME << " - Resource Packer\n";
std::cout << "==============================================\n";
std::cout << "Usage: pack_resources [options] [input_dir] [output_file]\n\n";
std::cout << "Options:\n";
std::cout << " --help Show this help message\n";
std::cout << " --list List contents of an existing pack file\n\n";
std::cout << "Arguments:\n";
std::cout << " input_dir Directory to pack (default: data)\n";
std::cout << " output_file Pack file name (default: resources.pack)\n";
}
auto resources = pack.getResourceList();
std::cout << "Pack file: " << packFile << std::endl;
std::cout << "Resources: " << resources.size() << std::endl;
std::cout << "Contents:" << std::endl;
for (const auto& resource : resources) {
std::cout << " " << resource << std::endl;
void listPackContents(const std::string& pack_file) {
ResourcePack pack;
if (!pack.loadPack(pack_file)) {
std::cerr << "Error: cannot open pack file: " << pack_file << '\n';
return;
}
auto resources = pack.getResourceList();
std::cout << "Pack file: " << pack_file << '\n';
std::cout << "Resources: " << resources.size() << '\n';
for (const auto& r : resources) { std::cout << " " << r << '\n'; }
}
}
} // namespace
int main(int argc, char* argv[]) {
std::string dataDir = "data";
std::string outputFile = "resources.pack";
bool listMode = false;
bool dataDirSet = false;
std::string data_dir = "data";
std::string output_file = "resources.pack";
bool list_mode = false;
bool data_dir_set = false;
// Parse arguments
for (int i = 1; i < argc; i++) {
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--help" || arg == "-h") {
showHelp();
return 0;
} else if (arg == "--list") {
listMode = true;
if (i + 1 < argc) {
outputFile = argv[++i]; // Next argument is pack file to list
}
} else if (!arg.empty() && arg[0] != '-') {
if (!dataDirSet) {
dataDir = arg;
dataDirSet = true;
}
if (arg == "--list") {
list_mode = true;
if (i + 1 < argc) { output_file = argv[++i]; }
continue;
}
if (!arg.empty() && arg[0] != '-') {
if (!data_dir_set) {
data_dir = arg;
data_dir_set = true;
} else {
outputFile = arg;
output_file = arg;
}
}
}
if (listMode) {
listPackContents(outputFile);
if (list_mode) {
listPackContents(output_file);
return 0;
}
std::cout << Version::APP_NAME << " - Resource Packer" << std::endl;
std::cout << "===============================================" << std::endl;
std::cout << "Input directory: " << dataDir << std::endl;
std::cout << "Output file: " << outputFile << std::endl;
std::cout << std::endl;
if (!std::filesystem::exists(dataDir)) {
std::cerr << "Error: Input directory does not exist: " << dataDir << std::endl;
std::cout << Version::APP_NAME << " - Resource Packer\n";
std::cout << "==============================================\n";
std::cout << "Input directory: " << data_dir << '\n';
std::cout << "Output file: " << output_file << '\n';
if (!std::filesystem::exists(data_dir)) {
std::cerr << "Error: input directory does not exist: " << data_dir << '\n';
return 1;
}
ResourcePack pack;
std::cout << "Scanning and packing resources..." << std::endl;
if (!pack.addDirectory(dataDir)) {
std::cerr << "Error: Failed to add directory to pack" << std::endl;
std::cout << "Scanning and packing resources...\n";
if (!pack.addDirectory(data_dir)) {
std::cerr << "Error: failed to add directory to pack\n";
return 1;
}
std::cout << "Found " << pack.getResourceCount() << " resources" << std::endl;
std::cout << "Saving pack file..." << std::endl;
if (!pack.savePack(outputFile)) {
std::cerr << "Error: Failed to save pack file" << std::endl;
std::cout << "Found " << pack.getResourceCount() << " resources\n";
std::cout << "Saving pack file...\n";
if (!pack.savePack(output_file)) {
std::cerr << "Error: failed to save pack file\n";
return 1;
}
std::filesystem::path packPath(outputFile);
auto fileSize = std::filesystem::file_size(packPath);
std::cout << "Pack file created successfully!" << std::endl;
std::cout << "File size: " << (fileSize / 1024.0 / 1024.0) << " MB" << std::endl;
auto file_size = std::filesystem::file_size(std::filesystem::path(output_file));
std::cout << "Pack file created: " << output_file << " ("
<< (static_cast<double>(file_size) / 1024.0 / 1024.0) << " MB)\n";
return 0;
}
}