Compare commits

..

20 Commits

Author SHA1 Message Date
JailDesigner a43c3fc5d1 afegeix CHANGELOG.md per a release v2.4.0 2026-05-19 21:04:10 +02:00
JailDesigner bdbb6bc764 Merge branch 'ui-fixes' 2026-05-19 21:00:22 +02:00
JailDesigner fa2dc9bbf3 fps: overlay debug a dalt-dreta del canvas (verd, F10 toggle, 8bithud, alineat amb notificacions i overscan-aware) 2026-05-19 20:59:35 +02:00
JailDesigner 73f210bc2c notifications: en overscan, desplaca la Y a la primera fila visible del canvas segons aspect ratio finestra/canvas 2026-05-19 20:48:26 +02:00
JailDesigner 74d96047c7 hotkey: F7 toggle vsync + F8 next presentation mode amb notificacions 2026-05-19 20:30:16 +02:00
JailDesigner 20325ddd5a presentation: bool integer_scale -> enum PresentationMode (integer_scale|letterbox|stretched|overscan) amb migracio de configs antics 2026-05-19 20:29:22 +02:00
JailDesigner ac997c185d notifications: paleta semi-saturada (mig cami entre pastel i color pur) 2026-05-19 20:08:07 +02:00
JailDesigner 5fcbce6e7b title: treu '(v2.3.4)' del COPYRIGHT (ara F11 mostra app+version+githash) 2026-05-19 20:04:35 +02:00
JailDesigner 984d1fca50 hotkey: F11 mostra notificacio blava amb 'AppName vX.Y.Z (githash)' 2026-05-19 20:04:12 +02:00
JailDesigner 66ad34b667 version: defines.hpp es la unica font de veritat (CMakeLists l'extreu via regex) + bump 2.3.4 -> 2.4.0 2026-05-19 20:04:06 +02:00
JailDesigner bded70a52a ui: F1-F12 i ESC deixen de comptar com a any-key skip (logo/intro/instructions/title/demo) 2026-05-19 20:03:11 +02:00
JailDesigner 1129f1116e Merge branch 'demo-time-based' 2026-05-19 19:43:31 +02:00
JailDesigner 1ddc821f6f demo: en saltar amb tecla torna a TITLE_3 (menu visible) en lloc de instructions/TITLE_1, reseteja el comptador de demo 2026-05-19 19:43:02 +02:00
JailDesigner 49be109560 demo: Title no consumeix dt quan delega a demo_game (era doble tick, deixava la demo congelada a frame 0) 2026-05-19 19:32:59 +02:00
JailDesigner 63eaaa8b5c demo time-based: porta el patro de CCAE (multi-set, index = elapsed_s*60, % size per safe loop), substitueix demo.bin per demo1/2/3.bin 2026-05-19 19:16:36 +02:00
JailDesigner 748673f41b afegeix generació de version.h amb git hash 2026-05-19 18:58:56 +02:00
JailDesigner 8af4b0c259 llista explícita de fonts en lloc de GLOB_RECURSE 2026-05-19 18:53:53 +02:00
JailDesigner be1a9a1d9b activa -Wextra -Wpedantic i neteja warnings 2026-05-19 18:49:51 +02:00
JailDesigner 7bd4d4d114 alinea CMake amb la resta de projectes 2026-05-19 18:46:18 +02:00
JailDesigner 0148ccc4d5 Merge branch 'time-based' 2026-05-19 18:42:07 +02:00
37 changed files with 851 additions and 270 deletions
+66
View File
@@ -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 F1F12 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
View File
@@ -1,45 +1,120 @@
# CMakeLists.txt
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
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
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++
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Configuración global de flags de compilación
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Os -ffunction-sections -fdata-sections")
# --- GENERACIÓN DE VERSIÓN AUTOMÁTICA ---
# Si GIT_HASH se ha pasado desde fuera (p.ej. desde el Makefile via -DGIT_HASH=xxx),
# 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
set(DIR_SOURCES "${CMAKE_SOURCE_DIR}/source")
# Cargar todos los archivos fuente en DIR_SOURCES (recursivo, sin external/)
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "${DIR_SOURCES}/*.cpp")
list(FILTER SOURCES EXCLUDE REGEX "${DIR_SOURCES}/external/.*")
# --- LISTA EXPLÍCITA DE FUENTES ---
set(APP_SOURCES
source/main.cpp
# En Emscripten no compilamos sdl3gpu_shader (SDL3 GPU no está soportado en WebGL2).
# Define NO_SHADERS más abajo y filtra el fuente aquí.
if(EMSCRIPTEN)
list(REMOVE_ITEM SOURCES "${DIR_SOURCES}/core/rendering/sdl3gpu/sdl3gpu_shader.cpp")
endif()
# --- core/audio ---
source/core/audio/audio.cpp
source/core/audio/audio_adapter.cpp
# Verificar si se encontraron archivos fuente
if(NOT SOURCES)
message(FATAL_ERROR "No se encontraron archivos fuente en ${DIR_SOURCES}.")
endif()
# --- core/input ---
source/core/input/global_inputs.cpp
source/core/input/input.cpp
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
if(EMSCRIPTEN)
@@ -114,14 +189,26 @@ else()
endif()
# 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)
add_dependencies(${PROJECT_NAME} shaders)
endif()
# 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
target_compile_definitions(${PROJECT_NAME} PRIVATE
@@ -184,8 +271,6 @@ file(GLOB_RECURSE 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).
set(CPPCHECK_SOURCES ${ALL_SOURCE_FILES})
list(FILTER CPPCHECK_SOURCES INCLUDE REGEX ".*\\.cpp$")
@@ -218,7 +303,7 @@ if(CLANG_FORMAT_EXE)
add_custom_target(format
COMMAND ${CLANG_FORMAT_EXE}
-i
${FORMAT_SOURCES}
${ALL_SOURCE_FILES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Running clang-format..."
)
@@ -227,7 +312,7 @@ if(CLANG_FORMAT_EXE)
COMMAND ${CLANG_FORMAT_EXE}
--dry-run
--Werror
${FORMAT_SOURCES}
${ALL_SOURCE_FILES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Checking clang-format..."
)
@@ -268,7 +353,7 @@ if(NOT EMSCRIPTEN)
source/core/resources/resource_pack.cpp
)
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
target_compile_options(pack_resources PRIVATE -Wall)
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
# Regeneració automàtica de resources.pack en cada build si canvia data/.
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
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

+37
View File
@@ -7,6 +7,8 @@
#include "core/rendering/notifications.hpp"
#include "core/rendering/screen.h"
#include "game/options.hpp"
#include "utils/defines.hpp"
#include "version.h"
namespace GlobalInputs {
@@ -54,6 +56,23 @@ namespace GlobalInputs {
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() {
const Uint32 NOW = SDL_GetTicks();
if (NOW < exit_window_until_ticks) {
@@ -94,6 +113,24 @@ namespace GlobalInputs {
notifyShaderEnabled();
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.
if (Screen::isShaderEnabled()) {
if (Input::get()->checkInput(Input::Action::TOGGLE_SHADER_TYPE, Input::Repeat::OFF)) {
+27 -7
View File
@@ -157,25 +157,45 @@ auto Input::checkGameControllerInput(Action input, Repeat repeat, int index) ->
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 {
if (device == Device::ANY) {
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) {
const bool *key_states = SDL_GetKeyboardState(nullptr);
if (std::ranges::any_of(key_bindings_,
[key_states](const auto &key_binding) { return key_states[key_binding.scancode]; })) {
return true;
for (std::size_t i = 0; i < key_bindings_.size(); ++i) {
if (is_skippable(static_cast<Action>(i)) && key_states[key_bindings_[i].scancode]) {
return true;
}
}
}
if (gameControllerFound() && index >= 0 && index < (int)connected_controllers_.size()) {
if (device == Device::GAMECONTROLLER || device == Device::ANY) {
for (auto &game_controller_binding : game_controller_bindings_) {
if (SDL_GetGamepadButton(connected_controllers_[index], game_controller_binding.button)) {
for (std::size_t i = 0; i < game_controller_bindings_.size(); ++i) {
if (is_skippable(static_cast<Action>(i)) && SDL_GetGamepadButton(connected_controllers_[index], game_controller_bindings_[i].button)) {
return true;
}
}
+11 -5
View File
@@ -50,6 +50,12 @@ class Input {
TOGGLE_SHADER,
TOGGLE_SHADER_TYPE,
// Diagnostic / video toggles
SHOW_VERSION,
TOGGLE_VSYNC,
NEXT_PRESENTATION_MODE,
TOGGLE_FPS,
// Centinela final (usar para sizing)
NUMBER_OF_INPUTS
};
@@ -61,12 +67,12 @@ class Input {
~Input(); // Destructor
void update(); // Actualiza el estado del objeto
void update(); // Actualiza el estado del objeto
void bindKey(Action input, SDL_Scancode code); // Asigna inputs a teclas
void bindGameControllerButton(Action input, SDL_GamepadButton button); // Asigna inputs a botones del mando
auto checkInput(Action input, Repeat repeat = Repeat::ON, Device device = Device::ANY, int index = 0) -> bool; // Comprueba si un input esta activo
auto checkAnyInput(Device device = Device::ANY, int index = 0) -> bool; // Comprueba si hay almenos un input activo
auto checkAnyInput(Device device = Device::ANY, int index = 0) -> bool; // Comprueba si hay almenos un input activo
auto discoverGameController() -> bool; // Busca si hay un mando conectado
@@ -82,9 +88,9 @@ class Input {
[[nodiscard]] auto getNumControllers() const -> int; // Obten el numero de mandos conectados
auto getControllerName(int index) -> std::string; // Obten el nombre de un mando de juego
void setVerbose(bool value); // Establece si ha de mostrar mensajes
void disableUntil(Disable value); // Deshabilita las entradas durante un periodo de tiempo
void enable(); // Hablita las entradas
void setVerbose(bool value); // Establece si ha de mostrar mensajes
void disableUntil(Disable value); // Deshabilita las entradas durante un periodo de tiempo
void enable(); // Hablita las entradas
private:
struct KeyBindings {
+1 -1
View File
@@ -351,7 +351,7 @@ void AnimatedSprite::update(float dt_s) {
}
// 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});
}
+7 -6
View File
@@ -6,13 +6,14 @@
namespace Notifications {
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.)
const Color INFO_COLOR{0xF0, 0xE0, 0x90}; // groc trigo
const Color TOGGLE_COLOR{0xA0, 0xE0, 0xF0}; // cian gel
const Color CHOICE_COLOR{0xE0, 0xA0, 0xE0}; // rosa orquídia
const Color SUCCESS_COLOR{0xB0, 0xE6, 0xB0}; // verd menta
const Color DANGER_COLOR{0xF0, 0xA0, 0xA0}; // rosa salmó
const Color INFO_COLOR{0xF0, 0xD0, 0x40}; // groc
const Color TOGGLE_COLOR{0x60, 0xC0, 0xF0}; // cian
const Color CHOICE_COLOR{0xD0, 0x60, 0xD0}; // magenta
const Color SUCCESS_COLOR{0x70, 0xD0, 0x70}; // verd
const Color DANGER_COLOR{0xF0, 0x60, 0x60}; // vermell
// 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
+152 -36
View File
@@ -169,9 +169,12 @@ void Screen::start() {
// Vuelca el contenido del renderizador en pantalla
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_);
renderNotification();
renderFps();
#ifndef NO_SHADERS
// Si el backend GPU està viu i accelerat, passem sempre per ell (tant amb
@@ -326,16 +329,27 @@ void Screen::detectMaxZoom() {
#endif
}
// Establece el escalado entero
void Screen::setIntegerScale(bool enabled) {
if (Options::video.integer_scale == enabled) { return; }
Options::video.integer_scale = enabled;
// Estableix el mode de presentacio del canvas i reaplica el layout
void Screen::setPresentationMode(Options::PresentationMode mode) {
if (Options::video.presentation_mode == mode) { return; }
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);
}
// Alterna el escalado entero
void Screen::toggleIntegerScale() {
setIntegerScale(!Options::video.integer_scale);
// Cicla integer_scale -> letterbox -> stretched -> overscan -> integer_scale
void Screen::nextPresentationMode() {
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
@@ -407,39 +421,75 @@ void Screen::applyFullscreenLayout() {
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() {
if (Options::video.integer_scale) {
// Calcula el tamaño de la escala máxima
int scale = 0;
while (((game_canvas_width_ * (scale + 1)) <= window_width_) && ((game_canvas_height_ * (scale + 1)) <= window_height_)) {
scale++;
}
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_);
dest_.w = game_canvas_width_ * scale;
dest_.h = game_canvas_height_ * scale;
dest_.x = (window_width_ - dest_.w) / 2;
dest_.y = (window_height_ - dest_.h) / 2;
} else {
// Manté la relació d'aspecte sense escalat enter (letterbox/pillarbox).
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_.w = static_cast<int>(std::lround(window_height_ * ratio));
dest_.x = (window_width_ - dest_.w) / 2;
dest_.y = (window_height_ - dest_.h) / 2;
} else {
switch (Options::video.presentation_mode) {
case Options::PresentationMode::INTEGER_SCALE: {
int scale = 0;
while (((game_canvas_width_ * (scale + 1)) <= window_width_) && ((game_canvas_height_ * (scale + 1)) <= window_height_)) {
scale++;
}
dest_.w = game_canvas_width_ * scale;
dest_.h = game_canvas_height_ * scale;
break;
}
case Options::PresentationMode::LETTERBOX: {
if (WINDOW_RATIO >= CANVAS_RATIO) {
dest_.h = window_height_;
dest_.w = static_cast<int>(std::lround(window_height_ * CANVAS_RATIO));
} else {
dest_.w = window_width_;
dest_.h = static_cast<int>(std::lround(window_width_ / CANVAS_RATIO));
}
break;
}
case Options::PresentationMode::STRETCHED: {
dest_.w = window_width_;
dest_.h = static_cast<int>(std::lround(window_width_ / ratio));
dest_.x = (window_width_ - dest_.w) / 2;
dest_.y = (window_height_ - dest_.h) / 2;
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_.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) {
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;
}
@@ -461,14 +511,16 @@ void Screen::clearNotification() {
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() {
if (notification_text_ == nullptr || SDL_GetTicks() >= notification_end_time_) {
return;
}
notification_text_->writeDX(Text::FLAG_CENTER | Text::FLAG_COLOR | Text::FLAG_STROKE,
game_canvas_width_ / 2,
notification_y_,
notification_y_ + safeNotificationY(),
notification_message_,
1,
notification_text_color_,
@@ -476,6 +528,70 @@ void Screen::renderNotification() {
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
// principi del fitxer i l'anonymous namespace amb els callbacks natius).
@@ -545,7 +661,7 @@ void Screen::initShaders() {
}
}
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);
// Resol els índexs de preset a partir del nom emmagatzemat al config.
+30 -13
View File
@@ -6,7 +6,8 @@
#include <string> // for string
#include <vector> // for vector
#include "utils/utils.h" // for Color
#include "game/options.hpp" // for Options::PresentationMode
#include "utils/utils.h" // for Color
#ifndef NO_SHADERS
#include "core/rendering/shader_backend.hpp" // for Rendering::ShaderType
@@ -51,17 +52,18 @@ class Screen {
void blit(); // Vuelca el contenido del renderizador en pantalla
// Video y ventana
void setVideoMode(bool fullscreen); // Establece el modo de video
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
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 setIntegerScale(bool enabled); // Establece el escalado entero
void toggleVSync(); // Alterna 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 incWindowZoom() -> bool; // Aumenta el zoom de la ventana (devuelve true si cambió)
auto setWindowZoom(int zoom) -> bool; // Establece el zoom de la ventana (devuelve true si cambió)
void setVideoMode(bool fullscreen); // Establece el modo de video
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
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 nextPresentationMode(); // Cicla integer_scale -> letterbox -> stretched -> overscan
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 setVSync(bool enabled); // Establece el V-Sync
auto decWindowZoom() -> bool; // Reduce el zoom de la ventana (devuelve true si cambió)
auto incWindowZoom() -> bool; // Aumenta el zoom de la ventana (devuelve true si cambió)
auto setWindowZoom(int zoom) -> bool; // Establece el zoom de la ventana (devuelve true si cambió)
// Borde
void setBorderColor(Color color); // Cambia el color del borde
@@ -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 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.
void initShaders(); // Crea el backend GPU si no existe y lo inicializa
void shutdownShaders(); // Libera el backend GPU
@@ -110,7 +116,12 @@ class Screen {
void registerEmscriptenEventCallbacks(); // Registra los callbacks nativos de Emscripten para fullscreenchange y orientationchange. No-op fuera de Emscripten
// 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
// 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
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
// GPU / shaders
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)
// ---------------------------------------------------------------------------
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 vh = 0.0F;
if (integer_scale_) {
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);
vh = static_cast<float>(game_height_ * SCALE);
} else {
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;
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_));
vw = static_cast<float>(game_width_ * SCALE);
vh = static_cast<float>(game_height_ * SCALE);
break;
}
case PresentationMode::LETTERBOX: {
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((static_cast<float>(sw) - vw) * 0.5F);
const float VY = std::floor((static_cast<float>(sh) - vh) * 0.5F);
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};
}
@@ -569,8 +596,8 @@ namespace Rendering {
}
}
void SDL3GPUShader::setScaleMode(bool integer_scale) {
integer_scale_ = integer_scale;
void SDL3GPUShader::setPresentationMode(PresentationMode mode) {
presentation_mode_ = mode;
}
// ---------------------------------------------------------------------------
@@ -79,7 +79,7 @@ namespace Rendering {
const std::string& fragment_source) -> bool override;
void render() override;
void setTextureSize(float width, float height) override {}
void setTextureSize(float /*width*/, float /*height*/) override {}
void cleanup() final; // Libera pipeline/texturas pero mantiene el device vivo
void destroy(); // Limpieza completa (device + swapchain); llamar solo al cerrar
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
@@ -97,8 +97,8 @@ namespace Rendering {
// Activa/desactiva VSync en el swapchain
void setVSync(bool vsync) override;
// Activa/desactiva escalado entero (integer scale)
void setScaleMode(bool integer_scale) override;
// Estableix el mode de presentacio del canvas
void setPresentationMode(PresentationMode mode) override;
// Selecciona el shader de post-procesado activo (POSTFX o CRTPI)
void setActiveShader(ShaderType type) override;
@@ -153,8 +153,40 @@ namespace Rendering {
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
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};
CrtPiUniforms crtpi_uniforms_{.scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = 1, .enable_multisample = 1, .enable_gamma = 1};
PostFXUniforms uniforms_{
.vignette_strength = 0.6F,
.chroma_min = 0.15F,
.scanline_strength = 0.7F,
.screen_height = 192.0F,
.mask_strength = 0.0F,
.gamma_strength = 0.0F,
.curvature = 0.0F,
.bleeding = 0.0F,
.pixel_scale = 1.0F,
.time = 0.0F,
.flicker = 0.0F,
.chroma_max = 0.15F,
.scan_dark_ratio = 0.333F,
.scan_dark_floor = 0.42F,
.scan_edge_soft = 1.0F,
.pad3 = 0.0F};
CrtPiUniforms crtpi_uniforms_{
.scanline_weight = 6.0F,
.scanline_gap_brightness = 0.12F,
.bloom_factor = 3.5F,
.input_gamma = 2.4F,
.output_gamma = 2.2F,
.mask_brightness = 0.80F,
.curvature_x = 0.05F,
.curvature_y = 0.10F,
.mask_type = 2,
.enable_scanlines = 1,
.enable_multisample = 1,
.enable_gamma = 1,
.enable_curvature = 0,
.enable_sharper = 0,
.texture_width = 0.0F,
.texture_height = 0.0F};
ShaderType active_shader_ = ShaderType::POSTFX; // Shader de post-procesado activo
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)
bool is_initialized_ = false;
bool vsync_ = true;
bool integer_scale_ = false;
PresentationMode presentation_mode_ = PresentationMode::INTEGER_SCALE;
};
} // namespace Rendering
+9 -2
View File
@@ -112,9 +112,16 @@ namespace Rendering {
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
+2 -1
View File
@@ -1,5 +1,6 @@
#pragma once
#include <cstddef> // for size_t
#include <cstdint> // for uint8_t
#include <string> // for string, basic_string
#include <vector> // for vector
@@ -41,7 +42,7 @@ class Asset {
private:
// 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::string executable_path_; // Ruta al ejecutable
bool verbose_{true}; // Indica si ha de mostrar información por pantalla
+3 -2
View File
@@ -144,8 +144,9 @@ void Resource::loadDataAsset(const std::string &bname, const std::vector<uint8_t
lines.push_back(line);
}
animation_lines_[bname] = std::move(lines);
} else if (bname == "demo.bin") {
demo_bytes_ = bytes;
} else if (bname.size() > 5 && bname.substr(0, 4) == "demo" && bname.substr(bname.size() - 4) == ".bin") {
// 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
}
+3 -2
View File
@@ -29,7 +29,8 @@ class Resource {
auto getAnimationLines(const std::string &name) -> std::vector<std::string> &;
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 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:
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, Text *> texts_;
std::unordered_map<std::string, Menu *> menus_;
std::vector<uint8_t> demo_bytes_;
std::vector<std::vector<uint8_t>> demo_bytes_;
static Resource *instance;
};
+17
View File
@@ -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;
}
+47
View File
@@ -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;
+8 -2
View File
@@ -232,6 +232,10 @@ void Director::initInput() {
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::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
Input::get()->bindGameControllerButton(Input::Action::UP, SDL_GAMEPAD_BUTTON_DPAD_UP);
@@ -338,7 +342,9 @@ auto Director::setFileList() -> bool {
// Ficheros de configuración
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
Asset::get()->add(PREFIX + "/data/music/intro.ogg", Asset::Type::MUSIC);
@@ -496,7 +502,7 @@ void Director::createSystemFolder(const std::string &folder) {
// En Emscripten no necesitamos crear carpetas (MEMFS las crea automáticamente)
(void)folder;
#else
struct stat st = {.st_dev = 0};
struct stat st{};
if (stat(system_folder_.c_str(), &st) == -1) {
errno = 0;
#ifdef _WIN32
+2 -1
View File
@@ -19,7 +19,8 @@ namespace Defaults::Video {
constexpr SDL_ScaleMode SCALE_MODE = SDL_ScaleMode::SDL_SCALEMODE_NEAREST;
constexpr bool FULLSCREEN = false;
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 const char *GPU_PREFERRED_DRIVER = "";
constexpr bool SHADER_ENABLED = false;
+93 -69
View File
@@ -277,7 +277,8 @@ void Game::init() {
// Modo demo
demo_.recording = false;
demo_.counter = 0;
demo_.elapsed_s = 0.0F;
demo_.index = 0;
// Inicializa el objeto para el fundido
fade_->init(0x27, 0x27, 0x36);
@@ -521,34 +522,31 @@ auto Game::loadScoreFile() -> bool {
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 {
// Lee los datos de la demo desde Resource (precargados al arrancar).
const auto &bytes = Resource::get()->getDemoBytes();
const size_t EXPECTED = sizeof(DemoKeys) * TOTAL_DEMO_DATA;
if (bytes.size() >= EXPECTED) {
for (int i = 0; i < TOTAL_DEMO_DATA; ++i) {
memcpy(&demo_.data_file[i], bytes.data() + (i * sizeof(DemoKeys)), sizeof(DemoKeys));
}
if (Options::settings.console) {
std::cout << "Demo data loaded (" << bytes.size() << " bytes)" << '\n';
}
} else {
// Si no hay datos (bytes vacíos o tamaño inválido), inicializamos a cero.
if (Options::settings.console) {
std::cout << "Warning: demo data missing or too small, initializing to zero" << '\n';
}
for (auto &i : demo_.data_file) {
demo_.keys.left = 0;
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;
demo_.data.clear();
const size_t NUM = Resource::get()->getDemoCount();
for (size_t i = 0; i < NUM; ++i) {
DemoData dd = loadDemoDataFromBytes(Resource::get()->getDemoBytes(i));
if (!dd.empty()) {
demo_.data.push_back(std::move(dd));
}
}
return true;
if (!demo_.data.empty()) {
demo_selected_set_ = static_cast<size_t>(rand()) % demo_.data.size();
} else {
demo_selected_set_ = 0;
}
if (Options::settings.console) {
if (demo_.data.empty()) {
std::cout << "Warning: no demo data loaded" << '\n';
} else {
std::cout << "Demo data loaded (" << demo_.data.size() << " sets, playing #" << demo_selected_set_ << ")" << '\n';
}
}
return !demo_.data.empty();
}
// Guarda el fichero de puntos
@@ -577,32 +575,29 @@ auto Game::saveScoreFile() -> bool {
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 {
bool success = true;
const std::string P = Asset::get()->get("demo.bin");
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");
if (file != nullptr) {
// Guardamos los datos
for (auto &i : demo_.data_file) {
SDL_WriteIO(file, &i, sizeof(DemoKeys));
}
if (Options::settings.console) {
std::cout << "Writing file " << FILE_NAME.c_str() << '\n';
}
// Cerramos el fichero
SDL_CloseIO(file);
} else {
if (Options::settings.console) {
std::cout << "Error: Unable to save " << FILE_NAME.c_str() << " file! " << SDL_GetError() << '\n';
}
}
if (!demo_.recording || demo_.data.empty()) {
return true;
}
return success;
const std::string P = Asset::get()->get("demo1.bin");
const std::string FILE_NAME = P.substr(P.find_last_of("\\/") + 1);
SDL_IOStream *file = SDL_IOFromFile(P.c_str(), "w+b");
if (file == nullptr) {
if (Options::settings.console) {
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';
}
SDL_CloseIO(file);
return true;
}
// Inicializa las formaciones enemigas
@@ -2276,6 +2271,12 @@ void Game::update(float dt_s) {
elapsed_s_ += dt_s;
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();
updatePlayers(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() {
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) {
players_[INDEX]->setInput(Input::Action::LEFT);
@@ -2479,18 +2503,10 @@ void Game::processDemoInput() {
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()) {
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
@@ -2553,14 +2569,22 @@ void Game::processPlayerLiveInput(Player *player, int i) {
section_->subsection = SUBSECTION_GAME_PAUSE;
}
// Grabación de demo
if (demo_.counter < TOTAL_DEMO_DATA) {
if (demo_.recording) {
demo_.data_file[demo_.counter] = demo_.keys;
// Gravacio de demo (mode recording). L'index ja s'ha actualitzat a
// Game::update() via elapsed_s; aqui nomes escrivim el frame actual al
// primer set, redimensionant on demand.
if (demo_.recording) {
if (demo_.index >= TOTAL_DEMO_DATA) {
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;
}
demo_.counter++;
} else if (demo_.recording) {
section_->name = SECTION_PROG_QUIT;
}
}
@@ -2590,7 +2614,7 @@ void Game::renderMessages() {
// D E M O
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);
}
}
+3 -10
View File
@@ -6,9 +6,10 @@
#include <string> // for string, basic_string
#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/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 Fade;
class Menu;
@@ -45,7 +46,6 @@ class Game {
// Cantidad de elementos a escribir en los ficheros de datos
static constexpr int TOTAL_SCORE_DATA = 3;
static constexpr int TOTAL_DEMO_DATA = 2000;
// Contadores
static constexpr int STAGE_COUNTER = 200;
@@ -138,14 +138,6 @@ class Game {
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 render(); // Dibuja el juego
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
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
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 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)
+49 -3
View File
@@ -31,6 +31,42 @@ namespace Options {
std::string crtpi_file_path;
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
// 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).
@@ -82,7 +118,16 @@ namespace Options {
parseBoolField(vid, "fullscreen", video.fullscreen);
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);
if (tryGet<int>(vid, "scale_mode", 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
window.zoom = 4;
video.fullscreen = false;
video.integer_scale = true;
video.presentation_mode = PresentationMode::INTEGER_SCALE;
#endif
// Dispositius d'entrada per defecte
@@ -283,7 +328,8 @@ namespace Options {
file << "video:\n";
file << " fullscreen: " << boolToString(video.fullscreen) << "\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)
<< " # " << static_cast<int>(SDL_SCALEMODE_NEAREST) << ": nearest, "
<< static_cast<int>(SDL_SCALEMODE_LINEAR) << ": linear\n";
+16 -1
View File
@@ -18,6 +18,16 @@
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 {
std::string caption = Defaults::Window::CAPTION;
int zoom = Defaults::Window::ZOOM;
@@ -42,11 +52,16 @@ namespace Options {
SDL_ScaleMode scale_mode = Defaults::Video::SCALE_MODE;
bool fullscreen = Defaults::Video::FULLSCREEN;
bool vsync = Defaults::Video::VSYNC;
bool integer_scale = Defaults::Video::INTEGER_SCALE;
PresentationMode presentation_mode = PresentationMode::INTEGER_SCALE;
GPU gpu;
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 {
bool enabled = Defaults::Music::ENABLED;
float volume = Defaults::Music::VOLUME;
+1 -1
View File
@@ -239,7 +239,7 @@ void Instructions::checkInput() {
// pulsació; el quit es propaga via Director::iterate.
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) {
finished_ = true;
} else {
+2 -2
View File
@@ -197,7 +197,7 @@ void Intro::checkInput() {
// pulsació; el quit es propaga via Director::iterate.
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();
section_->name = SECTION_PROG_TITLE;
section_->subsection = SUBSECTION_TITLE_1;
@@ -412,6 +412,6 @@ void Intro::iterate() {
}
// 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
}
+2 -2
View File
@@ -65,7 +65,7 @@ void Logo::checkInput() {
// pulsació; el quit es propaga via Director::iterate.
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_->subsection = SUBSECTION_TITLE_1;
}
@@ -125,6 +125,6 @@ void Logo::iterate() {
}
// 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
}
+55 -35
View File
@@ -853,6 +853,51 @@ void Title::applyOptions() {
// Ejecuta un frame
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();
// Si las instrucciones están activas, delega el frame
@@ -882,40 +927,6 @@ void Title::iterate() {
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
update(DELTA_TIME_S);
render();
@@ -941,7 +952,16 @@ void Title::handleEvent(const SDL_Event *event) {
}
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;
demo_remaining_s_ = DEMO_TIMEOUT_S;
}
+1 -1
View File
@@ -33,7 +33,7 @@ class Title {
void handleEvent(const SDL_Event *event); // Procesa un evento
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
// logo o llançar el demo. 800 frames a 60Hz ⇒ 13.333 s.
static constexpr float DEMO_TIMEOUT_S = 13.333F;
+1 -1
View File
@@ -58,6 +58,6 @@ auto SDL_AppEvent(void *appstate, SDL_Event *event) -> SDL_AppResult {
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);
}
+1 -1
View File
@@ -1,5 +1,5 @@
#pragma once
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
+3 -13
View File
@@ -60,21 +60,11 @@ struct Section {
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
struct InputDevice {
int id; // Identificador en el vector de mandos
std::string name; // Nombre del dispositivo
Input::Device device_type; // Tipo de dispositivo (teclado o mando)
int id; // Identificador en el vector de mandos
std::string name; // Nombre del dispositivo
Input::Device device_type; // Tipo de dispositivo (teclado o mando)
};
// Calcula el cuadrado de la distancia entre dos puntos
+6
View File
@@ -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 -4
View File
@@ -3,13 +3,12 @@
#include <string>
#include "core/resources/resource_pack.h"
#include "../../build/version.h" // Version::APP_NAME
namespace {
constexpr const char* APP_NAME = "Coffee Crisis";
void showHelp() {
std::cout << APP_NAME << " - Resource Packer\n";
std::cout << Version::APP_NAME << " - Resource Packer\n";
std::cout << "===============================\n";
std::cout << "Usage: pack_resources [options] [input_dir] [output_file]\n\n";
std::cout << "Options:\n";
@@ -66,7 +65,7 @@ int main(int argc, char* argv[]) {
return 0;
}
std::cout << APP_NAME << " - Resource Packer\n";
std::cout << Version::APP_NAME << " - Resource Packer\n";
std::cout << "===============================\n";
std::cout << "Input directory: " << data_dir << '\n';
std::cout << "Output file: " << output_file << '\n';