Compare commits
25 Commits
time-based
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 551cd23318 | |||
| 3b675246bb | |||
| 88b295bc13 | |||
| 86bdfb8f73 | |||
| 7a4b340ee4 | |||
| a43c3fc5d1 | |||
| bdbb6bc764 | |||
| fa2dc9bbf3 | |||
| 73f210bc2c | |||
| 74d96047c7 | |||
| 20325ddd5a | |||
| ac997c185d | |||
| 5fcbce6e7b | |||
| 984d1fca50 | |||
| 66ad34b667 | |||
| bded70a52a | |||
| 1129f1116e | |||
| 1ddc821f6f | |||
| 49be109560 | |||
| 63eaaa8b5c | |||
| 748673f41b | |||
| 8af4b0c259 | |||
| be1a9a1d9b | |||
| 7bd4d4d114 | |||
| 0148ccc4d5 |
@@ -0,0 +1,66 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v2.4.0
|
||||||
|
|
||||||
|
### Novetats principals
|
||||||
|
|
||||||
|
- **Migració a temps real (time-based)**. Tot el joc ja no depèn d'una cadència fixa de 60 frames per segon: els moviments, les animacions, les fades, els temporitzadors i els comptadors es calculen a partir del temps real transcorregut entre frames (`dt_s`). Això corregeix l'acceleració del jugador i les animacions en monitors de 144 Hz, i prepara el joc per a qualsevol freqüència de refresc. El refactor s'ha fet entitat per entitat (Bullet, Item, Player, Balloon) i, finalment, al motor principal `Game`.
|
||||||
|
- **Demo multi-set**. El sistema de demo s'ha portat al patró time-based de Coffee Crisis Arcade Edition: 3 fitxers de demo distints (`demo1.bin`, `demo2.bin`, `demo3.bin`) es trien aleatòriament en cada execució, l'índex es calcula a partir del temps acumulat i el playback és immune a salts de frames. En saltar-la amb una tecla es torna correctament al títol amb el menú visible.
|
||||||
|
- **Modes de presentació del canvas**. Nou cicle de presentació de la imatge a la finestra: `integer_scale` (escalat enter), `letterbox` (manté l'aspect ratio amb barres negres), `stretched` (omple la finestra deformant) i `overscan` (omple la finestra retallant). El valor antic `integer_scale` del config es migra automàticament.
|
||||||
|
|
||||||
|
### Hotkeys (notificacions visibles a la part superior del canvas)
|
||||||
|
|
||||||
|
| Tecla | Acció |
|
||||||
|
|------:|-------|
|
||||||
|
| F1 / F2 | Reduir / augmentar el zoom de la finestra |
|
||||||
|
| F3 | Alternar pantalla completa |
|
||||||
|
| F4 | Activar/desactivar post-procesat (shaders) |
|
||||||
|
| F5 | Alternar entre shader PostFX i CrtPi |
|
||||||
|
| F6 | Següent preset del shader actiu |
|
||||||
|
| F7 | Activar/desactivar V-Sync |
|
||||||
|
| F8 | Cicla el mode de presentació (integer_scale → letterbox → stretched → overscan) |
|
||||||
|
| F10 | Mostrar/amagar comptador de FPS (cantonada superior dreta) |
|
||||||
|
| F11 | Mostrar nom de l'app + versió + hash de git |
|
||||||
|
| F12 | Pausa |
|
||||||
|
| ESC | Eixir (doble pulsació per confirmar) |
|
||||||
|
| BACKSPACE | Cancel·lar a menús |
|
||||||
|
|
||||||
|
Les notificacions tenen ara una paleta semàntica (informació, toggle, confirmació, èxit, perill) i un color més saturat (a mig camí entre pastel i color pur).
|
||||||
|
|
||||||
|
### Millores
|
||||||
|
|
||||||
|
- **Cap tecla de funció ni ESC fa "saltar" cap secció**. Logo, intro, instruccions, títol i demo només es passen amb tecles humanes (Enter, disparar, moviment, botons de gamepad) — les F1–F12 i ESC es reserven íntegrament per als hotkeys globals.
|
||||||
|
- **Notificacions en mode overscan**. Quan el mode `overscan` retalla la franja superior del canvas segons l'aspect ratio de la finestra, les notificacions i el comptador de FPS es desplacen automàticament a la primera fila visible.
|
||||||
|
- **Versió única de l'aplicació**. La cadena de versió ja només viu a `source/utils/defines.hpp`; CMake l'extreu via regex per al projecte i el Makefile l'usa per als noms dels release. F11 mostra `Coffee Crisis v2.4.0 (<git_hash>)`.
|
||||||
|
- **Comptador de FPS** dibuixat a la part superior dreta en verd, recalcat cada segon de temps real.
|
||||||
|
- **Confirmació d'eixida (ESC × 2)** amb finestra visual de confirmació en roig.
|
||||||
|
- **Pausa amb compte enrere configurable** (`gameplay.pause_countdown` a `config.yaml`).
|
||||||
|
- **Zoom màxim de la finestra detectat automàticament** segons la resolució del display.
|
||||||
|
- **Paquet de recursos més robust**: `resources.pack` localitzat via `SDL_GetBasePath` amb fallback al filesystem.
|
||||||
|
- **Build estandarditzada**: llista explícita de fonts en lloc de `GLOB_RECURSE`, `-Wextra -Wpedantic` activats i warnings netejats.
|
||||||
|
- **`pre-commit` hooks** per `clang-format`, `clang-tidy` i `cppcheck`.
|
||||||
|
|
||||||
|
### Correccions de bugs
|
||||||
|
|
||||||
|
- Demo congelada en obrir-se per culpa d'un doble `DeltaTime::tick()` al títol (Game rebia `dt ≈ 0`).
|
||||||
|
- Salt visual al fons diagonal del títol per posició inicial no ancorada al cicle de tile.
|
||||||
|
- Rotació de la `PowerBall` perduda en passar per `Game::startAllBalloons` després del rellotge.
|
||||||
|
- Pausa que es disparava amb el flanc residual de CANCEL/EXIT en entrar al menú.
|
||||||
|
- Animació del jugador accelerada en monitors de 144 Hz (no propagava `dt_s` als sprites).
|
||||||
|
- Sub-bucles aniats de pausa, game over, instruccions i demo aplanats a un únic loop SDL3.
|
||||||
|
- WASM/Emscripten: reset en fer "exit", eliminades les opcions d'eixida, fix de fullscreen, mode `integer_scale=false` per defecte.
|
||||||
|
- Windows: parsers de text amb finals de línia CRLF, headers SPV del PostFX regenerats.
|
||||||
|
- `pack_resources` anava a la rel en comptes de `build/`.
|
||||||
|
|
||||||
|
### Canvis interns destacats
|
||||||
|
|
||||||
|
- **Pipeline SDL3 GPU + fallback `SDL_Renderer`**: shaders PostFX i CrtPi (Vulkan/Metal/D3D12), presets persistents en YAML, scanlines analítiques sense supersampling.
|
||||||
|
- **Sistema d'opcions modern**: tot a `config.yaml` amb fkyaml; `postfx.yaml` i `crtpi.yaml` per als presets.
|
||||||
|
- **Sistema d'àudio nou** (`Ja::` namespace, streaming d'OGG, tipus sense prefix `JA_`).
|
||||||
|
- **API SDL3 Callbacks** (`SDL_AppInit`/`Iterate`/`Event`/`Quit`).
|
||||||
|
- Migració a `enum class` per a Input::Action, Input::Device, Input::Repeat, Input::Disable, Fade::Type i Bullet::Kind.
|
||||||
|
- Convencions `CamelCase` aplicades a tipus que encara duien `_t`/`_e` o eren niats sense format.
|
||||||
|
- Singletons (Lang, Audio, Input, Resource) i sistema de loaders de recursos.
|
||||||
|
- Migració d'estructura a `source/core/{audio,input,locale,rendering,resources,system}/` i `source/game/{entities,scenes,ui}/`.
|
||||||
|
- Generació automàtica de `version.h` amb el hash curt de git.
|
||||||
|
- `clang-format` i `clang-tidy` unificats amb la resta de projectes germans.
|
||||||
+114
-29
@@ -1,45 +1,120 @@
|
|||||||
# CMakeLists.txt
|
# CMakeLists.txt
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.10)
|
cmake_minimum_required(VERSION 3.10)
|
||||||
project(coffee_crisis VERSION 1.00)
|
|
||||||
|
# La versió de l'app es defineix una sola vegada a source/utils/defines.hpp
|
||||||
|
# (Defines::VERSION). El Makefile ja la grepeja per als noms de release; aqui
|
||||||
|
# l'extreiem perque project(... VERSION ...) i tots els consumidors interns
|
||||||
|
# de CMake (CPack, install, etc.) usin la mateixa font de veritat.
|
||||||
|
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/source/utils/defines.hpp" _DEFINES_CONTENT)
|
||||||
|
string(REGEX MATCH "VERSION = \"([0-9]+\\.[0-9]+\\.[0-9]+)\"" _ "${_DEFINES_CONTENT}")
|
||||||
|
set(APP_VERSION "${CMAKE_MATCH_1}")
|
||||||
|
if(APP_VERSION STREQUAL "")
|
||||||
|
message(FATAL_ERROR "No s'ha pogut extreure VERSION de source/utils/defines.hpp")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
project(coffee_crisis VERSION ${APP_VERSION})
|
||||||
|
|
||||||
# Tipus de build per defecte (Debug) si no se n'ha especificat cap
|
# Tipus de build per defecte (Debug) si no se n'ha especificat cap
|
||||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
|
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Configuración de compilador para MinGW en Windows
|
|
||||||
if(WIN32 AND NOT CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
|
|
||||||
set(CMAKE_CXX_COMPILER "g++")
|
|
||||||
set(CMAKE_C_COMPILER "gcc")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Establecer estándar de C++
|
# Establecer estándar de C++
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
# Configuración global de flags de compilación
|
# --- GENERACIÓN DE VERSIÓN AUTOMÁTICA ---
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
|
# Si GIT_HASH se ha pasado desde fuera (p.ej. desde el Makefile via -DGIT_HASH=xxx),
|
||||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Os -ffunction-sections -fdata-sections")
|
# lo usamos tal cual. Esto evita problemas con Docker/emscripten, donde git aborta por
|
||||||
|
# "dubious ownership" en el volumen montado. En builds locales sin -DGIT_HASH, se
|
||||||
|
# resuelve aquí ejecutando git directamente.
|
||||||
|
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
|
||||||
|
find_package(Git QUIET)
|
||||||
|
if(GIT_FOUND)
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
OUTPUT_VARIABLE GIT_HASH
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
ERROR_QUIET
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
|
||||||
|
set(GIT_HASH "unknown")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Configurar archivo de versión
|
||||||
|
configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/version.h @ONLY)
|
||||||
|
|
||||||
# Define el directorio de los archivos fuente
|
# Define el directorio de los archivos fuente
|
||||||
set(DIR_SOURCES "${CMAKE_SOURCE_DIR}/source")
|
set(DIR_SOURCES "${CMAKE_SOURCE_DIR}/source")
|
||||||
|
|
||||||
# Cargar todos los archivos fuente en DIR_SOURCES (recursivo, sin external/)
|
# --- LISTA EXPLÍCITA DE FUENTES ---
|
||||||
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "${DIR_SOURCES}/*.cpp")
|
set(APP_SOURCES
|
||||||
list(FILTER SOURCES EXCLUDE REGEX "${DIR_SOURCES}/external/.*")
|
source/main.cpp
|
||||||
|
|
||||||
# En Emscripten no compilamos sdl3gpu_shader (SDL3 GPU no está soportado en WebGL2).
|
# --- core/audio ---
|
||||||
# Define NO_SHADERS más abajo y filtra el fuente aquí.
|
source/core/audio/audio.cpp
|
||||||
if(EMSCRIPTEN)
|
source/core/audio/audio_adapter.cpp
|
||||||
list(REMOVE_ITEM SOURCES "${DIR_SOURCES}/core/rendering/sdl3gpu/sdl3gpu_shader.cpp")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
# Verificar si se encontraron archivos fuente
|
# --- core/input ---
|
||||||
if(NOT SOURCES)
|
source/core/input/global_inputs.cpp
|
||||||
message(FATAL_ERROR "No se encontraron archivos fuente en ${DIR_SOURCES}.")
|
source/core/input/input.cpp
|
||||||
endif()
|
source/core/input/mouse.cpp
|
||||||
|
|
||||||
|
# --- core/locale ---
|
||||||
|
source/core/locale/lang.cpp
|
||||||
|
|
||||||
|
# --- core/rendering ---
|
||||||
|
source/core/rendering/animatedsprite.cpp
|
||||||
|
source/core/rendering/fade.cpp
|
||||||
|
source/core/rendering/movingsprite.cpp
|
||||||
|
source/core/rendering/notifications.cpp
|
||||||
|
source/core/rendering/screen.cpp
|
||||||
|
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
||||||
|
source/core/rendering/smartsprite.cpp
|
||||||
|
source/core/rendering/sprite.cpp
|
||||||
|
source/core/rendering/text.cpp
|
||||||
|
source/core/rendering/texture.cpp
|
||||||
|
source/core/rendering/writer.cpp
|
||||||
|
|
||||||
|
# --- core/resources ---
|
||||||
|
source/core/resources/asset.cpp
|
||||||
|
source/core/resources/resource.cpp
|
||||||
|
source/core/resources/resource_helper.cpp
|
||||||
|
source/core/resources/resource_loader.cpp
|
||||||
|
source/core/resources/resource_pack.cpp
|
||||||
|
|
||||||
|
# --- core/system ---
|
||||||
|
source/core/system/delta_time.cpp
|
||||||
|
source/core/system/demo.cpp
|
||||||
|
source/core/system/director.cpp
|
||||||
|
|
||||||
|
# --- game ---
|
||||||
|
source/game/game.cpp
|
||||||
|
source/game/options.cpp
|
||||||
|
|
||||||
|
# --- game/entities ---
|
||||||
|
source/game/entities/balloon.cpp
|
||||||
|
source/game/entities/bullet.cpp
|
||||||
|
source/game/entities/item.cpp
|
||||||
|
source/game/entities/player.cpp
|
||||||
|
|
||||||
|
# --- game/scenes ---
|
||||||
|
source/game/scenes/instructions.cpp
|
||||||
|
source/game/scenes/intro.cpp
|
||||||
|
source/game/scenes/logo.cpp
|
||||||
|
source/game/scenes/title.cpp
|
||||||
|
|
||||||
|
# --- game/ui ---
|
||||||
|
source/game/ui/menu.cpp
|
||||||
|
|
||||||
|
# --- utils ---
|
||||||
|
source/utils/utils.cpp
|
||||||
|
)
|
||||||
|
|
||||||
# Configuración de SDL3
|
# Configuración de SDL3
|
||||||
if(EMSCRIPTEN)
|
if(EMSCRIPTEN)
|
||||||
@@ -114,14 +189,26 @@ else()
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Añadir ejecutable principal
|
# Añadir ejecutable principal
|
||||||
add_executable(${PROJECT_NAME} ${SOURCES})
|
if(EMSCRIPTEN)
|
||||||
|
# En Emscripten no compilem sdl3gpu_shader (SDL3 GPU no està suportat en WebGL2)
|
||||||
|
set(APP_SOURCES_WASM ${APP_SOURCES})
|
||||||
|
list(REMOVE_ITEM APP_SOURCES_WASM source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp)
|
||||||
|
add_executable(${PROJECT_NAME} ${APP_SOURCES_WASM})
|
||||||
|
else()
|
||||||
|
add_executable(${PROJECT_NAME} ${APP_SOURCES})
|
||||||
|
endif()
|
||||||
|
|
||||||
if(NOT APPLE AND NOT EMSCRIPTEN AND GLSLC_EXE)
|
if(NOT APPLE AND NOT EMSCRIPTEN AND GLSLC_EXE)
|
||||||
add_dependencies(${PROJECT_NAME} shaders)
|
add_dependencies(${PROJECT_NAME} shaders)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Includes relatius a source/ (p.e. `#include "core/rendering/texture.h"`)
|
# Includes relatius a source/ (p.e. `#include "core/rendering/texture.h"`)
|
||||||
target_include_directories(${PROJECT_NAME} PRIVATE ${DIR_SOURCES})
|
# ${CMAKE_BINARY_DIR} per al version.h generat per configure_file.
|
||||||
|
target_include_directories(${PROJECT_NAME} PRIVATE ${DIR_SOURCES} ${CMAKE_BINARY_DIR})
|
||||||
|
|
||||||
|
# Flags de compilació per-target
|
||||||
|
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:-Os -ffunction-sections -fdata-sections>)
|
||||||
|
|
||||||
# Añadir definiciones de compilación dependiendo del tipo de build
|
# Añadir definiciones de compilación dependiendo del tipo de build
|
||||||
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
||||||
@@ -184,8 +271,6 @@ file(GLOB_RECURSE ALL_SOURCE_FILES
|
|||||||
|
|
||||||
set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES})
|
set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES})
|
||||||
|
|
||||||
set(FORMAT_SOURCES ${ALL_SOURCE_FILES})
|
|
||||||
|
|
||||||
# Para cppcheck, pasar solo .cpp (los headers se procesan transitivamente).
|
# Para cppcheck, pasar solo .cpp (los headers se procesan transitivamente).
|
||||||
set(CPPCHECK_SOURCES ${ALL_SOURCE_FILES})
|
set(CPPCHECK_SOURCES ${ALL_SOURCE_FILES})
|
||||||
list(FILTER CPPCHECK_SOURCES INCLUDE REGEX ".*\\.cpp$")
|
list(FILTER CPPCHECK_SOURCES INCLUDE REGEX ".*\\.cpp$")
|
||||||
@@ -218,7 +303,7 @@ if(CLANG_FORMAT_EXE)
|
|||||||
add_custom_target(format
|
add_custom_target(format
|
||||||
COMMAND ${CLANG_FORMAT_EXE}
|
COMMAND ${CLANG_FORMAT_EXE}
|
||||||
-i
|
-i
|
||||||
${FORMAT_SOURCES}
|
${ALL_SOURCE_FILES}
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
COMMENT "Running clang-format..."
|
COMMENT "Running clang-format..."
|
||||||
)
|
)
|
||||||
@@ -227,7 +312,7 @@ if(CLANG_FORMAT_EXE)
|
|||||||
COMMAND ${CLANG_FORMAT_EXE}
|
COMMAND ${CLANG_FORMAT_EXE}
|
||||||
--dry-run
|
--dry-run
|
||||||
--Werror
|
--Werror
|
||||||
${FORMAT_SOURCES}
|
${ALL_SOURCE_FILES}
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
COMMENT "Checking clang-format..."
|
COMMENT "Checking clang-format..."
|
||||||
)
|
)
|
||||||
@@ -268,7 +353,7 @@ if(NOT EMSCRIPTEN)
|
|||||||
source/core/resources/resource_pack.cpp
|
source/core/resources/resource_pack.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
|
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/.
|
# Regeneració automàtica de resources.pack en cada build si canvia data/.
|
||||||
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
|
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,542 @@
|
|||||||
|
# Arquitectura de **Coffee Crisis**
|
||||||
|
|
||||||
|
> 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, o donde el código contradice a la documentación previa, lo
|
||||||
|
> digo explícitamente en lugar de inventarlo.
|
||||||
|
>
|
||||||
|
> **Coffee Crisis** es un arcade en C++20 + SDL3: el jugador defiende la UPV de
|
||||||
|
> globos de café rebotantes a lo largo de 10 fases. Soporta 1–2 jugadores,
|
||||||
|
> teclado y mando, y varios idiomas. Es el **predecesor** de *Coffee Crisis
|
||||||
|
> Arcade Edition*; al final del documento ([§15](#15-diferencias-frente-a-la-arcade-edition))
|
||||||
|
> hay un resumen de las diferencias entre ambos. 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](#7-entidades)
|
||||||
|
8. [Modo demo y attract mode](#8-modo-demo-y-attract-mode)
|
||||||
|
9. [Recursos](#9-recursos)
|
||||||
|
10. [Audio](#10-audio)
|
||||||
|
11. [Configuración y constantes](#11-configuración-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)
|
||||||
|
15. [Diferencias frente a la Arcade Edition](#15-diferencias-frente-a-la-arcade-edition)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Visión general
|
||||||
|
|
||||||
|
El árbol `source/` separa **motor** y **juego**:
|
||||||
|
|
||||||
|
- **`source/core/`** — motor genérico: `system` (`director`, `delta_time`,
|
||||||
|
`demo`), `rendering` (+ `sdl3gpu`, sprites), `input`, `resources`, `audio`,
|
||||||
|
`locale`.
|
||||||
|
- **`source/game/`** — el juego concreto: `game.*` (el hub de gameplay),
|
||||||
|
`entities/` (player, balloon, bullet, item), `scenes/` (logo, intro, title,
|
||||||
|
instructions), `ui/` (menu), `options.*` y `defaults.hpp`.
|
||||||
|
- **`source/utils/`** — `utils.*` (helpers, `struct Section`, `Color`,
|
||||||
|
dificultad…) y `defines.hpp` (macros de build).
|
||||||
|
- **`source/external/`** — vendorizado: `stb_image`, `stb_vorbis` (y headers
|
||||||
|
YAML/JSON).
|
||||||
|
|
||||||
|
~51 ficheros C++ y ~16.000 líneas. **Nota sobre cabeceras**: los módulos
|
||||||
|
antiguos usan extensión **`.h`** (p.ej. `director.h`, `game.h`, `screen.h`); los
|
||||||
|
módulos nuevos usan **`.hpp`** (p.ej. `demo.hpp`, `options.hpp`,
|
||||||
|
`delta_time.hpp`). Es un proyecto en migración, y eso se nota en varias capas.
|
||||||
|
|
||||||
|
**Ideas-fuerza que conviene interiorizar:**
|
||||||
|
|
||||||
|
1. El flujo se controla con un **`struct Section { name, subsection }`** que el
|
||||||
|
`Director` lee cada frame (§3).
|
||||||
|
2. El render dibuja con **texturas GPU** (`SDL_Renderer`) sobre un **canvas
|
||||||
|
virtual de 256×192**, con post-procesado opcional vía un backend SDL3 GPU
|
||||||
|
(§4).
|
||||||
|
3. El gameplay es **monolítico**: casi todo vive en la clase `Game`, con
|
||||||
|
vectores de **punteros crudos** y `new`/`delete` manual (§6, §7).
|
||||||
|
4. **Sí hay modo demo** (*attract mode*): **reproducción de input grabado**, no
|
||||||
|
IA, orquestada desde la pantalla de título (§8).
|
||||||
|
5. El proyecto está **migrando de frame-based a time-based** y de `config.txt`
|
||||||
|
a YAML; conviven ambos mundos (§2, §11).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
SDL[SDL3 callbacks · main.cpp] --> DIR[Director]
|
||||||
|
DIR -->|struct Section| ST{handleSectionTransition}
|
||||||
|
ST --> SEC["Logo / Intro / Title / Game"]
|
||||||
|
SEC --> TITLE[Title] -.attract.-> NESTED["Game anidado en demo + Instructions"]
|
||||||
|
SEC --> GAME["Game (monolítico)"]
|
||||||
|
GAME --> ENT["Player* / Balloon* / Bullet* / Item* (punteros crudos)"]
|
||||||
|
GAME --> DEMOSYS["Demo (playback grabado)"] -.-> ENT
|
||||||
|
GAME -->|SDL_RenderTexture| CANVAS["game_canvas_ 256×192"]
|
||||||
|
CANVAS --> SCREEN[Screen] --> SB["ShaderBackend PostFX/CrtPi"] --> WIN[Ventana]
|
||||||
|
RES["Asset / Resource"] -.-> 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 → new Director(argc, argv);
|
||||||
|
SDL_AppIterate→ Director::iterate(); // un frame
|
||||||
|
SDL_AppEvent → Director::handleEvent(event);
|
||||||
|
SDL_AppQuit → delete Director;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2. El `Director`
|
||||||
|
|
||||||
|
`source/core/system/director.h` / `.cpp`. Inicializa SDL, ventana y renderer,
|
||||||
|
crea la carpeta de sistema, monta input/audio/recursos, y mantiene **un
|
||||||
|
`unique_ptr` por sección** (`logo_`, `intro_`, `title_`, `game_`) de los que
|
||||||
|
**solo uno está vivo** (`director.h:55`). Guarda un puntero a la
|
||||||
|
`struct Section* section_` que comparte con la sección activa.
|
||||||
|
|
||||||
|
`Director::iterate()` cada frame: comprueba salida (doble ESC vía
|
||||||
|
`GlobalInputs::wantsQuit()`), actualiza la visibilidad del cursor, llama a
|
||||||
|
`handleSectionTransition()` y despacha `iterate()` a la sección activa
|
||||||
|
(`director.cpp`, `switch (active_section_)`).
|
||||||
|
|
||||||
|
### 2.3. Gestión del tiempo (en migración)
|
||||||
|
|
||||||
|
El reloj central es `source/core/system/delta_time.*`: `DeltaTime::tick()`
|
||||||
|
devuelve el delta en segundos consumido al inicio de cada frame de la sección
|
||||||
|
(`game.cpp`, `Game::iterate`). El proyecto **está migrando de frame-based a
|
||||||
|
time-based**: en `game.h` se ven contadores duplicados, el viejo frame-based
|
||||||
|
(`Uint16 death_counter_`) y el nuevo time-based (`float death_counter_s_`),
|
||||||
|
documentados como tales (`game.h:347`). El playback de la demo también es
|
||||||
|
time-based: `index = elapsed_s * 60` (`demo.hpp:11`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Secciones y flujo de la aplicación
|
||||||
|
|
||||||
|
### 3.1. `struct Section`
|
||||||
|
|
||||||
|
`source/utils/utils.h:58` define un POD minimalista:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct Section {
|
||||||
|
Uint8 name; // SECTION_PROG_* (LOGO/INTRO/TITLE/GAME/QUIT)
|
||||||
|
Uint8 subsection; // SUBSECTION_* (p.ej. GAME_PLAY_1P, GAME_PAUSE, TITLE_INSTRUCTIONS…)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Los valores son **constantes `constexpr int` en `source/game/defaults.hpp`**
|
||||||
|
(`SECTION_PROG_LOGO = 0`, …, `SECTION_PROG_QUIT = 4`; `SUBSECTION_GAME_PLAY_1P`,
|
||||||
|
`SUBSECTION_GAME_PAUSE`, `SUBSECTION_GAME_GAMEOVER`, `SUBSECTION_TITLE_INSTRUCTIONS`,
|
||||||
|
etc.; `defaults.hpp:90`). Cualquier parte del código cambia el flujo asignando
|
||||||
|
`section_->name = ...` / `section_->subsection = ...`.
|
||||||
|
|
||||||
|
### 3.2. Transición de secciones
|
||||||
|
|
||||||
|
`Director::handleSectionTransition()` (`director.cpp`):
|
||||||
|
|
||||||
|
- Traduce `section_->name` a un `enum class ActiveSection` (`director.h:24`).
|
||||||
|
- Si coincide con la activa, no hace nada.
|
||||||
|
- Si cambió: libera las cuatro secciones (`reset()`) y construye la nueva. Para
|
||||||
|
`GAME` decide el nº de jugadores según `section_->subsection`
|
||||||
|
(`SUBSECTION_GAME_PLAY_1P` → 1, si no → 2) y crea
|
||||||
|
`Game(NUM_PLAYERS, 0, renderer_, /*demo=*/false, section_)`.
|
||||||
|
|
||||||
|
Cada sección recibe el `renderer_` y el `Section*`, y expone `iterate()` (y, en
|
||||||
|
`Game`, `handleEvent()`).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
LOGO --> INTRO --> TITLE
|
||||||
|
TITLE -->|jugar| GAME --> TITLE
|
||||||
|
TITLE -->|attract / manual| INSTR["Instructions (dentro de Title)"]
|
||||||
|
TITLE -->|attract| DEMOG["Game anidado en demo (dentro de Title)"]
|
||||||
|
TITLE --> QUIT
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Matiz importante**: `Instructions` y la **demo** no son secciones del
|
||||||
|
> `Director`. Viven **dentro de `Title`**, que ejecuta un *attract loop* (ver
|
||||||
|
> §8). El `Director` solo conoce Logo/Intro/Title/Game/Quit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Renderizado: de la lógica al píxel
|
||||||
|
|
||||||
|
Los sprites son **texturas GPU** dibujadas por `SDL_Renderer` sobre un canvas
|
||||||
|
virtual; el post-procesado va por un backend SDL3 GPU.
|
||||||
|
|
||||||
|
### 4.1. El canvas virtual 256×192
|
||||||
|
|
||||||
|
`Screen` (`core/rendering/screen.h`) crea `game_canvas_`, una `SDL_Texture` de
|
||||||
|
**256×192** (`GAMECANVAS_WIDTH/HEIGHT` en `defaults.hpp:64`) con
|
||||||
|
`SDL_TEXTUREACCESS_TARGET` (`screen.cpp:106`). Toda la geometría del juego se
|
||||||
|
deriva de esa resolución y de un `BLOCK` base (áreas de juego en
|
||||||
|
`defaults.hpp`).
|
||||||
|
|
||||||
|
`Screen::start()` (`screen.cpp:166`) fija el render-target a `game_canvas_`;
|
||||||
|
a partir de ahí, la sección activa dibuja sus sprites sobre él.
|
||||||
|
|
||||||
|
### 4.2. Texturas y jerarquía de sprites
|
||||||
|
|
||||||
|
- `core/rendering/texture.h` — `Texture` envuelve un `SDL_Texture*` cargado de
|
||||||
|
PNG; método `render(...)` con clip/zoom/flip.
|
||||||
|
- `core/rendering/sprite.h` y derivados:
|
||||||
|
- `Sprite` — dibuja desde un *spritesheet*.
|
||||||
|
- `AnimatedSprite` — animación por fotogramas, definida en ficheros **`.ani`**.
|
||||||
|
- `MovingSprite` — añade posición/velocidad (p.ej. las nubes del fondo).
|
||||||
|
- `SmartSprite` — sprite autónomo (popups de puntuación, el café que salta al
|
||||||
|
recibir un golpe).
|
||||||
|
- Texto: `core/rendering/text.h` + `writer.h` (fuentes bitmap).
|
||||||
|
- Transiciones: `core/rendering/fade.h`. Notificaciones:
|
||||||
|
`core/rendering/notifications.*`.
|
||||||
|
|
||||||
|
### 4.3. Post-procesado y presentación
|
||||||
|
|
||||||
|
El path de presentación (`screen.cpp:185`) decide cómo llega el canvas a la
|
||||||
|
ventana:
|
||||||
|
|
||||||
|
- **Con backend GPU acelerado**: lee los píxeles de `game_canvas_` con
|
||||||
|
`SDL_RenderReadPixels` a un `pixel_buffer_` (ARGB8888;
|
||||||
|
`screen.h:162`), los sube al backend (`shader_backend_->uploadPixels(...)`) y
|
||||||
|
este renderiza con el shader activo a la ventana.
|
||||||
|
- **Sin backend / desactivado** (fallback): `SDL_RenderTexture` del
|
||||||
|
`game_canvas_` a la ventana y `SDL_RenderPresent` (`screen.cpp:233`).
|
||||||
|
|
||||||
|
El backend vive en `core/rendering/sdl3gpu/` (interfaz abstracta en
|
||||||
|
`shader_backend.hpp`). Dos shaders: **PostFX** (viñeta, scanlines, chroma,
|
||||||
|
gamma, máscara, curvatura, *bleeding*, flicker) y **CrtPi** (scanlines continuas
|
||||||
|
con bloom). Los GLSL de `data/shaders/` se compilan a SPIR-V (`spv/*_spv.h`) vía
|
||||||
|
`glslc`; en macOS se usan shaders **Metal (MSL)** inline (`sdl3gpu/msl/`). El
|
||||||
|
build `NO_SHADERS` (Emscripten) fuerza la ruta clásica.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
OBJ["fondo, globos, jugador, balas, items…"] -->|SDL_RenderTexture| CANVAS["game_canvas_ 256×192 (render target)"]
|
||||||
|
CANVAS -->|RenderReadPixels → uploadPixels| SHADER["ShaderBackend (PostFX / CrtPi)"]
|
||||||
|
SHADER --> WIN[Ventana]
|
||||||
|
CANVAS -.fallback sin GPU.-> WIN
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4. Modos de escalado y efectos
|
||||||
|
|
||||||
|
La presentación a la ventana respeta `Options::video.presentation_mode`
|
||||||
|
(`INTEGER_SCALE`, `LETTERBOX`, `STRETCHED`, `OVERSCAN`; `options.hpp:24`). El
|
||||||
|
`Game` añade efectos como *flash*, *shake* y un *death shake* intenso
|
||||||
|
(`game.h:100`, `DeathShake`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Entrada
|
||||||
|
|
||||||
|
### 5.1. `Input`
|
||||||
|
|
||||||
|
`source/core/input/input.h` — abstracción de teclado y mando bajo un enum de
|
||||||
|
acciones (`Input::Action`). El jugador se mueve con flechas y dispara
|
||||||
|
izquierda/centro/derecha; el `Input::Device` selecciona teclado o mando por
|
||||||
|
jugador (`Game::player_one_control_`, `game.h:379`).
|
||||||
|
|
||||||
|
### 5.2. Hotkeys globales y salida
|
||||||
|
|
||||||
|
`source/core/input/global_inputs.*` gestiona las teclas de sistema (ventana,
|
||||||
|
vídeo, post-FX, idioma, FPS overlay…) y la **salida en dos pasos**: la primera
|
||||||
|
pulsación de ESC arma una confirmación y la segunda activa `wantsQuit()`, que el
|
||||||
|
`Director` traduce a `SECTION_PROG_QUIT` (`director.cpp`). El cursor del ratón
|
||||||
|
se autooculta (`core/input/mouse.*`).
|
||||||
|
|
||||||
|
Las hotkeys de shaders documentadas: **F4** activa/desactiva post-procesado,
|
||||||
|
**F5** alterna PostFX↔CrtPi, **F6** siguiente preset (ver `CLAUDE.md`).
|
||||||
|
|
||||||
|
### 5.3. Cómo llega la entrada al jugador
|
||||||
|
|
||||||
|
Dentro de `Game`, `checkGameInput()` → `processLiveInput()` →
|
||||||
|
`processPlayerLiveInput(player, i)` consulta `Input` y llama a
|
||||||
|
`player->setInput(Input::Action)` y a `createBullet(...)` al disparar
|
||||||
|
(`game.h:228`). En modo demo, esa misma vía la alimenta `processDemoInput()`
|
||||||
|
con datos grabados (§8).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Lógica del juego: la clase `Game`
|
||||||
|
|
||||||
|
`source/game/game.{h,cpp}` es **el hub de gameplay y, con diferencia, la clase
|
||||||
|
más grande** del proyecto: ~400 líneas solo de declaración. A diferencia de la
|
||||||
|
Arcade Edition, **no delega en managers**: las formaciones, fases, globos,
|
||||||
|
balas e ítems se gestionan directamente aquí.
|
||||||
|
|
||||||
|
### 6.1. El frame y sus sub-bucles
|
||||||
|
|
||||||
|
`Game::iterate()` (`game.cpp`) consume el delta con `DeltaTime::tick()` y
|
||||||
|
despacha según `section_->subsection`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
switch (section_->subsection) {
|
||||||
|
case SUBSECTION_GAME_PAUSE: iteratePaused(dt); break;
|
||||||
|
case SUBSECTION_GAME_GAMEOVER: iterateGameOver(dt); break;
|
||||||
|
case SUBSECTION_GAME_PLAY_1P:
|
||||||
|
case SUBSECTION_GAME_PLAY_2P: iteratePlaying(dt); break;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Es decir, **pausa y game-over son sub-estados** del propio `Game` (no secciones
|
||||||
|
del Director), cada uno con su `update`/`render`. En modo demo, entrar en pausa
|
||||||
|
o game-over rebota directamente a `Title` (`game.cpp`, `Game::iterate`).
|
||||||
|
|
||||||
|
### 6.2. Lo que gestiona `Game`
|
||||||
|
|
||||||
|
Todo dentro de la misma clase (`game.h`):
|
||||||
|
|
||||||
|
- **Fases (`Stage stage_[10]`)**: cada fase tiene un *pool* de formaciones
|
||||||
|
enemigas (`EnemyPool`/`EnemyFormation`/`EnemyInit`, structs internos), poder
|
||||||
|
para completarla y umbrales de amenaza. `initEnemyFormations*` precalcula las
|
||||||
|
formaciones (lineales, simétricas, hexágonos…).
|
||||||
|
- **Nivel de amenaza** (`menace_current_`/`menace_threshold_`): si la amenaza
|
||||||
|
cae bajo el umbral, se despliega otra formación (`updateMenace`,
|
||||||
|
`evaluateAndSetMenace`).
|
||||||
|
- **Ítems y power-ups**: disco/gaviota/pacmar (puntos), café (toque extra),
|
||||||
|
máquina de café (power-up), reloj (**detener el tiempo**,
|
||||||
|
`enableTimeStopItem`), *power ball*. Probabilidades en `Helper`
|
||||||
|
(`game.h:128`).
|
||||||
|
- **Colisiones**: jugador↔globo, jugador↔ítem, bala↔globo (`checkPlayer…`,
|
||||||
|
`checkBulletBalloonCollision`).
|
||||||
|
- **Muerte del jugador**: secuencia con *death shake* y fases
|
||||||
|
(`DeathSequence`/`DeathPhase`, `game.h:113`).
|
||||||
|
- **Marcador, hi-score, fades, fondo** (nubes con parallax via `MovingSprite`),
|
||||||
|
menús de pausa y game-over (`Menu`), audio (`Ja::Sound*`/`Ja::Music*`).
|
||||||
|
|
||||||
|
### 6.3. Gestión de memoria
|
||||||
|
|
||||||
|
Las entidades viven como **vectores de punteros crudos**
|
||||||
|
(`std::vector<Player*>`, `<Balloon*>`, `<Bullet*>`, `<Item*>`,
|
||||||
|
`<SmartSprite*>`; `game.h:264`), creados con `new` y liberados con métodos
|
||||||
|
`freeBalloons()`, `freeBullets()`, `freeItems()`, `deleteAllVectorObjects()`.
|
||||||
|
Es un estilo más antiguo que el de la Arcade Edition (smart pointers).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Entidades
|
||||||
|
|
||||||
|
`source/game/entities/`:
|
||||||
|
|
||||||
|
- **`Player`** (`player.h`) — movimiento, disparo (tres direcciones),
|
||||||
|
animaciones, power-up, invulnerabilidad, vidas/score. Puede usar teclado o
|
||||||
|
mando.
|
||||||
|
- **`Balloon`** (`balloon.h`) — enemigo básico que rebota; al explotar puede
|
||||||
|
generar globos hijos. Tiene varios contadores de estado.
|
||||||
|
- **`Bullet`** (`bullet.h`) — proyectil con `Kind` (UP/LEFT/RIGHT) y estado de
|
||||||
|
power-up.
|
||||||
|
- **`Item`** (`item.h`) — power-ups y objetos de puntos que caen, con `Id` por
|
||||||
|
tipo.
|
||||||
|
|
||||||
|
No hay clase base de entidad común ni managers: el ciclo de vida lo lleva
|
||||||
|
`Game` directamente sobre los vectores (§6.3). Los efectos visuales tipo
|
||||||
|
"popup de puntuación" o "café arrojado" se modelan como `SmartSprite`
|
||||||
|
(`core/rendering/smartsprite.h`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Modo demo y attract mode
|
||||||
|
|
||||||
|
> **El modo demo SÍ existe, y NO es IA**: es **reproducción de input
|
||||||
|
> pregrabado**, igual concepto que en la Arcade Edition.
|
||||||
|
|
||||||
|
### 8.1. Formato
|
||||||
|
|
||||||
|
`source/core/system/demo.hpp`: cada fotograma 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 "a 60 Hz de
|
||||||
|
referencia" (`demo.hpp:9`). Hay tres ficheros: `data/demo/demo{1,2,3}.bin`. El
|
||||||
|
playback es **time-based**: `index = elapsed_s * 60`.
|
||||||
|
|
||||||
|
### 8.2. Reproducción
|
||||||
|
|
||||||
|
Cuando un `Game` corre en modo demo, `processDemoInput()` (`game.cpp`) lee el
|
||||||
|
fotograma actual del set seleccionado y lo inyecta por la **misma vía que un
|
||||||
|
humano** sobre `players_[0]`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
const DemoKeys &keys = dd.at(demo_.index % dd.size());
|
||||||
|
if (keys.left == 1) players_[0]->setInput(Input::Action::LEFT);
|
||||||
|
if (keys.fire == 1 && players_[0]->canFire()) {
|
||||||
|
players_[0]->setInput(Input::Action::FIRE_CENTER);
|
||||||
|
createBullet(...); players_[0]->setFireCooldown(10);
|
||||||
|
}
|
||||||
|
// … (right, no_input, fire_left, fire_right)
|
||||||
|
```
|
||||||
|
|
||||||
|
No hay toma de decisiones: repite las pulsaciones grabadas. Al agotar el
|
||||||
|
playback (`index >= TOTAL_DEMO_DATA`) vuelve a `Title`.
|
||||||
|
|
||||||
|
### 8.3. Attract mode (dentro de `Title`)
|
||||||
|
|
||||||
|
El bucle de atracción vive en `source/game/scenes/title.cpp`: el Title arma un
|
||||||
|
*timeout* (`demo_remaining_s_`) y, al agotarse, lanza un **`Game` anidado en
|
||||||
|
modo demo** (`runDemoGame()`, `demo_game_`, `demo_game_active_`;
|
||||||
|
`title.cpp:323`). Title tiquea ese `demo_game_->iterate()` directamente y, al
|
||||||
|
terminar la demo, encadena las **instrucciones**
|
||||||
|
(`demo_then_instructions_` → `runInstructions(Instructions::Mode::AUTO)`,
|
||||||
|
`title.cpp:334`) antes de volver al título. Así el Title alterna
|
||||||
|
atracción → demo → instrucciones de forma autónoma.
|
||||||
|
|
||||||
|
> Es una diferencia notable con la Arcade Edition, donde la demo es una sección
|
||||||
|
> `GAME_DEMO` propia del Director. Aquí el `Director` ni se entera: todo el
|
||||||
|
> attract está encapsulado en `Title`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Recursos
|
||||||
|
|
||||||
|
- **`Asset`** (`core/resources/asset.h`) — índice de ficheros de recurso
|
||||||
|
(`add`/`get` por nombre).
|
||||||
|
- **`Resource`** (`core/resources/resource.h`) — carga y caché de los recursos
|
||||||
|
(texturas, sonidos, música, fuentes, animaciones).
|
||||||
|
- **Pack**: `resource_pack.*` + `resource_loader.*` + `resource_helper.*`
|
||||||
|
sirven desde **`resources.pack`**, con *fallback* al filesystem en desarrollo.
|
||||||
|
- **Formatos**: PNG (spritesheets) + ficheros **`.ani`** (definición de
|
||||||
|
animaciones); OGG (audio, vía `stb_vorbis`); fuentes bitmap en `data/font/`.
|
||||||
|
Los shaders GLSL de `data/shaders/` **no** van al pack (se embeben en el
|
||||||
|
binario como cabeceras SPIR-V).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Audio
|
||||||
|
|
||||||
|
`source/core/audio/` — `Audio` (`audio.hpp`) + `audio_adapter` sobre
|
||||||
|
**`jail_audio`** (`jail_audio.hpp`), wrapper de audio SDL3 *first-party* (no
|
||||||
|
librería externa) que usa `stb_vorbis` para OGG y mezcla por canales (API
|
||||||
|
`JA_*`). `Game` mantiene punteros `Ja::Sound*` para cada efecto (explosión,
|
||||||
|
disparo, colisión, reloj, etc.) y un `Ja::Music* game_music_`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Configuración y constantes
|
||||||
|
|
||||||
|
- **`Options`** (`source/game/options.hpp`) — opciones persistentes en el
|
||||||
|
namespace `Options::` (`window`, `video` con `gpu`/`shader`, `audio`,
|
||||||
|
`loading`, `settings`, `gameplay`, `inputs`), más presets `PostFXPreset` y
|
||||||
|
`CrtPiPreset`.
|
||||||
|
|
||||||
|
> ⚠️ **El `CLAUDE.md` está desactualizado en este punto**: dice que la config
|
||||||
|
> vive en `config.txt` con "migración a YAML pendiente". El código real
|
||||||
|
> (`options.hpp:16`) ya **persiste en `config.yaml` vía fkyaml**, con presets
|
||||||
|
> de shaders en `postfx.yaml`/`crtpi.yaml`. El código manda.
|
||||||
|
|
||||||
|
- **`defaults.hpp`** (`source/game/`) — constantes de gameplay y layout: tamaño
|
||||||
|
de canvas (256×192), `BLOCK`, áreas de juego, colores, y las constantes
|
||||||
|
`SECTION_PROG_*` / `SUBSECTION_*` del flujo (§3).
|
||||||
|
- **`utils/defines.hpp`** — macros de build.
|
||||||
|
|
||||||
|
### Builds condicionales
|
||||||
|
|
||||||
|
Aparecen sobre todo en `Director`/`Screen`: `__EMSCRIPTEN__` (web: no se puede
|
||||||
|
salir, reinicia al logo; `NO_SHADERS` forzado), `DEBUG`, y la selección de
|
||||||
|
plataforma para shaders (SPIR-V vs Metal). `make release` empaqueta `.tar.gz` /
|
||||||
|
`.dmg` / `.zip` según el SO.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Localización
|
||||||
|
|
||||||
|
`source/core/locale/lang.*` — `Lang` carga las cadenas desde `data/lang/`
|
||||||
|
(es_ES, ba_BA/euskera, en_UK). El idioma se elige en `Options::settings.language`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Convenciones y patrones recurrentes
|
||||||
|
|
||||||
|
- **Cabeceras mixtas `.h` / `.hpp`**: `.h` en lo antiguo, `.hpp` en lo nuevo —
|
||||||
|
pista fiable de qué módulos se han reescrito.
|
||||||
|
- **Punteros crudos + `new`/`delete`** en el gameplay (`Game`), frente a smart
|
||||||
|
pointers en el resto (secciones, `Screen`). Migración a medias.
|
||||||
|
- **Migración frame-based → time-based**: contadores duplicados
|
||||||
|
(`x_counter_` + `x_counter_s_`) conviviendo; el reloj es `DeltaTime::tick()`.
|
||||||
|
- **Flujo por `struct Section` + constantes `SECTION_PROG_*`** (no enums
|
||||||
|
tipados ni objetos de transición).
|
||||||
|
- **Sub-estados dentro de la sección** (pausa/game-over como `subsection` de
|
||||||
|
`Game`), no como secciones del `Director`.
|
||||||
|
- **Attract mode encapsulado en `Title`** (demo + instrucciones).
|
||||||
|
- **`Game` monolítico**: la lógica no está repartida en managers; todo cuelga
|
||||||
|
de la clase `Game` y de structs internos (`Stage`, `EnemyFormation`, …).
|
||||||
|
- **Comentarios** en español/valenciano; muchos `#include` con comentario
|
||||||
|
"// for X" (estilo IWYU).
|
||||||
|
- **El `CLAUDE.md` puede ir por detrás del código** (caso config.txt→YAML): ante
|
||||||
|
duda, manda el código.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Guía de navegación: "si quieres tocar X, mira Y"
|
||||||
|
|
||||||
|
| Quiero… | Empieza por… |
|
||||||
|
|---|---|
|
||||||
|
| Entender el arranque | `source/core/system/director.cpp` |
|
||||||
|
| Cambiar el flujo de pantallas | `struct Section` (`utils/utils.h`) + constantes en `game/defaults.hpp` + `handleSectionTransition` |
|
||||||
|
| Añadir/editar una pantalla | `source/game/scenes/` (Logo/Intro/Title/Instructions) |
|
||||||
|
| Gestión del tiempo | `source/core/system/delta_time.*` |
|
||||||
|
| Cómo se dibuja todo | `Screen::start`/render (`core/rendering/screen.cpp`) |
|
||||||
|
| Canvas / resolución / áreas | `source/game/defaults.hpp` (256×192, BLOCK) |
|
||||||
|
| Sprites / animaciones `.ani` | `core/rendering/sprite.h` + `animatedsprite.h` + `texture.h` |
|
||||||
|
| Shaders / CRT / post-FX | `core/rendering/sdl3gpu/` + `data/shaders/` + `Options` |
|
||||||
|
| Modos de escalado / efectos | `Screen` + `Options::video.presentation_mode` |
|
||||||
|
| Controles / mandos | `core/input/input.h` |
|
||||||
|
| Hotkeys / salida en dos pasos | `core/input/global_inputs.cpp` |
|
||||||
|
| **Toda la lógica de partida** | `source/game/game.cpp` (`iteratePlaying/Paused/GameOver`) |
|
||||||
|
| Fases / formaciones / amenaza | `Game::initEnemyFormations*`, `Stage stage_[10]`, `updateMenace` |
|
||||||
|
| Globos / balas / ítems | `game/entities/{balloon,bullet,item}.*` (gestionados en `Game`) |
|
||||||
|
| El jugador | `game/entities/player.*` |
|
||||||
|
| Ítems y power-ups | `Game::dropItem/createItem`, `Helper` (`game.h`) |
|
||||||
|
| **Modo demo / attract** | `core/system/demo.*`, `Game::processDemoInput`, `scenes/title.cpp` (`runDemoGame`) |
|
||||||
|
| Cargar un recurso | `core/resources/asset.h` + `resource.h` |
|
||||||
|
| Audio | `core/audio/audio.hpp` + `jail_audio.hpp` |
|
||||||
|
| Opciones del usuario | `game/options.hpp` (+ `config.yaml`) |
|
||||||
|
| Valores por defecto / constantes | `game/defaults.hpp`, `utils/defines.hpp` |
|
||||||
|
| Idiomas | `core/locale/lang.*` + `data/lang/` |
|
||||||
|
| Empaquetar datos | `tools/` + `make pack` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Diferencias frente a la Arcade Edition
|
||||||
|
|
||||||
|
`coffee_crisis` es el **predecesor** de `coffee_crisis_arcade_edition`. Ambos
|
||||||
|
comparten ADN (SDL3, `jail_audio`, demo por input grabado, backend SDL3 GPU con
|
||||||
|
PostFX/CrtPi, capas core/game), pero el código diverge de forma sistemática.
|
||||||
|
Resumen de lo observado leyendo ambos repos:
|
||||||
|
|
||||||
|
| Dimensión | **Coffee Crisis (este)** | **Arcade Edition** |
|
||||||
|
|---|---|---|
|
||||||
|
| Tamaño | ~51 ficheros, ~16k LOC | ~150 ficheros, ~32k LOC |
|
||||||
|
| Cabeceras | mixto `.h` (antiguo) / `.hpp` | todo `.hpp` |
|
||||||
|
| Flujo | `struct Section{name,subsection}` + `enum ActiveSection`; **4 secciones** (Logo/Intro/Title/Game) | variable global `Section::name` con **muchas más** (Preload, HiScore, Credits, GameDemo, Instructions…) |
|
||||||
|
| Arranque | directo | **no bloqueante** con sección `PRELOAD` + `Resource::loadStep(50ms)` |
|
||||||
|
| Gameplay | **`Game` monolítico**; formaciones/fases como structs internos | **managers** (`BalloonManager`, `BulletManager`, `StageManager` con `IStageInfo`) |
|
||||||
|
| Memoria de entidades | **punteros crudos** + `new`/`delete` | `shared_ptr`/`unique_ptr` + listas |
|
||||||
|
| Pausa / game-over | **sub-estados** dentro de `Game` (`subsection`) | FSM de estados de `Game` + managers dedicados |
|
||||||
|
| Demo / attract | **encapsulado en `Title`** (Game anidado en demo + instrucciones) | sección `GAME_DEMO` propia del Director + attract Title↔Logo/Demo |
|
||||||
|
| Canvas | **256×192** fijo (`defaults.hpp`) | parametrizable (`param_320x*.txt`) |
|
||||||
|
| Render | un único `game_canvas_` → readback → shader / fallback | dos render-targets (`canvas_` zona de juego → `game_canvas_`) → shader / fallback |
|
||||||
|
| Sprites | `Sprite/AnimatedSprite/MovingSprite/SmartSprite` | añade `PathSprite`, `CardSprite` |
|
||||||
|
| Reinicio en caliente | (no observado a nivel de `relaunch()`) | `Director::relaunch()` vía `execv` |
|
||||||
|
| Plataformas | Linux/macOS/Windows/Emscripten | + Raspberry Pi, Anbernic |
|
||||||
|
| Estado del código | **en migración**: frame→time-based, `config.txt`→YAML | más consolidado (YAML, time-based) |
|
||||||
|
|
||||||
|
En una frase: la Arcade Edition es esta misma idea **refactorizada y ampliada**
|
||||||
|
— se troceó el `Game` monolítico en managers, se pasó a smart pointers, se
|
||||||
|
añadieron secciones y plataformas, y se consolidó la migración a time-based y
|
||||||
|
YAML que aquí todavía está a medias.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*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.*
|
||||||
@@ -8,7 +8,12 @@
|
|||||||
// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp).
|
// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp).
|
||||||
// clang-format off
|
// clang-format off
|
||||||
#undef STB_VORBIS_HEADER_ONLY
|
#undef STB_VORBIS_HEADER_ONLY
|
||||||
|
// stb_vorbis (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"
|
||||||
#include "external/stb_vorbis.h"
|
#include "external/stb_vorbis.h"
|
||||||
|
#pragma GCC diagnostic pop
|
||||||
// stb_vorbis.c filtra les macros L, C i R (i PLAYBACK_*) al TU. Les netegem
|
// 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.
|
// perquè xocarien amb noms de paràmetres de plantilla en altres headers.
|
||||||
#undef L
|
#undef L
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
#include "core/rendering/notifications.hpp"
|
#include "core/rendering/notifications.hpp"
|
||||||
#include "core/rendering/screen.h"
|
#include "core/rendering/screen.h"
|
||||||
#include "game/options.hpp"
|
#include "game/options.hpp"
|
||||||
|
#include "utils/defines.hpp"
|
||||||
|
#include "version.h"
|
||||||
|
|
||||||
namespace GlobalInputs {
|
namespace GlobalInputs {
|
||||||
|
|
||||||
@@ -54,6 +56,23 @@ namespace GlobalInputs {
|
|||||||
Notifications::show(MSG, Notifications::Palette::SUCCESS, Notifications::STANDARD_MS);
|
Notifications::show(MSG, Notifications::Palette::SUCCESS, Notifications::STANDARD_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void notifyVersion() {
|
||||||
|
// Format: "<APP_NAME> v<VERSION> (<GIT_HASH>)"
|
||||||
|
const std::string MSG = std::string(Version::APP_NAME) + " v" + Texts::VERSION + " (" + Version::GIT_HASH + ")";
|
||||||
|
Notifications::show(MSG, Notifications::Palette::TOGGLE, Notifications::LONG_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifyVSync() {
|
||||||
|
const std::string STATE = Options::video.vsync ? "ON" : "OFF";
|
||||||
|
const std::string MSG = std::string("VSync ") + STATE;
|
||||||
|
Notifications::show(MSG, Notifications::Palette::TOGGLE, Notifications::STANDARD_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifyPresentationMode() {
|
||||||
|
const std::string MSG = std::string("Mode ") + Screen::getPresentationModeName();
|
||||||
|
Notifications::show(MSG, Notifications::Palette::CHOICE, Notifications::STANDARD_MS);
|
||||||
|
}
|
||||||
|
|
||||||
void onExit() {
|
void onExit() {
|
||||||
const Uint32 NOW = SDL_GetTicks();
|
const Uint32 NOW = SDL_GetTicks();
|
||||||
if (NOW < exit_window_until_ticks) {
|
if (NOW < exit_window_until_ticks) {
|
||||||
@@ -94,6 +113,24 @@ namespace GlobalInputs {
|
|||||||
notifyShaderEnabled();
|
notifyShaderEnabled();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (Input::get()->checkInput(Input::Action::SHOW_VERSION, Input::Repeat::OFF)) {
|
||||||
|
notifyVersion();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Input::get()->checkInput(Input::Action::TOGGLE_VSYNC, Input::Repeat::OFF)) {
|
||||||
|
Screen::get()->toggleVSync();
|
||||||
|
notifyVSync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Input::get()->checkInput(Input::Action::NEXT_PRESENTATION_MODE, Input::Repeat::OFF)) {
|
||||||
|
Screen::get()->nextPresentationMode();
|
||||||
|
notifyPresentationMode();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Input::get()->checkInput(Input::Action::TOGGLE_FPS, Input::Repeat::OFF)) {
|
||||||
|
Screen::get()->toggleFps();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// F5/F6 només actuen quan el post-procesado està actiu.
|
// F5/F6 només actuen quan el post-procesado està actiu.
|
||||||
if (Screen::isShaderEnabled()) {
|
if (Screen::isShaderEnabled()) {
|
||||||
if (Input::get()->checkInput(Input::Action::TOGGLE_SHADER_TYPE, Input::Repeat::OFF)) {
|
if (Input::get()->checkInput(Input::Action::TOGGLE_SHADER_TYPE, Input::Repeat::OFF)) {
|
||||||
|
|||||||
@@ -157,25 +157,45 @@ auto Input::checkGameControllerInput(Action input, Repeat repeat, int index) ->
|
|||||||
return PRESS_EDGE;
|
return PRESS_EDGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comprueba si hay almenos un input activo
|
// Comprueba si hay almenos un input "humano" activo (moviment, ACCEPT/CANCEL,
|
||||||
|
// FIRE_*). Exclou les accions reservades a hotkeys globals (EXIT, PAUSE,
|
||||||
|
// WINDOW_*, *SHADER*) perque prémer F1-F12 o ESC no s'ha de comptar com
|
||||||
|
// "qualsevol tecla" — ningu vol saltar una intro per modificar el zoom.
|
||||||
auto Input::checkAnyInput(Device device, int index) -> bool {
|
auto Input::checkAnyInput(Device device, int index) -> bool {
|
||||||
if (device == Device::ANY) {
|
if (device == Device::ANY) {
|
||||||
index = 0;
|
index = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto is_skippable = [](Action a) {
|
||||||
|
switch (a) {
|
||||||
|
case Action::UP:
|
||||||
|
case Action::DOWN:
|
||||||
|
case Action::LEFT:
|
||||||
|
case Action::RIGHT:
|
||||||
|
case Action::ACCEPT:
|
||||||
|
case Action::CANCEL:
|
||||||
|
case Action::FIRE_LEFT:
|
||||||
|
case Action::FIRE_CENTER:
|
||||||
|
case Action::FIRE_RIGHT:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (device == Device::KEYBOARD || device == Device::ANY) {
|
if (device == Device::KEYBOARD || device == Device::ANY) {
|
||||||
const bool *key_states = SDL_GetKeyboardState(nullptr);
|
const bool *key_states = SDL_GetKeyboardState(nullptr);
|
||||||
|
for (std::size_t i = 0; i < key_bindings_.size(); ++i) {
|
||||||
if (std::ranges::any_of(key_bindings_,
|
if (is_skippable(static_cast<Action>(i)) && key_states[key_bindings_[i].scancode]) {
|
||||||
[key_states](const auto &key_binding) { return key_states[key_binding.scancode]; })) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (gameControllerFound() && index >= 0 && index < (int)connected_controllers_.size()) {
|
if (gameControllerFound() && index >= 0 && index < (int)connected_controllers_.size()) {
|
||||||
if (device == Device::GAMECONTROLLER || device == Device::ANY) {
|
if (device == Device::GAMECONTROLLER || device == Device::ANY) {
|
||||||
for (auto &game_controller_binding : game_controller_bindings_) {
|
for (std::size_t i = 0; i < game_controller_bindings_.size(); ++i) {
|
||||||
if (SDL_GetGamepadButton(connected_controllers_[index], game_controller_binding.button)) {
|
if (is_skippable(static_cast<Action>(i)) && SDL_GetGamepadButton(connected_controllers_[index], game_controller_bindings_[i].button)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ class Input {
|
|||||||
TOGGLE_SHADER,
|
TOGGLE_SHADER,
|
||||||
TOGGLE_SHADER_TYPE,
|
TOGGLE_SHADER_TYPE,
|
||||||
|
|
||||||
|
// Diagnostic / video toggles
|
||||||
|
SHOW_VERSION,
|
||||||
|
TOGGLE_VSYNC,
|
||||||
|
NEXT_PRESENTATION_MODE,
|
||||||
|
TOGGLE_FPS,
|
||||||
|
|
||||||
// Centinela final (usar para sizing)
|
// Centinela final (usar para sizing)
|
||||||
NUMBER_OF_INPUTS
|
NUMBER_OF_INPUTS
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ void AnimatedSprite::update(float dt_s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Establece el rectangulo para un frame de una animación
|
// Establece el rectangulo para un frame de una animación
|
||||||
void AnimatedSprite::setAnimationFrames(Uint8 index_animation, Uint8 index_frame, int x, int y, int w, int h) {
|
void AnimatedSprite::setAnimationFrames(Uint8 index_animation, Uint8 /*index_frame*/, int x, int y, int w, int h) {
|
||||||
animation_[index_animation].frames.push_back({x, y, w, h});
|
animation_[index_animation].frames.push_back({x, y, w, h});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,4 @@ class Fade {
|
|||||||
Uint32 last_square_ticks_ = 0; // Ticks del último cuadrado dibujado (RANDOM_SQUARE)
|
Uint32 last_square_ticks_ = 0; // Ticks del último cuadrado dibujado (RANDOM_SQUARE)
|
||||||
Uint16 squares_drawn_ = 0; // Número de cuadrados dibujados (RANDOM_SQUARE)
|
Uint16 squares_drawn_ = 0; // Número de cuadrados dibujados (RANDOM_SQUARE)
|
||||||
bool fullscreen_done_ = false; // Indica si el fade fullscreen ha terminado la fase de fundido
|
bool fullscreen_done_ = false; // Indica si el fade fullscreen ha terminado la fase de fundido
|
||||||
SDL_Rect rect1_{}; // Rectangulo usado para crear los efectos de transición
|
|
||||||
SDL_Rect rect2_{}; // Rectangulo usado para crear los efectos de transición
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
namespace Notifications {
|
namespace Notifications {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Paleta pastel. Per a tunejar l'aparença només cal tocar aquí.
|
// Paleta semi-saturada: a mig cami entre pastel i color "pur". Manté
|
||||||
|
// contrast del outline (foscor) sense diluir el matís.
|
||||||
// (Color no és literal type ⇒ const, no constexpr.)
|
// (Color no és literal type ⇒ const, no constexpr.)
|
||||||
const Color INFO_COLOR{0xF0, 0xE0, 0x90}; // groc trigo
|
const Color INFO_COLOR{0xF0, 0xD0, 0x40}; // groc
|
||||||
const Color TOGGLE_COLOR{0xA0, 0xE0, 0xF0}; // cian gel
|
const Color TOGGLE_COLOR{0x60, 0xC0, 0xF0}; // cian
|
||||||
const Color CHOICE_COLOR{0xE0, 0xA0, 0xE0}; // rosa orquídia
|
const Color CHOICE_COLOR{0xD0, 0x60, 0xD0}; // magenta
|
||||||
const Color SUCCESS_COLOR{0xB0, 0xE6, 0xB0}; // verd menta
|
const Color SUCCESS_COLOR{0x70, 0xD0, 0x70}; // verd
|
||||||
const Color DANGER_COLOR{0xF0, 0xA0, 0xA0}; // rosa salmó
|
const Color DANGER_COLOR{0xF0, 0x60, 0x60}; // vermell
|
||||||
|
|
||||||
// Factor de foscor per a l'outline (~40% de la lluminositat del
|
// Factor de foscor per a l'outline (~40% de la lluminositat del
|
||||||
// color base): manté el matís i queda prou fosc per a contrastar
|
// color base): manté el matís i queda prou fosc per a contrastar
|
||||||
|
|||||||
@@ -169,9 +169,12 @@ void Screen::start() {
|
|||||||
|
|
||||||
// Vuelca el contenido del renderizador en pantalla
|
// Vuelca el contenido del renderizador en pantalla
|
||||||
void Screen::blit() {
|
void Screen::blit() {
|
||||||
// Dibuja la notificación activa sobre el gameCanvas antes de presentar
|
updateFps();
|
||||||
|
|
||||||
|
// Dibuja la notificación activa i, si toca, l'overlay de FPS sobre el gameCanvas
|
||||||
SDL_SetRenderTarget(renderer_, game_canvas_);
|
SDL_SetRenderTarget(renderer_, game_canvas_);
|
||||||
renderNotification();
|
renderNotification();
|
||||||
|
renderFps();
|
||||||
|
|
||||||
#ifndef NO_SHADERS
|
#ifndef NO_SHADERS
|
||||||
// Si el backend GPU està viu i accelerat, passem sempre per ell (tant amb
|
// Si el backend GPU està viu i accelerat, passem sempre per ell (tant amb
|
||||||
@@ -326,16 +329,27 @@ void Screen::detectMaxZoom() {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Establece el escalado entero
|
// Estableix el mode de presentacio del canvas i reaplica el layout
|
||||||
void Screen::setIntegerScale(bool enabled) {
|
void Screen::setPresentationMode(Options::PresentationMode mode) {
|
||||||
if (Options::video.integer_scale == enabled) { return; }
|
if (Options::video.presentation_mode == mode) { return; }
|
||||||
Options::video.integer_scale = enabled;
|
Options::video.presentation_mode = mode;
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
if (shader_backend_) {
|
||||||
|
shader_backend_->setPresentationMode(static_cast<Rendering::ShaderBackend::PresentationMode>(mode));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
setVideoMode(Options::video.fullscreen);
|
setVideoMode(Options::video.fullscreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alterna el escalado entero
|
// Cicla integer_scale -> letterbox -> stretched -> overscan -> integer_scale
|
||||||
void Screen::toggleIntegerScale() {
|
void Screen::nextPresentationMode() {
|
||||||
setIntegerScale(!Options::video.integer_scale);
|
setPresentationMode(Options::nextPresentationMode(Options::video.presentation_mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nom curt del mode actual (per a notificacions). Static perque no necessita
|
||||||
|
// estat d'instancia: nomes consulta Options::video.
|
||||||
|
auto Screen::getPresentationModeName() -> const char * {
|
||||||
|
return Options::presentationModeToString(Options::video.presentation_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Establece el V-Sync
|
// Establece el V-Sync
|
||||||
@@ -407,39 +421,75 @@ void Screen::applyFullscreenLayout() {
|
|||||||
computeFullscreenGameRect();
|
computeFullscreenGameRect();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calcula el rectángulo dest para fullscreen: integer_scale / aspect ratio
|
// Calcula el rectangle dest segons el PresentationMode actiu.
|
||||||
|
// INTEGER_SCALE: x sencera maxima (1x, 2x, 3x...) centrada amb barres.
|
||||||
|
// LETTERBOX: mante aspect ratio, ajusta al menor dels eixos, barres.
|
||||||
|
// STRETCHED: omple tota la finestra deformant la relacio d'aspecte.
|
||||||
|
// OVERSCAN: mante aspect ratio omplint la finestra (retalla el sobrant).
|
||||||
void Screen::computeFullscreenGameRect() {
|
void Screen::computeFullscreenGameRect() {
|
||||||
if (Options::video.integer_scale) {
|
const float CANVAS_RATIO = static_cast<float>(game_canvas_width_) / static_cast<float>(game_canvas_height_);
|
||||||
// Calcula el tamaño de la escala máxima
|
const float WINDOW_RATIO = static_cast<float>(window_width_) / static_cast<float>(window_height_);
|
||||||
|
|
||||||
|
switch (Options::video.presentation_mode) {
|
||||||
|
case Options::PresentationMode::INTEGER_SCALE: {
|
||||||
int scale = 0;
|
int scale = 0;
|
||||||
while (((game_canvas_width_ * (scale + 1)) <= window_width_) && ((game_canvas_height_ * (scale + 1)) <= window_height_)) {
|
while (((game_canvas_width_ * (scale + 1)) <= window_width_) && ((game_canvas_height_ * (scale + 1)) <= window_height_)) {
|
||||||
scale++;
|
scale++;
|
||||||
}
|
}
|
||||||
|
|
||||||
dest_.w = game_canvas_width_ * scale;
|
dest_.w = game_canvas_width_ * scale;
|
||||||
dest_.h = game_canvas_height_ * scale;
|
dest_.h = game_canvas_height_ * scale;
|
||||||
dest_.x = (window_width_ - dest_.w) / 2;
|
break;
|
||||||
dest_.y = (window_height_ - dest_.h) / 2;
|
}
|
||||||
} else {
|
case Options::PresentationMode::LETTERBOX: {
|
||||||
// Manté la relació d'aspecte sense escalat enter (letterbox/pillarbox).
|
if (WINDOW_RATIO >= CANVAS_RATIO) {
|
||||||
float ratio = (float)game_canvas_width_ / (float)game_canvas_height_;
|
|
||||||
if ((window_width_ - game_canvas_width_) >= (window_height_ - game_canvas_height_)) {
|
|
||||||
dest_.h = window_height_;
|
dest_.h = window_height_;
|
||||||
dest_.w = static_cast<int>(std::lround(window_height_ * ratio));
|
dest_.w = static_cast<int>(std::lround(window_height_ * CANVAS_RATIO));
|
||||||
dest_.x = (window_width_ - dest_.w) / 2;
|
|
||||||
dest_.y = (window_height_ - dest_.h) / 2;
|
|
||||||
} else {
|
} else {
|
||||||
dest_.w = window_width_;
|
dest_.w = window_width_;
|
||||||
dest_.h = static_cast<int>(std::lround(window_width_ / ratio));
|
dest_.h = static_cast<int>(std::lround(window_width_ / CANVAS_RATIO));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Options::PresentationMode::STRETCHED: {
|
||||||
|
dest_.w = window_width_;
|
||||||
|
dest_.h = window_height_;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Options::PresentationMode::OVERSCAN: {
|
||||||
|
// Mante aspect: dimensiona al major dels eixos (l'altre desborda).
|
||||||
|
if (WINDOW_RATIO >= CANVAS_RATIO) {
|
||||||
|
dest_.w = window_width_;
|
||||||
|
dest_.h = static_cast<int>(std::lround(window_width_ / CANVAS_RATIO));
|
||||||
|
} else {
|
||||||
|
dest_.h = window_height_;
|
||||||
|
dest_.w = static_cast<int>(std::lround(window_height_ * CANVAS_RATIO));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
dest_.x = (window_width_ - dest_.w) / 2;
|
dest_.x = (window_width_ - dest_.w) / 2;
|
||||||
dest_.y = (window_height_ - dest_.h) / 2;
|
dest_.y = (window_height_ - dest_.h) / 2;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aplica la logical presentation y persiste el estado en options
|
// Aplica la logical presentation segons el PresentationMode actiu (ruta SDL_Renderer fallback).
|
||||||
|
// La ruta GPU calcula el viewport ella mateixa via computeViewport().
|
||||||
void Screen::applyLogicalPresentation(bool fullscreen) {
|
void Screen::applyLogicalPresentation(bool fullscreen) {
|
||||||
SDL_SetRenderLogicalPresentation(renderer_, window_width_, window_height_, SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
SDL_RendererLogicalPresentation lp = SDL_LOGICAL_PRESENTATION_LETTERBOX;
|
||||||
|
switch (Options::video.presentation_mode) {
|
||||||
|
case Options::PresentationMode::INTEGER_SCALE:
|
||||||
|
lp = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
|
||||||
|
break;
|
||||||
|
case Options::PresentationMode::LETTERBOX:
|
||||||
|
lp = SDL_LOGICAL_PRESENTATION_LETTERBOX;
|
||||||
|
break;
|
||||||
|
case Options::PresentationMode::STRETCHED:
|
||||||
|
lp = SDL_LOGICAL_PRESENTATION_STRETCH;
|
||||||
|
break;
|
||||||
|
case Options::PresentationMode::OVERSCAN:
|
||||||
|
lp = SDL_LOGICAL_PRESENTATION_OVERSCAN;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
SDL_SetRenderLogicalPresentation(renderer_, window_width_, window_height_, lp);
|
||||||
Options::video.fullscreen = fullscreen;
|
Options::video.fullscreen = fullscreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,14 +511,16 @@ void Screen::clearNotification() {
|
|||||||
notification_message_.clear();
|
notification_message_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dibuja la notificación activa (si la hay) sobre el gameCanvas
|
// Dibuja la notificación activa (si la hay) sobre el gameCanvas. La Y es
|
||||||
|
// el `notification_y_` configurat, desplacat cap avall si en overscan part
|
||||||
|
// de la franja superior queda fora de pantalla.
|
||||||
void Screen::renderNotification() {
|
void Screen::renderNotification() {
|
||||||
if (notification_text_ == nullptr || SDL_GetTicks() >= notification_end_time_) {
|
if (notification_text_ == nullptr || SDL_GetTicks() >= notification_end_time_) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
notification_text_->writeDX(Text::FLAG_CENTER | Text::FLAG_COLOR | Text::FLAG_STROKE,
|
notification_text_->writeDX(Text::FLAG_CENTER | Text::FLAG_COLOR | Text::FLAG_STROKE,
|
||||||
game_canvas_width_ / 2,
|
game_canvas_width_ / 2,
|
||||||
notification_y_,
|
notification_y_ + safeNotificationY(),
|
||||||
notification_message_,
|
notification_message_,
|
||||||
1,
|
1,
|
||||||
notification_text_color_,
|
notification_text_color_,
|
||||||
@@ -476,6 +528,70 @@ void Screen::renderNotification() {
|
|||||||
notification_outline_color_);
|
notification_outline_color_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alterna la visibilitat de l'overlay de FPS. No persisteix a config.
|
||||||
|
void Screen::toggleFps() {
|
||||||
|
fps_visible_ = !fps_visible_;
|
||||||
|
if (fps_visible_) {
|
||||||
|
fps_window_start_ticks_ = SDL_GetTicks();
|
||||||
|
fps_frame_count_ = 0;
|
||||||
|
fps_value_ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Screen::isFpsVisible() const -> bool {
|
||||||
|
return fps_visible_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acumula frames i recalcula el FPS cada segon real (no afectat per dt del joc).
|
||||||
|
// Cridat des de blit() una vegada per frame.
|
||||||
|
void Screen::updateFps() {
|
||||||
|
if (!fps_visible_) { return; }
|
||||||
|
++fps_frame_count_;
|
||||||
|
const Uint32 NOW = SDL_GetTicks();
|
||||||
|
const Uint32 ELAPSED = NOW - fps_window_start_ticks_;
|
||||||
|
if (ELAPSED >= 1000) {
|
||||||
|
fps_value_ = static_cast<int>((static_cast<Uint64>(fps_frame_count_) * 1000) / ELAPSED);
|
||||||
|
fps_frame_count_ = 0;
|
||||||
|
fps_window_start_ticks_ = NOW;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dibuixa "NN FPS" a dalt a la dreta del canvas, mateixa Y que les notificacions
|
||||||
|
// (incloent l'ajust per overscan) i amb la mateixa font 8bithud.
|
||||||
|
void Screen::renderFps() {
|
||||||
|
if (!fps_visible_ || notification_text_ == nullptr) { return; }
|
||||||
|
constexpr int RIGHT_MARGIN = 2;
|
||||||
|
const Color FPS_COLOR{0x60, 0xD0, 0x70}; // verd (mateix to que SUCCESS de notificacions)
|
||||||
|
const Color FPS_OUTLINE{0x26, 0x53, 0x2C}; // ~40% darken del verd
|
||||||
|
const std::string MSG = std::to_string(fps_value_) + " FPS";
|
||||||
|
const int TEXT_W = notification_text_->lenght(MSG, 1);
|
||||||
|
const int X = game_canvas_width_ - TEXT_W - RIGHT_MARGIN;
|
||||||
|
const int Y = notification_y_ + safeNotificationY();
|
||||||
|
notification_text_->writeDX(Text::FLAG_COLOR | Text::FLAG_STROKE,
|
||||||
|
X,
|
||||||
|
Y,
|
||||||
|
MSG,
|
||||||
|
1,
|
||||||
|
FPS_COLOR,
|
||||||
|
1,
|
||||||
|
FPS_OUTLINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Y minima del canvas visible. Solo no zero quan estem en overscan i l'aspect
|
||||||
|
// de finestra obliga a escalar mes ample que alt (el canvas vertical desborda
|
||||||
|
// i la franja superior es retalla). En cas contrari (qualsevol altre mode, o
|
||||||
|
// overscan amb retall horitzontal nomes), retorna 0.
|
||||||
|
auto Screen::safeNotificationY() const -> int {
|
||||||
|
if (Options::video.presentation_mode != Options::PresentationMode::OVERSCAN) { return 0; }
|
||||||
|
if (window_width_ <= 0 || window_height_ <= 0 || game_canvas_height_ <= 0) { return 0; }
|
||||||
|
const float CANVAS_RATIO = static_cast<float>(game_canvas_width_) / static_cast<float>(game_canvas_height_);
|
||||||
|
const float WINDOW_RATIO = static_cast<float>(window_width_) / static_cast<float>(window_height_);
|
||||||
|
if (WINDOW_RATIO < CANVAS_RATIO) { return 0; } // retall horitzontal nomes
|
||||||
|
const float OVERSCAN_SCALE = static_cast<float>(window_width_) / static_cast<float>(game_canvas_width_);
|
||||||
|
const float VH = static_cast<float>(game_canvas_height_) * OVERSCAN_SCALE;
|
||||||
|
return static_cast<int>(std::ceil((VH - static_cast<float>(window_height_)) / (2.0F * OVERSCAN_SCALE)));
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Emscripten — fix per a fullscreen/resize (veure el bloc de comentaris al
|
// Emscripten — fix per a fullscreen/resize (veure el bloc de comentaris al
|
||||||
// principi del fitxer i l'anonymous namespace amb els callbacks natius).
|
// principi del fitxer i l'anonymous namespace amb els callbacks natius).
|
||||||
@@ -545,7 +661,7 @@ void Screen::initShaders() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (shader_backend_->isHardwareAccelerated()) {
|
if (shader_backend_->isHardwareAccelerated()) {
|
||||||
shader_backend_->setScaleMode(Options::video.integer_scale);
|
shader_backend_->setPresentationMode(static_cast<Rendering::ShaderBackend::PresentationMode>(Options::video.presentation_mode));
|
||||||
shader_backend_->setVSync(Options::video.vsync);
|
shader_backend_->setVSync(Options::video.vsync);
|
||||||
|
|
||||||
// Resol els índexs de preset a partir del nom emmagatzemat al config.
|
// Resol els índexs de preset a partir del nom emmagatzemat al config.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <string> // for string
|
#include <string> // for string
|
||||||
#include <vector> // for vector
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include "game/options.hpp" // for Options::PresentationMode
|
||||||
#include "utils/utils.h" // for Color
|
#include "utils/utils.h" // for Color
|
||||||
|
|
||||||
#ifndef NO_SHADERS
|
#ifndef NO_SHADERS
|
||||||
@@ -55,8 +56,9 @@ class Screen {
|
|||||||
void toggleVideoMode(); // Cambia entre pantalla completa y ventana
|
void toggleVideoMode(); // Cambia entre pantalla completa y ventana
|
||||||
void handleCanvasResized(); // En Emscripten, reaplica setVideoMode tras un cambio del navegador (salida de fullscreen con Esc, rotación). No-op fuera de Emscripten
|
void handleCanvasResized(); // En Emscripten, reaplica setVideoMode tras un cambio del navegador (salida de fullscreen con Esc, rotación). No-op fuera de Emscripten
|
||||||
static void syncFullscreenFlagFromBrowser(bool is_fullscreen); // Sincroniza el flag interno de fullscreen con el estado real del navegador. Debe llamarse antes de diferir handleCanvasResized. No-op fuera de Emscripten
|
static void syncFullscreenFlagFromBrowser(bool is_fullscreen); // Sincroniza el flag interno de fullscreen con el estado real del navegador. Debe llamarse antes de diferir handleCanvasResized. No-op fuera de Emscripten
|
||||||
void toggleIntegerScale(); // Alterna el escalado entero
|
void nextPresentationMode(); // Cicla integer_scale -> letterbox -> stretched -> overscan
|
||||||
void setIntegerScale(bool enabled); // Establece el escalado entero
|
void setPresentationMode(Options::PresentationMode mode); // Estableix el mode de presentacio del canvas
|
||||||
|
[[nodiscard]] static auto getPresentationModeName() -> const char *; // Nom curt del mode actual (per a notificacions)
|
||||||
void toggleVSync(); // Alterna el V-Sync
|
void toggleVSync(); // Alterna el V-Sync
|
||||||
void setVSync(bool enabled); // Establece el V-Sync
|
void setVSync(bool enabled); // Establece el V-Sync
|
||||||
auto decWindowZoom() -> bool; // Reduce el zoom de la ventana (devuelve true si cambió)
|
auto decWindowZoom() -> bool; // Reduce el zoom de la ventana (devuelve true si cambió)
|
||||||
@@ -71,6 +73,10 @@ class Screen {
|
|||||||
void notify(const std::string &text, Color text_color, Color outline_color, Uint32 duration_ms); // Muestra una notificación en la línea superior del canvas durante durationMs. Sobrescribe cualquier notificación activa (sin apilación).
|
void notify(const std::string &text, Color text_color, Color outline_color, Uint32 duration_ms); // Muestra una notificación en la línea superior del canvas durante durationMs. Sobrescribe cualquier notificación activa (sin apilación).
|
||||||
void clearNotification(); // Limpia la notificación actual
|
void clearNotification(); // Limpia la notificación actual
|
||||||
|
|
||||||
|
// FPS overlay (debug, no persistent)
|
||||||
|
void toggleFps(); // Alterna la visibilitat de l'overlay de FPS
|
||||||
|
[[nodiscard]] auto isFpsVisible() const -> bool; // Estat actual
|
||||||
|
|
||||||
// GPU / shaders (post-procesado). En builds con NO_SHADERS (Emscripten) son no-op.
|
// GPU / shaders (post-procesado). En builds con NO_SHADERS (Emscripten) son no-op.
|
||||||
void initShaders(); // Crea el backend GPU si no existe y lo inicializa
|
void initShaders(); // Crea el backend GPU si no existe y lo inicializa
|
||||||
void shutdownShaders(); // Libera el backend GPU
|
void shutdownShaders(); // Libera el backend GPU
|
||||||
@@ -111,6 +117,11 @@ class Screen {
|
|||||||
|
|
||||||
// Notificaciones
|
// Notificaciones
|
||||||
void renderNotification(); // Dibuja la notificación activa (si la hay) sobre el gameCanvas
|
void renderNotification(); // Dibuja la notificación activa (si la hay) sobre el gameCanvas
|
||||||
|
[[nodiscard]] auto safeNotificationY() const -> int; // Y minima dins del canvas que segueix sent visible en overscan (segons aspect ratio finestra/canvas)
|
||||||
|
|
||||||
|
// FPS overlay
|
||||||
|
void updateFps(); // Acumula temps i recalcula fps cada segon (a cridar des de blit)
|
||||||
|
void renderFps(); // Dibuixa "NN FPS" a dalt a la dreta del canvas
|
||||||
|
|
||||||
#ifndef NO_SHADERS
|
#ifndef NO_SHADERS
|
||||||
// Aplica els paràmetres actuals del shader al backend segons options
|
// Aplica els paràmetres actuals del shader al backend segons options
|
||||||
@@ -139,6 +150,12 @@ class Screen {
|
|||||||
Uint32 notification_end_time_; // SDL_GetTicks() hasta el cual se muestra
|
Uint32 notification_end_time_; // SDL_GetTicks() hasta el cual se muestra
|
||||||
int notification_y_; // Fila vertical en el canvas virtual
|
int notification_y_; // Fila vertical en el canvas virtual
|
||||||
|
|
||||||
|
// FPS overlay (debug, no persistent)
|
||||||
|
bool fps_visible_{false}; // F10 toggle
|
||||||
|
int fps_value_{0}; // Ultim valor calculat (frames per segon)
|
||||||
|
int fps_frame_count_{0}; // Frames acumulats durant la finestra actual
|
||||||
|
Uint32 fps_window_start_ticks_{0}; // Inici de la finestra d'1s actual (SDL_GetTicks)
|
||||||
|
|
||||||
#ifndef NO_SHADERS
|
#ifndef NO_SHADERS
|
||||||
// GPU / shaders
|
// GPU / shaders
|
||||||
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; // Backend GPU (nullptr si inactivo)
|
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; // Backend GPU (nullptr si inactivo)
|
||||||
|
|||||||
@@ -292,21 +292,48 @@ namespace Rendering {
|
|||||||
// computeViewport — dimensions lògiques del canvas dins del swapchain (letterbox)
|
// computeViewport — dimensions lògiques del canvas dins del swapchain (letterbox)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
auto SDL3GPUShader::computeViewport(Uint32 sw, Uint32 sh) const -> Viewport {
|
auto SDL3GPUShader::computeViewport(Uint32 sw, Uint32 sh) const -> Viewport {
|
||||||
|
const auto SWF = static_cast<float>(sw);
|
||||||
|
const auto SHF = static_cast<float>(sh);
|
||||||
|
const float CANVAS_RATIO = static_cast<float>(game_width_) / static_cast<float>(game_height_);
|
||||||
|
const float WINDOW_RATIO = SWF / SHF;
|
||||||
|
|
||||||
float vw = 0.0F;
|
float vw = 0.0F;
|
||||||
float vh = 0.0F;
|
float vh = 0.0F;
|
||||||
if (integer_scale_) {
|
switch (presentation_mode_) {
|
||||||
|
case PresentationMode::INTEGER_SCALE: {
|
||||||
const int SCALE = std::max(1, std::min(static_cast<int>(sw) / game_width_, static_cast<int>(sh) / game_height_));
|
const int SCALE = std::max(1, std::min(static_cast<int>(sw) / game_width_, static_cast<int>(sh) / game_height_));
|
||||||
vw = static_cast<float>(game_width_ * SCALE);
|
vw = static_cast<float>(game_width_ * SCALE);
|
||||||
vh = static_cast<float>(game_height_ * SCALE);
|
vh = static_cast<float>(game_height_ * SCALE);
|
||||||
} else {
|
break;
|
||||||
const float SCALE = std::min(
|
|
||||||
static_cast<float>(sw) / static_cast<float>(game_width_),
|
|
||||||
static_cast<float>(sh) / static_cast<float>(game_height_));
|
|
||||||
vw = static_cast<float>(game_width_) * SCALE;
|
|
||||||
vh = static_cast<float>(game_height_) * SCALE;
|
|
||||||
}
|
}
|
||||||
const float VX = std::floor((static_cast<float>(sw) - vw) * 0.5F);
|
case PresentationMode::LETTERBOX: {
|
||||||
const float VY = std::floor((static_cast<float>(sh) - vh) * 0.5F);
|
if (WINDOW_RATIO >= CANVAS_RATIO) {
|
||||||
|
vh = SHF;
|
||||||
|
vw = SHF * CANVAS_RATIO;
|
||||||
|
} else {
|
||||||
|
vw = SWF;
|
||||||
|
vh = SWF / CANVAS_RATIO;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PresentationMode::STRETCHED: {
|
||||||
|
vw = SWF;
|
||||||
|
vh = SHF;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PresentationMode::OVERSCAN: {
|
||||||
|
if (WINDOW_RATIO >= CANVAS_RATIO) {
|
||||||
|
vw = SWF;
|
||||||
|
vh = SWF / CANVAS_RATIO;
|
||||||
|
} else {
|
||||||
|
vh = SHF;
|
||||||
|
vw = SHF * CANVAS_RATIO;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const float VX = std::floor((SWF - vw) * 0.5F);
|
||||||
|
const float VY = std::floor((SHF - vh) * 0.5F);
|
||||||
return {.x = VX, .y = VY, .w = vw, .h = vh};
|
return {.x = VX, .y = VY, .w = vw, .h = vh};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,8 +596,8 @@ namespace Rendering {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SDL3GPUShader::setScaleMode(bool integer_scale) {
|
void SDL3GPUShader::setPresentationMode(PresentationMode mode) {
|
||||||
integer_scale_ = integer_scale;
|
presentation_mode_ = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ namespace Rendering {
|
|||||||
const std::string& fragment_source) -> bool override;
|
const std::string& fragment_source) -> bool override;
|
||||||
|
|
||||||
void render() override;
|
void render() override;
|
||||||
void setTextureSize(float width, float height) override {}
|
void setTextureSize(float /*width*/, float /*height*/) override {}
|
||||||
void cleanup() final; // Libera pipeline/texturas pero mantiene el device vivo
|
void cleanup() final; // Libera pipeline/texturas pero mantiene el device vivo
|
||||||
void destroy(); // Limpieza completa (device + swapchain); llamar solo al cerrar
|
void destroy(); // Limpieza completa (device + swapchain); llamar solo al cerrar
|
||||||
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
|
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
|
||||||
@@ -97,8 +97,8 @@ namespace Rendering {
|
|||||||
// Activa/desactiva VSync en el swapchain
|
// Activa/desactiva VSync en el swapchain
|
||||||
void setVSync(bool vsync) override;
|
void setVSync(bool vsync) override;
|
||||||
|
|
||||||
// Activa/desactiva escalado entero (integer scale)
|
// Estableix el mode de presentacio del canvas
|
||||||
void setScaleMode(bool integer_scale) override;
|
void setPresentationMode(PresentationMode mode) override;
|
||||||
|
|
||||||
// Selecciona el shader de post-procesado activo (POSTFX o CRTPI)
|
// Selecciona el shader de post-procesado activo (POSTFX o CRTPI)
|
||||||
void setActiveShader(ShaderType type) override;
|
void setActiveShader(ShaderType type) override;
|
||||||
@@ -153,8 +153,40 @@ namespace Rendering {
|
|||||||
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
|
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
|
||||||
SDL_GPUSampler* sampler_ = nullptr; // NEAREST
|
SDL_GPUSampler* sampler_ = nullptr; // NEAREST
|
||||||
|
|
||||||
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};
|
PostFXUniforms uniforms_{
|
||||||
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};
|
.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; // Shader de post-procesado activo
|
ShaderType active_shader_ = ShaderType::POSTFX; // Shader de post-procesado activo
|
||||||
|
|
||||||
int game_width_ = 0; // Dimensiones originales del canvas
|
int game_width_ = 0; // Dimensiones originales del canvas
|
||||||
@@ -163,7 +195,7 @@ namespace Rendering {
|
|||||||
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
|
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
|
||||||
bool is_initialized_ = false;
|
bool is_initialized_ = false;
|
||||||
bool vsync_ = true;
|
bool vsync_ = true;
|
||||||
bool integer_scale_ = false;
|
PresentationMode presentation_mode_ = PresentationMode::INTEGER_SCALE;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Rendering
|
} // namespace Rendering
|
||||||
|
|||||||
@@ -112,9 +112,16 @@ namespace Rendering {
|
|||||||
virtual void setVSync(bool /*vsync*/) {}
|
virtual void setVSync(bool /*vsync*/) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Activa o desactiva el escalado entero (integer scale)
|
* @brief Estableix el mode de presentacio del canvas dins del swapchain.
|
||||||
|
* El backend calcula el viewport en consequencia.
|
||||||
*/
|
*/
|
||||||
virtual void setScaleMode(bool /*integer_scale*/) {}
|
enum class PresentationMode : std::uint8_t {
|
||||||
|
INTEGER_SCALE,
|
||||||
|
LETTERBOX,
|
||||||
|
STRETCHED,
|
||||||
|
OVERSCAN
|
||||||
|
};
|
||||||
|
virtual void setPresentationMode(PresentationMode /*mode*/) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Verifica si el backend está usando aceleración por hardware
|
* @brief Verifica si el backend está usando aceleración por hardware
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef> // for size_t
|
||||||
#include <cstdint> // for uint8_t
|
#include <cstdint> // for uint8_t
|
||||||
#include <string> // for string, basic_string
|
#include <string> // for string, basic_string
|
||||||
#include <vector> // for vector
|
#include <vector> // for vector
|
||||||
@@ -41,7 +42,7 @@ class Asset {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
// Variables
|
// Variables
|
||||||
int longest_name_{0}; // Contiene la longitud del nombre de fichero mas largo
|
std::size_t longest_name_{0}; // Contiene la longitud del nombre de fichero mas largo
|
||||||
std::vector<Item> file_list_; // Listado con todas las rutas a los ficheros
|
std::vector<Item> file_list_; // Listado con todas las rutas a los ficheros
|
||||||
std::string executable_path_; // Ruta al ejecutable
|
std::string executable_path_; // Ruta al ejecutable
|
||||||
bool verbose_{true}; // Indica si ha de mostrar información por pantalla
|
bool verbose_{true}; // Indica si ha de mostrar información por pantalla
|
||||||
|
|||||||
@@ -144,8 +144,9 @@ void Resource::loadDataAsset(const std::string &bname, const std::vector<uint8_t
|
|||||||
lines.push_back(line);
|
lines.push_back(line);
|
||||||
}
|
}
|
||||||
animation_lines_[bname] = std::move(lines);
|
animation_lines_[bname] = std::move(lines);
|
||||||
} else if (bname == "demo.bin") {
|
} else if (bname.size() > 5 && bname.substr(0, 4) == "demo" && bname.substr(bname.size() - 4) == ".bin") {
|
||||||
demo_bytes_ = bytes;
|
// Acumula tots els demo*.bin (demo1.bin, demo2.bin, ...) en ordre d'aparicio
|
||||||
|
demo_bytes_.push_back(bytes);
|
||||||
}
|
}
|
||||||
// Menús (.men): se construyen en pass 2 porque dependen de textos y sonidos
|
// Menús (.men): se construyen en pass 2 porque dependen de textos y sonidos
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ class Resource {
|
|||||||
auto getAnimationLines(const std::string &name) -> std::vector<std::string> &;
|
auto getAnimationLines(const std::string &name) -> std::vector<std::string> &;
|
||||||
auto getText(const std::string &name) -> Text *; // name sin extensión: "smb2", "nokia2", ...
|
auto getText(const std::string &name) -> Text *; // name sin extensión: "smb2", "nokia2", ...
|
||||||
auto getMenu(const std::string &name) -> Menu *; // name sin extensión: "title", "options", ...
|
auto getMenu(const std::string &name) -> Menu *; // name sin extensión: "title", "options", ...
|
||||||
auto getDemoBytes() const -> const std::vector<uint8_t> & { return demo_bytes_; }
|
[[nodiscard]] auto getDemoCount() const -> size_t { return demo_bytes_.size(); }
|
||||||
|
[[nodiscard]] auto getDemoBytes(size_t index) const -> const std::vector<uint8_t> & { return demo_bytes_.at(index); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
explicit Resource(SDL_Renderer *renderer);
|
explicit Resource(SDL_Renderer *renderer);
|
||||||
@@ -51,7 +52,7 @@ class Resource {
|
|||||||
std::unordered_map<std::string, std::vector<std::string>> animation_lines_;
|
std::unordered_map<std::string, std::vector<std::string>> animation_lines_;
|
||||||
std::unordered_map<std::string, Text *> texts_;
|
std::unordered_map<std::string, Text *> texts_;
|
||||||
std::unordered_map<std::string, Menu *> menus_;
|
std::unordered_map<std::string, Menu *> menus_;
|
||||||
std::vector<uint8_t> demo_bytes_;
|
std::vector<std::vector<uint8_t>> demo_bytes_;
|
||||||
|
|
||||||
static Resource *instance;
|
static Resource *instance;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#include "core/system/demo.hpp"
|
||||||
|
|
||||||
|
#include <cstring> // for memcpy
|
||||||
|
|
||||||
|
// Desempaqueta un blob binari amb TOTAL_DEMO_DATA registres consecutius
|
||||||
|
// de DemoKeys (struct POD de 6 bytes). Si el blob no te la mida esperada,
|
||||||
|
// torna un vector buit perque el playback el detecti i no peti.
|
||||||
|
auto loadDemoDataFromBytes(const std::vector<uint8_t> &bytes) -> DemoData {
|
||||||
|
DemoData dd;
|
||||||
|
const size_t EXPECTED = sizeof(DemoKeys) * TOTAL_DEMO_DATA;
|
||||||
|
if (bytes.size() < EXPECTED) {
|
||||||
|
return dd;
|
||||||
|
}
|
||||||
|
dd.resize(TOTAL_DEMO_DATA);
|
||||||
|
std::memcpy(dd.data(), bytes.data(), EXPECTED);
|
||||||
|
return dd;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Total de "frames" gravats a 60Hz de referencia. Equival a 2000/60 ~ 33.3s
|
||||||
|
// reals, independentment del refresc real perque el playback es time-based
|
||||||
|
// (index = elapsed_s * 60).
|
||||||
|
constexpr int TOTAL_DEMO_DATA = 2000;
|
||||||
|
|
||||||
|
// Pulsacions per frame de referencia gravades a disc / reproduides al playback.
|
||||||
|
struct DemoKeys {
|
||||||
|
Uint8 left;
|
||||||
|
Uint8 right;
|
||||||
|
Uint8 no_input;
|
||||||
|
Uint8 fire;
|
||||||
|
Uint8 fire_left;
|
||||||
|
Uint8 fire_right;
|
||||||
|
|
||||||
|
explicit DemoKeys(Uint8 l = 0, Uint8 r = 0, Uint8 ni = 0, Uint8 f = 0, Uint8 fl = 0, Uint8 fr = 0)
|
||||||
|
: left(l),
|
||||||
|
right(r),
|
||||||
|
no_input(ni),
|
||||||
|
fire(f),
|
||||||
|
fire_left(fl),
|
||||||
|
fire_right(fr) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Una demo completa: vector de frames.
|
||||||
|
using DemoData = std::vector<DemoKeys>;
|
||||||
|
|
||||||
|
// Estat del subsistema de demo dins de Game.
|
||||||
|
struct Demo {
|
||||||
|
bool enabled{false}; // Mode demo actiu (reproduccio)
|
||||||
|
bool recording{false}; // Mode gravacio actiu
|
||||||
|
float elapsed_s{0.0F}; // Temps acumulat des de l'inici de la demo
|
||||||
|
int index{0}; // index = elapsed_s * 60 (derivat)
|
||||||
|
DemoKeys keys; // Buffer de tecles del frame actual (gravacio)
|
||||||
|
std::vector<DemoData> data; // Vector de sets de demo carregats (multi-fitxer)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Carrega un fitxer .bin (TOTAL_DEMO_DATA * sizeof(DemoKeys) bytes) i
|
||||||
|
// retorna el DemoData. Si el fitxer no es troba, retorna un DemoData buit.
|
||||||
|
auto loadDemoDataFromBytes(const std::vector<uint8_t> &bytes) -> DemoData;
|
||||||
@@ -232,6 +232,10 @@ void Director::initInput() {
|
|||||||
Input::get()->bindKey(Input::Action::TOGGLE_SHADER, SDL_SCANCODE_F4);
|
Input::get()->bindKey(Input::Action::TOGGLE_SHADER, SDL_SCANCODE_F4);
|
||||||
Input::get()->bindKey(Input::Action::TOGGLE_SHADER_TYPE, SDL_SCANCODE_F5);
|
Input::get()->bindKey(Input::Action::TOGGLE_SHADER_TYPE, SDL_SCANCODE_F5);
|
||||||
Input::get()->bindKey(Input::Action::NEXT_SHADER_PRESET, SDL_SCANCODE_F6);
|
Input::get()->bindKey(Input::Action::NEXT_SHADER_PRESET, SDL_SCANCODE_F6);
|
||||||
|
Input::get()->bindKey(Input::Action::TOGGLE_VSYNC, SDL_SCANCODE_F7);
|
||||||
|
Input::get()->bindKey(Input::Action::NEXT_PRESENTATION_MODE, SDL_SCANCODE_F8);
|
||||||
|
Input::get()->bindKey(Input::Action::TOGGLE_FPS, SDL_SCANCODE_F10);
|
||||||
|
Input::get()->bindKey(Input::Action::SHOW_VERSION, SDL_SCANCODE_F11);
|
||||||
|
|
||||||
// Mando - Movimiento del jugador
|
// Mando - Movimiento del jugador
|
||||||
Input::get()->bindGameControllerButton(Input::Action::UP, SDL_GAMEPAD_BUTTON_DPAD_UP);
|
Input::get()->bindGameControllerButton(Input::Action::UP, SDL_GAMEPAD_BUTTON_DPAD_UP);
|
||||||
@@ -338,7 +342,9 @@ auto Director::setFileList() -> bool {
|
|||||||
|
|
||||||
// Ficheros de configuración
|
// Ficheros de configuración
|
||||||
Asset::get()->add(system_folder_ + "/score.bin", Asset::Type::DATA, false, true);
|
Asset::get()->add(system_folder_ + "/score.bin", Asset::Type::DATA, false, true);
|
||||||
Asset::get()->add(PREFIX + "/data/demo/demo.bin", Asset::Type::DATA);
|
Asset::get()->add(PREFIX + "/data/demo/demo1.bin", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/demo/demo2.bin", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/demo/demo3.bin", Asset::Type::DATA);
|
||||||
|
|
||||||
// Musicas
|
// Musicas
|
||||||
Asset::get()->add(PREFIX + "/data/music/intro.ogg", Asset::Type::MUSIC);
|
Asset::get()->add(PREFIX + "/data/music/intro.ogg", Asset::Type::MUSIC);
|
||||||
@@ -469,13 +475,13 @@ void Director::checkProgramArguments(int argc, const char *argv[]) {
|
|||||||
void Director::createSystemFolder(const std::string &folder) {
|
void Director::createSystemFolder(const std::string &folder) {
|
||||||
#ifdef __EMSCRIPTEN__
|
#ifdef __EMSCRIPTEN__
|
||||||
// En Emscripten usamos una carpeta en MEMFS (no persistente)
|
// En Emscripten usamos una carpeta en MEMFS (no persistente)
|
||||||
systemFolder = "/config/" + folder;
|
system_folder_ = "/config/" + folder;
|
||||||
#elif _WIN32
|
#elif _WIN32
|
||||||
systemFolder = std::string(getenv("APPDATA")) + "/" + folder;
|
system_folder_ = std::string(getenv("APPDATA")) + "/" + folder;
|
||||||
#elif __APPLE__
|
#elif __APPLE__
|
||||||
struct passwd *pw = getpwuid(getuid());
|
struct passwd *pw = getpwuid(getuid());
|
||||||
const char *homedir = pw->pw_dir;
|
const char *homedir = pw->pw_dir;
|
||||||
systemFolder = std::string(homedir) + "/Library/Application Support" + "/" + folder;
|
system_folder_ = std::string(homedir) + "/Library/Application Support" + "/" + folder;
|
||||||
#elif __linux__
|
#elif __linux__
|
||||||
struct passwd *pw = getpwuid(getuid());
|
struct passwd *pw = getpwuid(getuid());
|
||||||
const char *homedir = pw->pw_dir;
|
const char *homedir = pw->pw_dir;
|
||||||
@@ -496,11 +502,11 @@ void Director::createSystemFolder(const std::string &folder) {
|
|||||||
// En Emscripten no necesitamos crear carpetas (MEMFS las crea automáticamente)
|
// En Emscripten no necesitamos crear carpetas (MEMFS las crea automáticamente)
|
||||||
(void)folder;
|
(void)folder;
|
||||||
#else
|
#else
|
||||||
struct stat st = {.st_dev = 0};
|
struct stat st{};
|
||||||
if (stat(system_folder_.c_str(), &st) == -1) {
|
if (stat(system_folder_.c_str(), &st) == -1) {
|
||||||
errno = 0;
|
errno = 0;
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
int ret = mkdir(systemFolder.c_str());
|
int ret = mkdir(system_folder_.c_str());
|
||||||
#else
|
#else
|
||||||
int ret = mkdir(system_folder_.c_str(), S_IRWXU);
|
int ret = mkdir(system_folder_.c_str(), S_IRWXU);
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ namespace Defaults::Video {
|
|||||||
constexpr SDL_ScaleMode SCALE_MODE = SDL_ScaleMode::SDL_SCALEMODE_NEAREST;
|
constexpr SDL_ScaleMode SCALE_MODE = SDL_ScaleMode::SDL_SCALEMODE_NEAREST;
|
||||||
constexpr bool FULLSCREEN = false;
|
constexpr bool FULLSCREEN = false;
|
||||||
constexpr bool VSYNC = true;
|
constexpr bool VSYNC = true;
|
||||||
constexpr bool INTEGER_SCALE = true;
|
// INTEGER_SCALE eliminat: ara es part de PresentationMode (a options.hpp).
|
||||||
|
// El default es defineix literal alli: PresentationMode::INTEGER_SCALE.
|
||||||
constexpr bool GPU_ACCELERATION = true;
|
constexpr bool GPU_ACCELERATION = true;
|
||||||
constexpr const char *GPU_PREFERRED_DRIVER = "";
|
constexpr const char *GPU_PREFERRED_DRIVER = "";
|
||||||
constexpr bool SHADER_ENABLED = false;
|
constexpr bool SHADER_ENABLED = false;
|
||||||
|
|||||||
+84
-60
@@ -277,7 +277,8 @@ void Game::init() {
|
|||||||
|
|
||||||
// Modo demo
|
// Modo demo
|
||||||
demo_.recording = false;
|
demo_.recording = false;
|
||||||
demo_.counter = 0;
|
demo_.elapsed_s = 0.0F;
|
||||||
|
demo_.index = 0;
|
||||||
|
|
||||||
// Inicializa el objeto para el fundido
|
// Inicializa el objeto para el fundido
|
||||||
fade_->init(0x27, 0x27, 0x36);
|
fade_->init(0x27, 0x27, 0x36);
|
||||||
@@ -521,34 +522,31 @@ auto Game::loadScoreFile() -> bool {
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carga el fichero de datos para la demo
|
// Carga els fitxers de dades de demo (multi-set) des de Resource. Tots els
|
||||||
|
// blobs es descompacten a DemoData via loadDemoDataFromBytes; si un blob no
|
||||||
|
// te la mida esperada s'omet.
|
||||||
auto Game::loadDemoFile() -> bool {
|
auto Game::loadDemoFile() -> bool {
|
||||||
// Lee los datos de la demo desde Resource (precargados al arrancar).
|
demo_.data.clear();
|
||||||
const auto &bytes = Resource::get()->getDemoBytes();
|
const size_t NUM = Resource::get()->getDemoCount();
|
||||||
const size_t EXPECTED = sizeof(DemoKeys) * TOTAL_DEMO_DATA;
|
for (size_t i = 0; i < NUM; ++i) {
|
||||||
if (bytes.size() >= EXPECTED) {
|
DemoData dd = loadDemoDataFromBytes(Resource::get()->getDemoBytes(i));
|
||||||
for (int i = 0; i < TOTAL_DEMO_DATA; ++i) {
|
if (!dd.empty()) {
|
||||||
memcpy(&demo_.data_file[i], bytes.data() + (i * sizeof(DemoKeys)), sizeof(DemoKeys));
|
demo_.data.push_back(std::move(dd));
|
||||||
}
|
}
|
||||||
if (Options::settings.console) {
|
|
||||||
std::cout << "Demo data loaded (" << bytes.size() << " bytes)" << '\n';
|
|
||||||
}
|
}
|
||||||
|
if (!demo_.data.empty()) {
|
||||||
|
demo_selected_set_ = static_cast<size_t>(rand()) % demo_.data.size();
|
||||||
} else {
|
} else {
|
||||||
// Si no hay datos (bytes vacíos o tamaño inválido), inicializamos a cero.
|
demo_selected_set_ = 0;
|
||||||
|
}
|
||||||
if (Options::settings.console) {
|
if (Options::settings.console) {
|
||||||
std::cout << "Warning: demo data missing or too small, initializing to zero" << '\n';
|
if (demo_.data.empty()) {
|
||||||
}
|
std::cout << "Warning: no demo data loaded" << '\n';
|
||||||
for (auto &i : demo_.data_file) {
|
} else {
|
||||||
demo_.keys.left = 0;
|
std::cout << "Demo data loaded (" << demo_.data.size() << " sets, playing #" << demo_selected_set_ << ")" << '\n';
|
||||||
demo_.keys.right = 0;
|
|
||||||
demo_.keys.no_input = 0;
|
|
||||||
demo_.keys.fire = 0;
|
|
||||||
demo_.keys.fire_left = 0;
|
|
||||||
demo_.keys.fire_right = 0;
|
|
||||||
i = demo_.keys;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return !demo_.data.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guarda el fichero de puntos
|
// Guarda el fichero de puntos
|
||||||
@@ -577,32 +575,29 @@ auto Game::saveScoreFile() -> bool {
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guarda el fichero de datos para la demo
|
// Guarda el primer set de demo (gravat en mode RECORDING) a demo1.bin.
|
||||||
auto Game::saveDemoFile() -> bool {
|
auto Game::saveDemoFile() -> bool {
|
||||||
bool success = true;
|
if (!demo_.recording || demo_.data.empty()) {
|
||||||
const std::string P = Asset::get()->get("demo.bin");
|
return true;
|
||||||
|
}
|
||||||
|
const std::string P = Asset::get()->get("demo1.bin");
|
||||||
const std::string FILE_NAME = P.substr(P.find_last_of("\\/") + 1);
|
const std::string FILE_NAME = P.substr(P.find_last_of("\\/") + 1);
|
||||||
if (demo_.recording) {
|
|
||||||
SDL_IOStream *file = SDL_IOFromFile(P.c_str(), "w+b");
|
SDL_IOStream *file = SDL_IOFromFile(P.c_str(), "w+b");
|
||||||
if (file != nullptr) {
|
if (file == nullptr) {
|
||||||
// Guardamos los datos
|
|
||||||
for (auto &i : demo_.data_file) {
|
|
||||||
SDL_WriteIO(file, &i, sizeof(DemoKeys));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Options::settings.console) {
|
if (Options::settings.console) {
|
||||||
std::cout << "Writing file " << FILE_NAME.c_str() << '\n';
|
std::cout << "Error: Unable to save " << FILE_NAME << " file! " << SDL_GetError() << '\n';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto &dd = demo_.data.at(0);
|
||||||
|
for (const auto &k : dd) {
|
||||||
|
SDL_WriteIO(file, &k, sizeof(DemoKeys));
|
||||||
|
}
|
||||||
|
if (Options::settings.console) {
|
||||||
|
std::cout << "Writing file " << FILE_NAME << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cerramos el fichero
|
|
||||||
SDL_CloseIO(file);
|
SDL_CloseIO(file);
|
||||||
} else {
|
return true;
|
||||||
if (Options::settings.console) {
|
|
||||||
std::cout << "Error: Unable to save " << FILE_NAME.c_str() << " file! " << SDL_GetError() << '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inicializa las formaciones enemigas
|
// Inicializa las formaciones enemigas
|
||||||
@@ -2276,6 +2271,12 @@ void Game::update(float dt_s) {
|
|||||||
elapsed_s_ += dt_s;
|
elapsed_s_ += dt_s;
|
||||||
counter_ = static_cast<Uint32>(elapsed_s_ * 60.0F);
|
counter_ = static_cast<Uint32>(elapsed_s_ * 60.0F);
|
||||||
|
|
||||||
|
// Avenc del temps de la demo (playback o gravacio). Index = elapsed_s * 60
|
||||||
|
if (demo_.enabled || demo_.recording) {
|
||||||
|
demo_.elapsed_s += dt_s;
|
||||||
|
demo_.index = static_cast<int>(demo_.elapsed_s * 60.0F);
|
||||||
|
}
|
||||||
|
|
||||||
checkGameInput();
|
checkGameInput();
|
||||||
updatePlayers(dt_s);
|
updatePlayers(dt_s);
|
||||||
updateBackground(dt_s);
|
updateBackground(dt_s);
|
||||||
@@ -2446,10 +2447,33 @@ void Game::checkGameInput() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rama de checkGameInput: reproduce el input grabado en data_file
|
// Rama de checkGameInput: reprodueix l'input gravat al set actiu de la demo.
|
||||||
|
// El index es time-based: index = elapsed_s * 60. L'avenc d'elapsed_s el fa
|
||||||
|
// Game::update() per evitar que el ritme de playback depengui dels frames
|
||||||
|
// que arribin a aquesta funcio.
|
||||||
void Game::processDemoInput() {
|
void Game::processDemoInput() {
|
||||||
const int INDEX = 0;
|
const int INDEX = 0;
|
||||||
const DemoKeys &keys = demo_.data_file[demo_.counter];
|
|
||||||
|
// Fi de la demo: salta a Title
|
||||||
|
if (demo_.index >= TOTAL_DEMO_DATA) {
|
||||||
|
section_->name = SECTION_PROG_TITLE;
|
||||||
|
section_->subsection = SUBSECTION_TITLE_INSTRUCTIONS;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si no hi ha dades carregades, sortim al menu
|
||||||
|
if (demo_.data.empty()) {
|
||||||
|
section_->name = SECTION_PROG_TITLE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accedeix al frame actual del set seleccionat amb % per seguretat
|
||||||
|
// davant de salts puntuals d'index.
|
||||||
|
const auto &dd = demo_.data.at(demo_selected_set_ % demo_.data.size());
|
||||||
|
if (dd.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const DemoKeys &keys = dd.at(static_cast<size_t>(demo_.index) % dd.size());
|
||||||
|
|
||||||
if (keys.left == 1) {
|
if (keys.left == 1) {
|
||||||
players_[INDEX]->setInput(Input::Action::LEFT);
|
players_[INDEX]->setInput(Input::Action::LEFT);
|
||||||
@@ -2479,18 +2503,10 @@ void Game::processDemoInput() {
|
|||||||
players_[INDEX]->setFireCooldown(10);
|
players_[INDEX]->setFireCooldown(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si se pulsa cualquier tecla, se sale del modo demo
|
// Si es prem qualsevol tecla, surt del mode demo
|
||||||
if (Input::get()->checkAnyInput()) {
|
if (Input::get()->checkAnyInput()) {
|
||||||
section_->name = SECTION_PROG_TITLE;
|
section_->name = SECTION_PROG_TITLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Incrementa el contador de la demo
|
|
||||||
if (demo_.counter < TOTAL_DEMO_DATA) {
|
|
||||||
demo_.counter++;
|
|
||||||
} else {
|
|
||||||
section_->name = SECTION_PROG_TITLE;
|
|
||||||
section_->subsection = SUBSECTION_TITLE_INSTRUCTIONS;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rama de checkGameInput: lee inputs reales del teclado/gamepad por jugador
|
// Rama de checkGameInput: lee inputs reales del teclado/gamepad por jugador
|
||||||
@@ -2553,14 +2569,22 @@ void Game::processPlayerLiveInput(Player *player, int i) {
|
|||||||
section_->subsection = SUBSECTION_GAME_PAUSE;
|
section_->subsection = SUBSECTION_GAME_PAUSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grabación de demo
|
// Gravacio de demo (mode recording). L'index ja s'ha actualitzat a
|
||||||
if (demo_.counter < TOTAL_DEMO_DATA) {
|
// Game::update() via elapsed_s; aqui nomes escrivim el frame actual al
|
||||||
|
// primer set, redimensionant on demand.
|
||||||
if (demo_.recording) {
|
if (demo_.recording) {
|
||||||
demo_.data_file[demo_.counter] = demo_.keys;
|
if (demo_.index >= TOTAL_DEMO_DATA) {
|
||||||
}
|
|
||||||
demo_.counter++;
|
|
||||||
} else if (demo_.recording) {
|
|
||||||
section_->name = SECTION_PROG_QUIT;
|
section_->name = SECTION_PROG_QUIT;
|
||||||
|
} else {
|
||||||
|
if (demo_.data.empty()) {
|
||||||
|
demo_.data.emplace_back();
|
||||||
|
}
|
||||||
|
auto &dd = demo_.data.at(0);
|
||||||
|
if (dd.size() <= static_cast<size_t>(demo_.index)) {
|
||||||
|
dd.resize(demo_.index + 1);
|
||||||
|
}
|
||||||
|
dd.at(demo_.index) = demo_.keys;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2590,7 +2614,7 @@ void Game::renderMessages() {
|
|||||||
|
|
||||||
// D E M O
|
// D E M O
|
||||||
if (demo_.enabled) {
|
if (demo_.enabled) {
|
||||||
if (demo_.counter % 30 > 14) {
|
if (demo_.index % 30 > 14) {
|
||||||
text_nokia_big2_->writeDX(Text::FLAG_CENTER, PLAY_AREA_CENTER_X, PLAY_AREA_FIRST_QUARTER_Y, Lang::get()->getText(37), 0, NO_COLOR, 2, SHADOW_COLOR);
|
text_nokia_big2_->writeDX(Text::FLAG_CENTER, PLAY_AREA_CENTER_X, PLAY_AREA_FIRST_QUARTER_Y, Lang::get()->getText(37), 0, NO_COLOR, 2, SHADOW_COLOR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-10
@@ -6,9 +6,10 @@
|
|||||||
#include <string> // for string, basic_string
|
#include <string> // for string, basic_string
|
||||||
#include <vector> // for vector
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include "core/system/demo.hpp" // for Demo (estat de la demo)
|
||||||
#include "game/entities/bullet.h" // for Bullet::Kind (signatura de createBullet)
|
#include "game/entities/bullet.h" // for Bullet::Kind (signatura de createBullet)
|
||||||
#include "game/entities/item.h" // for Item::Id (signatura de dropItem/createItem)
|
#include "game/entities/item.h" // for Item::Id (signatura de dropItem/createItem)
|
||||||
#include "utils/utils.h" // for DemoKeys, Color
|
#include "utils/utils.h" // for Color, Section
|
||||||
class Balloon;
|
class Balloon;
|
||||||
class Fade;
|
class Fade;
|
||||||
class Menu;
|
class Menu;
|
||||||
@@ -45,7 +46,6 @@ class Game {
|
|||||||
|
|
||||||
// Cantidad de elementos a escribir en los ficheros de datos
|
// Cantidad de elementos a escribir en los ficheros de datos
|
||||||
static constexpr int TOTAL_SCORE_DATA = 3;
|
static constexpr int TOTAL_SCORE_DATA = 3;
|
||||||
static constexpr int TOTAL_DEMO_DATA = 2000;
|
|
||||||
|
|
||||||
// Contadores
|
// Contadores
|
||||||
static constexpr int STAGE_COUNTER = 200;
|
static constexpr int STAGE_COUNTER = 200;
|
||||||
@@ -138,14 +138,6 @@ class Game {
|
|||||||
int item_coffee_machine_odds; // Probabilidad de aparición del objeto
|
int item_coffee_machine_odds; // Probabilidad de aparición del objeto
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Demo {
|
|
||||||
bool enabled; // Indica si está activo el modo demo
|
|
||||||
bool recording; // Indica si está activado el modo para grabar la demo
|
|
||||||
Uint16 counter; // Contador para el modo demo
|
|
||||||
DemoKeys keys; // Variable con las pulsaciones de teclas del modo demo
|
|
||||||
DemoKeys data_file[TOTAL_DEMO_DATA]; // Datos del fichero con los movimientos para la demo
|
|
||||||
};
|
|
||||||
|
|
||||||
void update(float dt_s); // Actualiza el juego
|
void update(float dt_s); // Actualiza el juego
|
||||||
void render(); // Dibuja el juego
|
void render(); // Dibuja el juego
|
||||||
void init(); // Inicializa las variables necesarias para la sección 'Game'
|
void init(); // Inicializa las variables necesarias para la sección 'Game'
|
||||||
@@ -389,6 +381,7 @@ class Game {
|
|||||||
EnemyPool enemy_pool_[10]; // Variable con los diferentes conjuntos de formaciones enemigas
|
EnemyPool enemy_pool_[10]; // Variable con los diferentes conjuntos de formaciones enemigas
|
||||||
Uint8 last_stage_reached_; // Contiene el numero de la última pantalla que se ha alcanzado
|
Uint8 last_stage_reached_; // Contiene el numero de la última pantalla que se ha alcanzado
|
||||||
Demo demo_; // Variable con todas las variables relacionadas con el modo demo
|
Demo demo_; // Variable con todas las variables relacionadas con el modo demo
|
||||||
|
size_t demo_selected_set_{0}; // Index del set de demo a reproduir (escollit a loadDemoFile)
|
||||||
int total_power_to_complete_game_; // La suma del poder necesario para completar todas las fases
|
int total_power_to_complete_game_; // La suma del poder necesario para completar todas las fases
|
||||||
int clouds_speed_{0}; // Velocidad a la que se desplazan las nubes
|
int clouds_speed_{0}; // Velocidad a la que se desplazan las nubes
|
||||||
int pause_counter_; // Contador per a sortir del menu de pausa (frame-based, frames)
|
int pause_counter_; // Contador per a sortir del menu de pausa (frame-based, frames)
|
||||||
|
|||||||
+49
-3
@@ -31,6 +31,42 @@ namespace Options {
|
|||||||
std::string crtpi_file_path;
|
std::string crtpi_file_path;
|
||||||
int current_crtpi_preset = 0;
|
int current_crtpi_preset = 0;
|
||||||
|
|
||||||
|
// Conversions PresentationMode <-> string per a config.yaml i UI
|
||||||
|
auto presentationModeToString(PresentationMode m) -> const char * {
|
||||||
|
switch (m) {
|
||||||
|
case PresentationMode::INTEGER_SCALE:
|
||||||
|
return "integer_scale";
|
||||||
|
case PresentationMode::LETTERBOX:
|
||||||
|
return "letterbox";
|
||||||
|
case PresentationMode::STRETCHED:
|
||||||
|
return "stretched";
|
||||||
|
case PresentationMode::OVERSCAN:
|
||||||
|
return "overscan";
|
||||||
|
}
|
||||||
|
return "integer_scale";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto presentationModeFromString(const std::string &s) -> PresentationMode {
|
||||||
|
if (s == "letterbox") { return PresentationMode::LETTERBOX; }
|
||||||
|
if (s == "stretched") { return PresentationMode::STRETCHED; }
|
||||||
|
if (s == "overscan") { return PresentationMode::OVERSCAN; }
|
||||||
|
return PresentationMode::INTEGER_SCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto nextPresentationMode(PresentationMode m) -> PresentationMode {
|
||||||
|
switch (m) {
|
||||||
|
case PresentationMode::INTEGER_SCALE:
|
||||||
|
return PresentationMode::LETTERBOX;
|
||||||
|
case PresentationMode::LETTERBOX:
|
||||||
|
return PresentationMode::STRETCHED;
|
||||||
|
case PresentationMode::STRETCHED:
|
||||||
|
return PresentationMode::OVERSCAN;
|
||||||
|
case PresentationMode::OVERSCAN:
|
||||||
|
return PresentationMode::INTEGER_SCALE;
|
||||||
|
}
|
||||||
|
return PresentationMode::INTEGER_SCALE;
|
||||||
|
}
|
||||||
|
|
||||||
// Lectura tolerant d'un camp YAML: assigna a `target` el valor del camp
|
// Lectura tolerant d'un camp YAML: assigna a `target` el valor del camp
|
||||||
// si existeix i el tipus encaixa. Si la clau no hi és o el tipus YAML
|
// si existeix i el tipus encaixa. Si la clau no hi és o el tipus YAML
|
||||||
// no és compatible amb T, conserva el valor previ de `target` (default).
|
// no és compatible amb T, conserva el valor previ de `target` (default).
|
||||||
@@ -82,7 +118,16 @@ namespace Options {
|
|||||||
|
|
||||||
parseBoolField(vid, "fullscreen", video.fullscreen);
|
parseBoolField(vid, "fullscreen", video.fullscreen);
|
||||||
parseBoolField(vid, "vsync", video.vsync);
|
parseBoolField(vid, "vsync", video.vsync);
|
||||||
parseBoolField(vid, "integer_scale", video.integer_scale);
|
// presentation_mode (nou): prefereix string explicit; cau a integer_scale legacy (bool) si no.
|
||||||
|
std::string pm_str;
|
||||||
|
if (tryGet<std::string>(vid, "presentation_mode", pm_str)) {
|
||||||
|
video.presentation_mode = presentationModeFromString(pm_str);
|
||||||
|
} else {
|
||||||
|
bool legacy_integer_scale = true;
|
||||||
|
if (tryGet<bool>(vid, "integer_scale", legacy_integer_scale)) {
|
||||||
|
video.presentation_mode = legacy_integer_scale ? PresentationMode::INTEGER_SCALE : PresentationMode::LETTERBOX;
|
||||||
|
}
|
||||||
|
}
|
||||||
int scale_mode_int = static_cast<int>(video.scale_mode);
|
int scale_mode_int = static_cast<int>(video.scale_mode);
|
||||||
if (tryGet<int>(vid, "scale_mode", scale_mode_int)) {
|
if (tryGet<int>(vid, "scale_mode", scale_mode_int)) {
|
||||||
video.scale_mode = static_cast<SDL_ScaleMode>(scale_mode_int);
|
video.scale_mode = static_cast<SDL_ScaleMode>(scale_mode_int);
|
||||||
@@ -197,7 +242,7 @@ namespace Options {
|
|||||||
// En Emscripten la ventana la gestiona el navegador
|
// En Emscripten la ventana la gestiona el navegador
|
||||||
window.zoom = 4;
|
window.zoom = 4;
|
||||||
video.fullscreen = false;
|
video.fullscreen = false;
|
||||||
video.integer_scale = true;
|
video.presentation_mode = PresentationMode::INTEGER_SCALE;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Dispositius d'entrada per defecte
|
// Dispositius d'entrada per defecte
|
||||||
@@ -283,7 +328,8 @@ namespace Options {
|
|||||||
file << "video:\n";
|
file << "video:\n";
|
||||||
file << " fullscreen: " << boolToString(video.fullscreen) << "\n";
|
file << " fullscreen: " << boolToString(video.fullscreen) << "\n";
|
||||||
file << " vsync: " << boolToString(video.vsync) << "\n";
|
file << " vsync: " << boolToString(video.vsync) << "\n";
|
||||||
file << " integer_scale: " << boolToString(video.integer_scale) << "\n";
|
file << " presentation_mode: " << presentationModeToString(video.presentation_mode)
|
||||||
|
<< " # integer_scale | letterbox | stretched | overscan\n";
|
||||||
file << " scale_mode: " << static_cast<int>(video.scale_mode)
|
file << " scale_mode: " << static_cast<int>(video.scale_mode)
|
||||||
<< " # " << static_cast<int>(SDL_SCALEMODE_NEAREST) << ": nearest, "
|
<< " # " << static_cast<int>(SDL_SCALEMODE_NEAREST) << ": nearest, "
|
||||||
<< static_cast<int>(SDL_SCALEMODE_LINEAR) << ": linear\n";
|
<< static_cast<int>(SDL_SCALEMODE_LINEAR) << ": linear\n";
|
||||||
|
|||||||
+16
-1
@@ -18,6 +18,16 @@
|
|||||||
|
|
||||||
namespace Options {
|
namespace Options {
|
||||||
|
|
||||||
|
// Modes de presentacio del canvas virtual a la finestra. Tot fullscreen i
|
||||||
|
// windowed amb zoom no-fit el respecta; en windowed amb zoom exacte (1x/2x/3x)
|
||||||
|
// l'efecte es null perque el canvas ja encaixa amb la finestra.
|
||||||
|
enum class PresentationMode : std::uint8_t {
|
||||||
|
INTEGER_SCALE, // Multiple enter (1x, 2x, 3x...), centrat, amb barres
|
||||||
|
LETTERBOX, // Mante aspect ratio, centrat, amb barres
|
||||||
|
STRETCHED, // Omple tota la finestra, deforma l'aspect ratio
|
||||||
|
OVERSCAN // Mante aspect ratio i omple la finestra retallant el contingut fora
|
||||||
|
};
|
||||||
|
|
||||||
struct Window {
|
struct Window {
|
||||||
std::string caption = Defaults::Window::CAPTION;
|
std::string caption = Defaults::Window::CAPTION;
|
||||||
int zoom = Defaults::Window::ZOOM;
|
int zoom = Defaults::Window::ZOOM;
|
||||||
@@ -42,11 +52,16 @@ namespace Options {
|
|||||||
SDL_ScaleMode scale_mode = Defaults::Video::SCALE_MODE;
|
SDL_ScaleMode scale_mode = Defaults::Video::SCALE_MODE;
|
||||||
bool fullscreen = Defaults::Video::FULLSCREEN;
|
bool fullscreen = Defaults::Video::FULLSCREEN;
|
||||||
bool vsync = Defaults::Video::VSYNC;
|
bool vsync = Defaults::Video::VSYNC;
|
||||||
bool integer_scale = Defaults::Video::INTEGER_SCALE;
|
PresentationMode presentation_mode = PresentationMode::INTEGER_SCALE;
|
||||||
GPU gpu;
|
GPU gpu;
|
||||||
ShaderConfig shader;
|
ShaderConfig shader;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Conversions string <-> PresentationMode per a config.yaml i notificacions
|
||||||
|
auto presentationModeToString(PresentationMode m) -> const char *;
|
||||||
|
auto presentationModeFromString(const std::string &s) -> PresentationMode;
|
||||||
|
auto nextPresentationMode(PresentationMode m) -> PresentationMode;
|
||||||
|
|
||||||
struct Music {
|
struct Music {
|
||||||
bool enabled = Defaults::Music::ENABLED;
|
bool enabled = Defaults::Music::ENABLED;
|
||||||
float volume = Defaults::Music::VOLUME;
|
float volume = Defaults::Music::VOLUME;
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ void Instructions::checkInput() {
|
|||||||
// pulsació; el quit es propaga via Director::iterate.
|
// pulsació; el quit es propaga via Director::iterate.
|
||||||
if (GlobalInputs::handle()) { return; }
|
if (GlobalInputs::handle()) { return; }
|
||||||
|
|
||||||
if (Input::get()->checkInput(Input::Action::PAUSE, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::ACCEPT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_LEFT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_CENTER, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_RIGHT, Input::Repeat::OFF)) {
|
if (Input::get()->checkInput(Input::Action::ACCEPT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_LEFT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_CENTER, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_RIGHT, Input::Repeat::OFF)) {
|
||||||
if (mode_ == Mode::AUTO) {
|
if (mode_ == Mode::AUTO) {
|
||||||
finished_ = true;
|
finished_ = true;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ void Intro::checkInput() {
|
|||||||
// pulsació; el quit es propaga via Director::iterate.
|
// pulsació; el quit es propaga via Director::iterate.
|
||||||
if (GlobalInputs::handle()) { return; }
|
if (GlobalInputs::handle()) { return; }
|
||||||
|
|
||||||
if (Input::get()->checkInput(Input::Action::PAUSE, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::ACCEPT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_LEFT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_CENTER, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_RIGHT, Input::Repeat::OFF)) {
|
if (Input::get()->checkInput(Input::Action::ACCEPT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_LEFT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_CENTER, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_RIGHT, Input::Repeat::OFF)) {
|
||||||
Audio::get()->stopMusic();
|
Audio::get()->stopMusic();
|
||||||
section_->name = SECTION_PROG_TITLE;
|
section_->name = SECTION_PROG_TITLE;
|
||||||
section_->subsection = SUBSECTION_TITLE_1;
|
section_->subsection = SUBSECTION_TITLE_1;
|
||||||
@@ -412,6 +412,6 @@ void Intro::iterate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Procesa un evento individual
|
// Procesa un evento individual
|
||||||
void Intro::handleEvent(const SDL_Event *event) {
|
void Intro::handleEvent(const SDL_Event * /*event*/) {
|
||||||
// SDL_EVENT_QUIT ya lo maneja Director
|
// SDL_EVENT_QUIT ya lo maneja Director
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ void Logo::checkInput() {
|
|||||||
// pulsació; el quit es propaga via Director::iterate.
|
// pulsació; el quit es propaga via Director::iterate.
|
||||||
if (GlobalInputs::handle()) { return; }
|
if (GlobalInputs::handle()) { return; }
|
||||||
|
|
||||||
if (Input::get()->checkInput(Input::Action::PAUSE, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::ACCEPT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_LEFT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_CENTER, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_RIGHT, Input::Repeat::OFF)) {
|
if (Input::get()->checkInput(Input::Action::ACCEPT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_LEFT, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_CENTER, Input::Repeat::OFF) || Input::get()->checkInput(Input::Action::FIRE_RIGHT, Input::Repeat::OFF)) {
|
||||||
section_->name = SECTION_PROG_TITLE;
|
section_->name = SECTION_PROG_TITLE;
|
||||||
section_->subsection = SUBSECTION_TITLE_1;
|
section_->subsection = SUBSECTION_TITLE_1;
|
||||||
}
|
}
|
||||||
@@ -125,6 +125,6 @@ void Logo::iterate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Procesa un evento individual
|
// Procesa un evento individual
|
||||||
void Logo::handleEvent(const SDL_Event *event) {
|
void Logo::handleEvent(const SDL_Event * /*event*/) {
|
||||||
// SDL_EVENT_QUIT ya lo maneja Director
|
// SDL_EVENT_QUIT ya lo maneja Director
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -853,6 +853,51 @@ void Title::applyOptions() {
|
|||||||
|
|
||||||
// Ejecuta un frame
|
// Ejecuta un frame
|
||||||
void Title::iterate() {
|
void Title::iterate() {
|
||||||
|
// Si el joc demo està actiu, NO consumim el dt aqui: el consumeix
|
||||||
|
// Game::iterate() en el seu propi tick(). Cridar-lo dues vegades per frame
|
||||||
|
// deixaria a Game un dt ~0 i la demo no avancaria (ni jugador ni globus).
|
||||||
|
if (demo_game_active_) {
|
||||||
|
// El demo Game necesita section->name == SECTION_PROG_GAME para funcionar
|
||||||
|
section_->name = SECTION_PROG_GAME;
|
||||||
|
demo_game_->iterate();
|
||||||
|
|
||||||
|
if (demo_game_->hasFinished()) {
|
||||||
|
// cppcheck-suppress knownConditionTrueFalse ; fals positiu: iterate() pot escriure section_->name=SECTION_PROG_QUIT (Alt+F4), cppcheck no creua la crida
|
||||||
|
const bool WAS_QUIT = (section_->name == SECTION_PROG_QUIT);
|
||||||
|
// Game::processDemoInput posa subsection=TITLE_INSTRUCTIONS només
|
||||||
|
// quan la demo s'acaba de manera natural (esgotat el playback).
|
||||||
|
// Si l'usuari l'ha saltat amb una tecla, la subsection queda en
|
||||||
|
// GAME_PLAY i tornem directament al titol, sense instructions.
|
||||||
|
const bool DEMO_ENDED_NATURALLY = (section_->subsection == SUBSECTION_TITLE_INSTRUCTIONS);
|
||||||
|
delete demo_game_;
|
||||||
|
demo_game_ = nullptr;
|
||||||
|
demo_game_active_ = false;
|
||||||
|
|
||||||
|
// cppcheck-suppress knownConditionTrueFalse ; fals positiu: WAS_QUIT depèn de iterate() que pot mutar section_->name
|
||||||
|
if (WAS_QUIT) {
|
||||||
|
section_->name = SECTION_PROG_QUIT;
|
||||||
|
} else if (demo_then_instructions_ && DEMO_ENDED_NATURALLY) {
|
||||||
|
section_->name = SECTION_PROG_TITLE;
|
||||||
|
section_->subsection = SUBSECTION_TITLE_3;
|
||||||
|
runInstructions(Instructions::Mode::AUTO);
|
||||||
|
} else {
|
||||||
|
// Demo saltada: tornem a l'estat final del titol (TITLE_3, menu
|
||||||
|
// visible i musica) i reiniciem el comptador de demo perque no
|
||||||
|
// salti immediatament una altra vegada.
|
||||||
|
section_->name = SECTION_PROG_TITLE;
|
||||||
|
section_->subsection = SUBSECTION_TITLE_3;
|
||||||
|
demo_remaining_s_ = DEMO_TIMEOUT_S;
|
||||||
|
}
|
||||||
|
demo_then_instructions_ = false;
|
||||||
|
// Reset del rellotge per evitar un dt enorme al tornar al Title.
|
||||||
|
DeltaTime::reset();
|
||||||
|
} else {
|
||||||
|
// Restaura section para que Director no transicione fuera de Title
|
||||||
|
section_->name = SECTION_PROG_TITLE;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const float DELTA_TIME_S = DeltaTime::tick();
|
const float DELTA_TIME_S = DeltaTime::tick();
|
||||||
|
|
||||||
// Si las instrucciones están activas, delega el frame
|
// Si las instrucciones están activas, delega el frame
|
||||||
@@ -882,40 +927,6 @@ void Title::iterate() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si el juego demo está activo, delega el frame
|
|
||||||
if (demo_game_active_) {
|
|
||||||
// El demo Game necesita section->name == SECTION_PROG_GAME para funcionar
|
|
||||||
section_->name = SECTION_PROG_GAME;
|
|
||||||
demo_game_->iterate();
|
|
||||||
|
|
||||||
if (demo_game_->hasFinished()) {
|
|
||||||
// cppcheck-suppress knownConditionTrueFalse ; fals positiu: iterate() pot escriure section_->name=SECTION_PROG_QUIT (Alt+F4), cppcheck no creua la crida
|
|
||||||
const bool WAS_QUIT = (section_->name == SECTION_PROG_QUIT);
|
|
||||||
delete demo_game_;
|
|
||||||
demo_game_ = nullptr;
|
|
||||||
demo_game_active_ = false;
|
|
||||||
|
|
||||||
// cppcheck-suppress knownConditionTrueFalse ; fals positiu: WAS_QUIT depèn de iterate() que pot mutar section_->name
|
|
||||||
if (WAS_QUIT) {
|
|
||||||
section_->name = SECTION_PROG_QUIT;
|
|
||||||
} else if (demo_then_instructions_) {
|
|
||||||
section_->name = SECTION_PROG_TITLE;
|
|
||||||
section_->subsection = SUBSECTION_TITLE_3;
|
|
||||||
demo_then_instructions_ = false;
|
|
||||||
runInstructions(Instructions::Mode::AUTO);
|
|
||||||
} else {
|
|
||||||
section_->name = SECTION_PROG_TITLE;
|
|
||||||
section_->subsection = SUBSECTION_TITLE_1;
|
|
||||||
}
|
|
||||||
// Reset del rellotge per evitar un dt enorme al tornar al Title.
|
|
||||||
DeltaTime::reset();
|
|
||||||
} else {
|
|
||||||
// Restaura section para que Director no transicione fuera de Title
|
|
||||||
section_->name = SECTION_PROG_TITLE;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ejecución normal del título
|
// Ejecución normal del título
|
||||||
update(DELTA_TIME_S);
|
update(DELTA_TIME_S);
|
||||||
render();
|
render();
|
||||||
@@ -941,7 +952,16 @@ void Title::handleEvent(const SDL_Event *event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (section_->subsection == SUBSECTION_TITLE_3) {
|
if (section_->subsection == SUBSECTION_TITLE_3) {
|
||||||
if ((event->type == SDL_EVENT_KEY_UP) || (event->type == SDL_EVENT_JOYSTICK_BUTTON_UP)) {
|
// Activa menu i reinicia el countdown de demo nomes amb tecles "humanes".
|
||||||
|
// F1-F12 i ESC son hotkeys globals (zoom, fullscreen, shaders, exit, version)
|
||||||
|
// i no han d'interferir amb el flux de l'UI del titol.
|
||||||
|
bool human_input = (event->type == SDL_EVENT_JOYSTICK_BUTTON_UP);
|
||||||
|
if (event->type == SDL_EVENT_KEY_UP) {
|
||||||
|
const SDL_Scancode S = event->key.scancode;
|
||||||
|
const bool IS_RESERVED = (S == SDL_SCANCODE_ESCAPE) || (S >= SDL_SCANCODE_F1 && S <= SDL_SCANCODE_F12);
|
||||||
|
human_input = !IS_RESERVED;
|
||||||
|
}
|
||||||
|
if (human_input) {
|
||||||
menu_visible_ = true;
|
menu_visible_ = true;
|
||||||
demo_remaining_s_ = DEMO_TIMEOUT_S;
|
demo_remaining_s_ = DEMO_TIMEOUT_S;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class Title {
|
|||||||
void handleEvent(const SDL_Event *event); // Procesa un evento
|
void handleEvent(const SDL_Event *event); // Procesa un evento
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr const char *COPYRIGHT = "@2020 JailDesigner (v2.3.4)";
|
static constexpr const char *COPYRIGHT = "@2020 JailDesigner";
|
||||||
// Time-based: temps màxim a la pantalla del títol abans de tornar al
|
// Time-based: temps màxim a la pantalla del títol abans de tornar al
|
||||||
// logo o llançar el demo. 800 frames a 60Hz ⇒ 13.333 s.
|
// logo o llançar el demo. 800 frames a 60Hz ⇒ 13.333 s.
|
||||||
static constexpr float DEMO_TIMEOUT_S = 13.333F;
|
static constexpr float DEMO_TIMEOUT_S = 13.333F;
|
||||||
|
|||||||
+1
-1
@@ -58,6 +58,6 @@ auto SDL_AppEvent(void *appstate, SDL_Event *event) -> SDL_AppResult {
|
|||||||
return static_cast<Director *>(appstate)->handleEvent(event);
|
return static_cast<Director *>(appstate)->handleEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SDL_AppQuit(void *appstate, SDL_AppResult result) {
|
void SDL_AppQuit(void *appstate, SDL_AppResult /*result*/) {
|
||||||
delete static_cast<Director *>(appstate);
|
delete static_cast<Director *>(appstate);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
namespace Texts {
|
namespace Texts {
|
||||||
constexpr const char* VERSION = "2.3.4"; // Versión del juego (también usada por el Makefile)
|
constexpr const char* VERSION = "2.4.0"; // Font de veritat: tambe la usen el Makefile (release names) i CMakeLists (project VERSION)
|
||||||
} // namespace Texts
|
} // namespace Texts
|
||||||
|
|||||||
@@ -60,16 +60,6 @@ struct Section {
|
|||||||
Uint8 subsection;
|
Uint8 subsection;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Estructura para mapear el teclado usado en la demo
|
|
||||||
struct DemoKeys {
|
|
||||||
Uint8 left;
|
|
||||||
Uint8 right;
|
|
||||||
Uint8 no_input;
|
|
||||||
Uint8 fire;
|
|
||||||
Uint8 fire_left;
|
|
||||||
Uint8 fire_right;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Estructura para albergar métodos de control
|
// Estructura para albergar métodos de control
|
||||||
struct InputDevice {
|
struct InputDevice {
|
||||||
int id; // Identificador en el vector de mandos
|
int id; // Identificador en el vector de mandos
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Version {
|
||||||
|
constexpr const char* GIT_HASH = "@GIT_HASH@";
|
||||||
|
constexpr const char* APP_NAME = "Coffee Crisis";
|
||||||
|
} // namespace Version
|
||||||
@@ -3,13 +3,12 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/resources/resource_pack.h"
|
#include "core/resources/resource_pack.h"
|
||||||
|
#include "../../build/version.h" // Version::APP_NAME
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr const char* APP_NAME = "Coffee Crisis";
|
|
||||||
|
|
||||||
void showHelp() {
|
void showHelp() {
|
||||||
std::cout << APP_NAME << " - Resource Packer\n";
|
std::cout << Version::APP_NAME << " - Resource Packer\n";
|
||||||
std::cout << "===============================\n";
|
std::cout << "===============================\n";
|
||||||
std::cout << "Usage: pack_resources [options] [input_dir] [output_file]\n\n";
|
std::cout << "Usage: pack_resources [options] [input_dir] [output_file]\n\n";
|
||||||
std::cout << "Options:\n";
|
std::cout << "Options:\n";
|
||||||
@@ -66,7 +65,7 @@ int main(int argc, char* argv[]) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << APP_NAME << " - Resource Packer\n";
|
std::cout << Version::APP_NAME << " - Resource Packer\n";
|
||||||
std::cout << "===============================\n";
|
std::cout << "===============================\n";
|
||||||
std::cout << "Input directory: " << data_dir << '\n';
|
std::cout << "Input directory: " << data_dir << '\n';
|
||||||
std::cout << "Output file: " << output_file << '\n';
|
std::cout << "Output file: " << output_file << '\n';
|
||||||
|
|||||||
Reference in New Issue
Block a user