Compare commits

...

33 Commits

Author SHA1 Message Date
2474283e07 afegit suppress a cppcheck 2026-04-18 12:28:19 +02:00
d58c0303e9 surface: hallazgo 1 — SurfaceData::width/height de float a int
Las dimensiones en píxeles son enteros por naturaleza. Convertidos los
miembros y constructores a int, y ajustados getWidth()/getHeight() para
devolver int. Eliminados los static_cast<int>(...->width/height) y
static_cast<int>(surface->getWidth/getHeight()) redundantes que sólo
existían para compensar el tipo erróneo.

Los callers que inicializan SDL_FRect directamente con getWidth/getHeight
requieren static_cast<float> explícito (sprite.cpp, animated_sprite.cpp,
notifier.cpp, title.cpp) por las reglas de narrowing de list-init.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:22:19 +02:00
5f0b4355e4 surface: hallazgo 4 — elimina render(6 floats) sin callers
La sobrecarga render(float dx, float dy, float sx, float sy, float w,
float h) no tenía un solo caller en el proyecto. Las otras dos
sobrecargas (con SDL_FRect) cubren todos los casos de uso reales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:16:46 +02:00
40ac657f74 surface: hallazgo 8 — elimina setSurfaceData muerto y documenta shared_ptr
setSurfaceData() no tenía callers. El shared_ptr<SurfaceData> se queda
porque render() puede aliasar el SurfaceData propio con el del renderer
surface (self-blit). Migrar a unique_ptr requeriría tocar Screen y
dissolve_sprite sin simplificación real.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:01:51 +02:00
6a12294a36 surface: hallazgo 5 — renderWithColorReplace aplica sub_palette_
Coherencia con render() y renderWithVerticalFade(): el píxel no
sustituido pasa por sub_palette_ en vez de copiarse crudo. Hoy es
no-op (las surfaces que usan color replace no hacen fadeSubPalette)
pero cierra la divergencia de API y previene regresiones futuras.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:58:46 +02:00
1f5b1ad1ab surface: hallazgo 2 — drawLine con Bresenham en enteros
El bucle usaba floats con comparación de igualdad exacta (x1==x2 &&
y1==y2) como condición de parada, con incrementos ±1.0f acumulados:
bug latente. Convertidos los endpoints de entrada con std::lround y
reescrito el algoritmo con ints. Firma pública float preservada para
no tocar callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:55:12 +02:00
b1413bbf8a surface: hallazgo 3 — sustituye sizeof check por static_assert en fade*Palette
palette_ y sub_palette_ son std::array de tamaño fijo, así que el check
en runtime nunca podía fallar. Movido a static_assert sobre tuple_size_v.
El throw asociado era código muerto.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:54:33 +02:00
eaf9d87d6d surface: hallazgo 6 — elimina doble std::min en render(int,int,...)
Las dos líneas de clamp contra el destino estaban duplicadas. Fusionado
el comentario y dejado un único bloque que limita contra origen y destino.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:53:59 +02:00
60adfc8fbb surface: hallazgo 7 — elimina NOLINT obsoleto en loadSurface
loadSurface es static en declaración y definición, así que el
NOLINT(readability-convert-member-functions-to-static) era dead noise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:53:33 +02:00
0dbf38f506 normalitzat Audio 2026-04-18 11:43:45 +02:00
53c2b345c9 build: unifica .clang-format/.clang-tidy i exclou external/ i spv/ amb dummies 2026-04-17 16:21:56 +02:00
74e19e9951 arreglos en make i cmake per estandaritzar amb la resta de projectes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 13:00:02 +02:00
7480616c07 fix: android input amb sdl_joystick 2026-04-15 23:54:15 +02:00
6a76c65771 make controllerdb
trim del nom del mando
2026-04-15 09:49:58 +02:00
9f22e1c58b opcions per amagar la pantalla de carrega 2026-04-15 09:23:36 +02:00
727e55af03 arreglos en screen 2026-04-15 06:31:43 +02:00
6e0d9235a3 undo android test 2026-04-13 21:26:41 +02:00
98f251d155 android test 2026-04-13 21:17:31 +02:00
acc1b0e8a1 canvis de paleta amb L i R del mando 2026-04-13 21:05:52 +02:00
49fb895984 fix: no restaurava la pantalla en emscripten al eixir de fullscreen 2026-04-13 20:42:40 +02:00
9047bd7d1f tone a commitar pa provar el canvi de pantalla en emscripten 2026-04-13 20:09:18 +02:00
9b8820ffa3 pantalla de carrega no bloquejant
streaming de audio per evitar precárrega i descompresió a memoria
2026-04-13 19:29:05 +02:00
585c93054e commit per a provar les coses rares de la pantalla en wasm 2026-04-13 18:31:16 +02:00
8bfc32de40 emscripten: no anava back en game 2026-04-13 18:06:54 +02:00
40766ad122 fix: en console faltava SCENE DEMO 2026-04-13 18:02:04 +02:00
e67aeb10fe fix: controls en el mando 2026-04-13 17:57:13 +02:00
5f293cbddf reordenades les layers del overlay
consola ara tanca i obri per temps en lloc de velocitat
2026-04-13 14:03:45 +02:00
7f470361cc soport de gamepad per a wasm 2026-04-13 13:20:50 +02:00
d9c41f420b fix: arrancar amb el borde desactivat feia crash al activarlo 2026-04-13 11:57:01 +02:00
023bbb224b arreglat el cuelgue de la precàrrega en wasm i afegit nom del recurs en curs
- CMakeLists.txt (Emscripten): afegit -fexceptions (compile + link) perquè
  fkyaml i altres throws ara es capturen pels try/catch enlloc de cridar
  abort(). També -sASSERTIONS=1 per veure missatges clars d'error en el
  runtime de Emscripten.
- resource_cache: abans de carregar cada recurs, desa el seu nom en
  current_loading_name_ i (en wasm/debug) el repinta immediatament sobre la
  barra de progrés. Ara, si la càrrega es penja en un fitxer concret, el
  nom queda visible en pantalla i ajuda a diagnosticar el problema.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 09:52:28 +02:00
70cfe5245d afegit suport Emscripten/WebAssembly al build system
- CMakeLists.txt: branca EMSCRIPTEN amb SDL3 via FetchContent, preload de data/
  config/ i gamecontrollerdb.txt, WebGL2, EMSCRIPTEN_BUILD define i sortida .html.
  Exclou sdl3gpu_shader (no soportat a WebGL2) i el pack_tool en wasm.
- Makefile: target wasm via Docker emscripten/emsdk, build a build/wasm i
  sortida a dist/wasm (.html .js .wasm .data).
- director.cpp: createSystemFolder utilitza MEMFS en wasm (sense pwd.h/unistd.h),
  executable_path buit, dev-mode forçat (filesystem preload, no pack), windowed.
- screen.cpp: initShaders és no-op en wasm (SDL3 GPU no suportat a WebGL2).
- global_inputs.cpp: handleQuit és no-op en wasm (no es pot eixir del joc).
- Director::handleEvent ignora SDL_EVENT_QUIT en wasm.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 09:02:00 +02:00
c32a880b6a refactor: migració a l'arquitectura SDL3 Callback API
Substitueix el bucle blocant main() → Director::run() → escena::run() per
SDL_AppInit/Iterate/Event/Quit. Cada escena implementa ara iterate() (un frame)
i handleEvent() (un event) sota una interfície base Scene.

- Director gestiona l'escena activa i les transicions via switchToActiveScene()
- Setup/cleanup que estava al voltant del while de run() mogut a ctor/dtor
  (música de Game/Ending/Ending2, volum de LoadingScreen)
- GlobalEvents ja no processa SDL_EVENT_QUIT (ho fa Director::handleEvent)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 08:49:19 +02:00
714de067c8 corregit makefile per a windows 2026-04-05 18:27:51 +02:00
71 changed files with 2455 additions and 1357 deletions

View File

@@ -2,29 +2,23 @@ Checks:
- readability-*
- modernize-*
- performance-*
- bugprone-unchecked-optional-access
- bugprone-sizeof-expression
- bugprone-suspicious-missing-comma
- bugprone-suspicious-index
- bugprone-undefined-memory-manipulation
- bugprone-use-after-move
- bugprone-out-of-bound-access
- bugprone-*
- -readability-identifier-length
- -readability-magic-numbers
- -bugprone-narrowing-conversions
- -performance-enum-size
- -performance-inefficient-string-concatenation
- -bugprone-integer-division
- -bugprone-easily-swappable-parameters
- -bugprone-narrowing-conversions
- -modernize-avoid-c-arrays,-warnings-as-errors
WarningsAsErrors: '*'
# Solo incluir archivos de tu código fuente (external tiene su propio .clang-tidy)
# Excluye jail_audio.hpp del análisis
HeaderFilterRegex: 'source/(?!core/audio/jail_audio\.hpp|core/rendering/sdl3gpu/.*_spv\.h).*'
# Solo headers del propio código fuente (external/ y spv/ tienen su propio .clang-tidy dummy)
HeaderFilterRegex: 'source/.*'
FormatStyle: file
CheckOptions:
# bugprone-empty-catch: aceptar catches vacíos marcados con @INTENTIONAL en un comentario
- { key: bugprone-empty-catch.IgnoreCatchWithKeywords, value: '@INTENTIONAL' }
# Variables locales en snake_case
- { key: readability-identifier-naming.VariableCase, value: lower_case }
@@ -45,7 +39,7 @@ CheckOptions:
# Variables estáticas privadas como miembros privados
- { key: readability-identifier-naming.StaticVariableCase, value: lower_case }
- { key: readability-identifier-naming.StaticVariableSuffix, value: _ }
# Constantes estáticas sin sufijo
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }

View File

@@ -3,6 +3,11 @@
cmake_minimum_required(VERSION 3.10)
project(jaildoctors_dilemma VERSION 1.00)
# 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()
# Establecer estándar de C++
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
@@ -11,17 +16,24 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# --- GENERACIÓN DE VERSIÓN AUTOMÁTICA ---
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
)
else()
set(GIT_HASH "unknown")
# 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
@@ -31,6 +43,7 @@ configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/versi
set(APP_SOURCES
# Core - Audio
source/core/audio/audio.cpp
source/core/audio/audio_adapter.cpp
# Core - Input
source/core/input/global_inputs.cpp
@@ -87,6 +100,7 @@ set(APP_SOURCES
source/game/gameplay/tilemap_renderer.cpp
# Game - Scenes
source/game/scenes/boot_loader.cpp
source/game/scenes/credits.cpp
source/game/scenes/ending.cpp
source/game/scenes/ending2.cpp
@@ -127,15 +141,31 @@ set(DEBUG_SOURCES
)
# Configuración de SDL3
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
if(EMSCRIPTEN)
# En Emscripten, SDL3 se compila desde source con FetchContent
include(FetchContent)
FetchContent_Declare(
SDL3
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
GIT_TAG release-3.4.4
GIT_SHALLOW TRUE
)
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
set(SDL_STATIC ON CACHE BOOL "" FORCE)
set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(SDL3)
message(STATUS "SDL3 compilado desde source para Emscripten")
else()
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
endif()
# --- SHADER COMPILATION (Linux/Windows only - macOS uses Metal) ---
if(NOT APPLE)
# --- SHADER COMPILATION (Linux/Windows only - macOS usa Metal, Emscripten no els necessita) ---
if(NOT APPLE AND NOT EMSCRIPTEN)
find_program(GLSLC_EXE NAMES glslc)
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/data/shaders")
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu")
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv")
set(SHADER_POSTFX_VERT_SRC "${SHADERS_DIR}/postfx.vert")
set(SHADER_POSTFX_FRAG_SRC "${SHADERS_DIR}/postfx.frag")
@@ -196,10 +226,15 @@ else()
endif()
# --- 2. AÑADIR EJECUTABLE ---
add_executable(${PROJECT_NAME} ${APP_SOURCES} ${RENDERING_SOURCES})
if(EMSCRIPTEN)
# En Emscripten no compilem sdl3gpu_shader (SDL3 GPU no està suportat a WebGL2)
add_executable(${PROJECT_NAME} ${APP_SOURCES})
else()
add_executable(${PROJECT_NAME} ${APP_SOURCES} ${RENDERING_SOURCES})
endif()
# Shaders deben compilarse antes que el ejecutable (Linux/Windows con glslc)
if(NOT APPLE AND GLSLC_EXE)
if(NOT APPLE AND NOT EMSCRIPTEN AND GLSLC_EXE)
add_dependencies(${PROJECT_NAME} shaders)
endif()
@@ -243,18 +278,39 @@ elseif(APPLE)
-rpath @executable_path/../Frameworks/
)
endif()
elseif(EMSCRIPTEN)
target_compile_definitions(${PROJECT_NAME} PRIVATE EMSCRIPTEN_BUILD)
# -fexceptions: habilita excepcions C++ (fkyaml, std::runtime_error...) — sense això qualsevol throw crida abort()
target_compile_options(${PROJECT_NAME} PRIVATE -fexceptions)
target_link_options(${PROJECT_NAME} PRIVATE
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/data@/data"
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/config@/config"
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt@/gamecontrollerdb.txt"
-fexceptions
-sALLOW_MEMORY_GROWTH=1
-sMAX_WEBGL_VERSION=2
-sINITIAL_MEMORY=67108864
-sASSERTIONS=1
# ASYNCIFY només per permetre emscripten_sleep(0) durant la precàrrega de recursos
# (el bucle principal del joc ja usa SDL3 Callback API, no depèn d'ASYNCIFY).
-sASYNCIFY=1
)
set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".html")
elseif(UNIX AND NOT APPLE)
target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD)
endif()
# Especificar la ubicación del ejecutable
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
# Especificar la ubicación del ejecutable (en desktop; a wasm queda a build/wasm/)
if(NOT EMSCRIPTEN)
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
endif()
# --- 5. STATIC ANALYSIS TARGETS ---
# Buscar herramientas de análisis estático
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
find_program(CLANG_FORMAT_EXE NAMES clang-format)
find_program(CPPCHECK_EXE NAMES cppcheck)
# Recopilar todos los archivos fuente para formateo
file(GLOB_RECURSE ALL_SOURCE_FILES
@@ -266,10 +322,11 @@ file(GLOB_RECURSE ALL_SOURCE_FILES
# Excluir directorio external del análisis
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX ".*/external/.*")
# Para clang-tidy, también excluir jail_audio.hpp
set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES})
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*jail_audio\\.hpp$")
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*_spv\\.h$")
# Para cppcheck, pasar solo .cpp (los headers se procesan transitivamente).
set(CPPCHECK_SOURCES ${ALL_SOURCE_FILES})
list(FILTER CPPCHECK_SOURCES INCLUDE REGEX ".*\\.cpp$")
# Targets de clang-tidy
if(CLANG_TIDY_EXE)
@@ -315,29 +372,55 @@ else()
message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles")
endif()
# --- 6. PACK RESOURCES TARGETS ---
set(PACK_TOOL_SOURCES
${CMAKE_SOURCE_DIR}/tools/pack_resources/pack_resources.cpp
${CMAKE_SOURCE_DIR}/source/core/resources/resource_pack.cpp
)
# Target de cppcheck
if(CPPCHECK_EXE)
add_custom_target(cppcheck
COMMAND ${CPPCHECK_EXE}
--enable=warning,style,performance,portability
--std=c++20
--language=c++
--inline-suppr
--suppress=missingIncludeSystem
--suppress=toomanyconfigs
--suppress=*:*/source/external/*
--suppress=*:*/source/core/rendering/sdl3gpu/spv/*
--quiet
-I ${CMAKE_SOURCE_DIR}/source
${CPPCHECK_SOURCES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Running cppcheck..."
)
else()
message(STATUS "cppcheck no encontrado - target 'cppcheck' no disponible")
endif()
add_executable(pack_tool ${PACK_TOOL_SOURCES})
target_include_directories(pack_tool PRIVATE ${CMAKE_SOURCE_DIR}/source)
set_target_properties(pack_tool PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/tools/pack_resources
)
# --- 6. EINA STANDALONE: pack_resources (no en Emscripten: s'utilitza --preload-file) ---
# Executable auxiliar que empaqueta `data/` a `resources.pack`.
# No es compila per defecte (EXCLUDE_FROM_ALL). Build explícit:
# cmake --build build --target pack_resources
if(NOT EMSCRIPTEN)
add_executable(pack_resources EXCLUDE_FROM_ALL
tools/pack_resources/pack_resources.cpp
source/core/resources/resource_pack.cpp
)
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
target_compile_options(pack_resources PRIVATE -Wall)
file(GLOB_RECURSE DATA_FILES "${CMAKE_SOURCE_DIR}/data/*")
# Regeneració automàtica de resources.pack en cada build si canvia data/.
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
set(RESOURCE_PACK "${CMAKE_SOURCE_DIR}/resources.pack")
add_custom_command(
OUTPUT "${CMAKE_SOURCE_DIR}/resources.pack"
COMMAND $<TARGET_FILE:pack_tool>
add_custom_command(
OUTPUT ${RESOURCE_PACK}
COMMAND $<TARGET_FILE:pack_resources>
"${CMAKE_SOURCE_DIR}/data"
"${CMAKE_SOURCE_DIR}/resources.pack"
DEPENDS pack_tool ${DATA_FILES}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Generando resources.pack desde data/..."
)
"${RESOURCE_PACK}"
DEPENDS pack_resources ${DATA_FILES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Empaquetant data/ → resources.pack"
VERBATIM
)
add_custom_target(pack DEPENDS "${CMAKE_SOURCE_DIR}/resources.pack")
add_dependencies(${PROJECT_NAME} pack)
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
add_dependencies(${PROJECT_NAME} resource_pack)
endif()

157
Makefile
View File

@@ -4,7 +4,6 @@
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
DIR_SOURCES := $(addsuffix /, $(DIR_ROOT)source)
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
DIR_TOOLS := $(addsuffix /, $(DIR_ROOT)tools)
# ==============================================================================
# TARGET NAMES
@@ -20,8 +19,6 @@ RESOURCE_FILE := release/windows/jdd.res
# ==============================================================================
# TOOLS
# ==============================================================================
DIR_PACK_TOOL := $(DIR_TOOLS)pack_resources
SHADER_SCRIPT := $(DIR_ROOT)tools/shaders/compile_spirv.sh
SHADER_CMAKE := $(DIR_ROOT)tools/shaders/compile_spirv.cmake
SHADERS_DIR := $(DIR_ROOT)data/shaders
HEADERS_DIR := $(DIR_ROOT)source/core/rendering/sdl3gpu
@@ -35,9 +32,23 @@ endif
# VERSION (extracted from defines.hpp)
# ==============================================================================
ifeq ($(OS),Windows_NT)
VERSION := v$(shell powershell -Command "(Select-String -Path 'source/utils/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value")
VERSION := $(shell powershell -Command "(Select-String -Path 'source/utils/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value")
else
VERSION := v$(shell grep 'constexpr const char\* VERSION' source/utils/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
VERSION := $(shell grep 'constexpr const char\* VERSION' source/utils/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
endif
# ==============================================================================
# GIT HASH (computat al host, passat a CMake via -DGIT_HASH)
# Evita que CMake haja de cridar git des de Docker/emscripten on falla per
# "dubious ownership" del volum muntat.
# ==============================================================================
ifeq ($(OS),Windows_NT)
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>NUL)
else
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>/dev/null)
endif
ifeq ($(GIT_HASH),)
GIT_HASH := unknown
endif
# ==============================================================================
@@ -54,9 +65,11 @@ endif
ifeq ($(OS),Windows_NT)
WIN_TARGET_FILE := $(DIR_BIN)$(APP_NAME)
WIN_RELEASE_FILE := $(RELEASE_FOLDER)/$(APP_NAME)
WIN_RELEASE_FILE_PS := $(subst ','',$(WIN_RELEASE_FILE))
else
WIN_TARGET_FILE := $(TARGET_FILE)
WIN_RELEASE_FILE := $(RELEASE_FILE)
WIN_RELEASE_FILE_PS := $(WIN_RELEASE_FILE)
endif
# ==============================================================================
@@ -82,15 +95,24 @@ else
UNAME_S := $(shell uname -s)
endif
# ==============================================================================
# CMAKE GENERATOR (Windows needs explicit MinGW Makefiles generator)
# ==============================================================================
ifeq ($(OS),Windows_NT)
CMAKE_GEN := -G "MinGW Makefiles"
else
CMAKE_GEN :=
endif
# ==============================================================================
# COMPILACIÓN CON CMAKE
# ==============================================================================
all:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build
debug:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH)
@cmake --build build
# ==============================================================================
@@ -98,12 +120,12 @@ debug:
# ==============================================================================
release:
ifeq ($(OS),Windows_NT)
@"$(MAKE)" windows_release
@"$(MAKE)" _windows_release
else
ifeq ($(UNAME_S),Darwin)
@$(MAKE) macos_release
@$(MAKE) _macos_release
else
@$(MAKE) linux_release
@$(MAKE) _linux_release
endif
endif
@@ -118,23 +140,22 @@ else
endif
# ==============================================================================
# REGLAS PARA HERRAMIENTA DE EMPAQUETADO Y RESOURCES.PACK
# EMPAQUETADO DE RECURSOS (build previ de l'eina + execució)
# ==============================================================================
pack_tool:
@$(MAKE) -C $(DIR_PACK_TOOL)
resources.pack: pack_tool
@$(MAKE) -C $(DIR_PACK_TOOL) pack
pack:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target pack_resources
@./build/pack_resources data resources.pack
# ==============================================================================
# COMPILACIÓN PARA WINDOWS (RELEASE)
# ==============================================================================
windows_release:
_windows_release:
@echo off
@echo Creando release para Windows - Version: $(VERSION)
# Compila con cmake (genera shaders, resources.pack y ejecutable)
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build
# Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER'
@@ -148,7 +169,7 @@ windows_release:
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item -Path '$(TARGET_FILE)' -Destination '\"$(WIN_RELEASE_FILE).exe\"'"
@powershell -Command "Copy-Item -Path '$(TARGET_FILE).exe' -Destination '$(WIN_RELEASE_FILE_PS).exe'"
strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
# Crea el fichero .zip
@@ -162,14 +183,14 @@ windows_release:
# ==============================================================================
# COMPILACIÓN PARA MACOS (RELEASE)
# ==============================================================================
macos_release:
_macos_release:
@echo "Creando release para macOS - Version: $(VERSION)"
# Verificar e instalar create-dmg si es necesario
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg)
# Compila la versión para procesadores Intel con cmake (genera shaders y resources.pack)
@cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON
@cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH)
@cmake --build build/intel
# Elimina datos de compilaciones anteriores
@@ -223,7 +244,7 @@ macos_release:
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"
# Compila la versión para procesadores Apple Silicon con cmake
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH)
@cmake --build build/arm
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
@@ -256,11 +277,11 @@ macos_release:
# ==============================================================================
# COMPILACIÓN PARA LINUX (RELEASE)
# ==============================================================================
linux_release:
_linux_release:
@echo "Creando release para Linux - Version: $(VERSION)"
# Compila con cmake (genera shaders, resources.pack y ejecutable)
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build
# Elimina carpeta temporal previa y la recrea (crea dist/ si no existe)
@@ -283,6 +304,76 @@ linux_release:
# Elimina la carpeta temporal
$(RMDIR) "$(RELEASE_FOLDER)"
# ==============================================================================
# COMPILACIÓN PARA WEBASSEMBLY (requiere Docker)
# ==============================================================================
wasm:
@echo "Compilando para WebAssembly - Version: $(VERSION)"
docker run --rm \
--user $(shell id -u):$(shell id -g) \
-v $(DIR_ROOT):/src \
-w /src \
emscripten/emsdk:latest \
bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm"
$(MKDIR) "$(DIST_DIR)/wasm"
cp build/wasm/$(TARGET_NAME).html $(DIST_DIR)/wasm/
cp build/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/
cp build/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/
cp build/wasm/$(TARGET_NAME).data $(DIST_DIR)/wasm/
@echo "Output: $(DIST_DIR)/wasm/"
scp $(DIST_DIR)/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/$(TARGET_NAME).data \
maverick:/home/sergio/gitea/web_jailgames/static/games/jaildoctors-dilemma/wasm/
ssh maverick 'cd /home/sergio/gitea/web_jailgames && ./deploy.sh'
@echo "Deployed to maverick"
# Versió Debug del build wasm: arrenca directament a la GAME (sense logo/loading/title)
# i activa l'editor i la consola. Sortida a dist/wasm_debug/.
wasm_debug:
@echo "Compilando WebAssembly Debug - Version: $(VERSION)"
docker run --rm \
-v $(DIR_ROOT):/src \
-w /src \
emscripten/emsdk:latest \
bash -c "emcmake cmake -S . -B build/wasm_debug -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm_debug"
$(MKDIR) "$(DIST_DIR)/wasm_debug"
cp build/wasm_debug/$(TARGET_NAME).html $(DIST_DIR)/wasm_debug/
cp build/wasm_debug/$(TARGET_NAME).js $(DIST_DIR)/wasm_debug/
cp build/wasm_debug/$(TARGET_NAME).wasm $(DIST_DIR)/wasm_debug/
cp build/wasm_debug/$(TARGET_NAME).data $(DIST_DIR)/wasm_debug/
@echo "Output: $(DIST_DIR)/wasm_debug/"
# ==============================================================================
# ==============================================================================
# CODE QUALITY (delegados a cmake)
# ==============================================================================
format:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target format
format-check:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target format-check
tidy:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target tidy
tidy-fix:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target tidy-fix
cppcheck:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target cppcheck
# DESCARGA DE GAMECONTROLLERDB
# ==============================================================================
controllerdb:
@echo "Descargando gamecontrollerdb.txt..."
curl -fsSL https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt \
-o gamecontrollerdb.txt
@echo "gamecontrollerdb.txt actualizado"
# ==============================================================================
# REGLAS ESPECIALES
# ==============================================================================
@@ -301,17 +392,23 @@ help:
@echo ""
@echo " Release:"
@echo " make release - Crear release (detecta SO automaticamente)"
@echo " make windows_release - Crear release para Windows"
@echo " make linux_release - Crear release para Linux"
@echo " make macos_release - Crear release para macOS"
@echo " make wasm - Crear release per a WebAssembly (requereix Docker)"
@echo " make wasm_debug - Crear build Debug per a WebAssembly (entra directe a la GAME)"
@echo ""
@echo " Herramientas:"
@echo " make compile_shaders - Compilar shaders SPIR-V"
@echo " make pack_tool - Compilar herramienta de empaquetado"
@echo " make resources.pack - Generar pack de recursos desde data/"
@echo " make pack - Empaquetar recursos a resources.pack"
@echo " make controllerdb - Descargar gamecontrollerdb.txt actualizado"
@echo ""
@echo " Calidad de codigo:"
@echo " make format - Formatear codigo con clang-format"
@echo " make format-check - Verificar formato sin modificar"
@echo " make tidy - Analisis estatico con clang-tidy"
@echo " make tidy-fix - Analisis estatico con auto-fix"
@echo " make cppcheck - Analisis estatico con cppcheck"
@echo ""
@echo " Otros:"
@echo " make show_version - Mostrar version actual ($(VERSION))"
@echo " make help - Mostrar esta ayuda"
.PHONY: all debug release windows_release macos_release linux_release compile_shaders pack_tool resources.pack show_version help
.PHONY: all debug release _windows_release _macos_release _linux_release wasm wasm_debug compile_shaders pack controllerdb format format-check tidy tidy-fix cppcheck show_version help

View File

@@ -201,7 +201,7 @@ categories:
DEBUG: [MODE, START]
DEBUG MODE: [ON, OFF]
DEBUG START: [HERE, ROOM, POS, SCENE]
DEBUG START SCENE: [LOGO, LOADING, TITLE, CREDITS, GAME, ENDING, ENDING2]
DEBUG START SCENE: [LOGO, LOADING, TITLE, CREDITS, GAME, DEMO, ENDING, ENDING2]
- keyword: ITEMS
handler: cmd_items
@@ -220,9 +220,9 @@ categories:
- keyword: SCENE
handler: cmd_scene
description: Change scene
usage: "SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART]"
usage: "SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|DEMO|ENDING|ENDING2|RESTART]"
completions:
SCENE: [LOGO, LOADING, TITLE, CREDITS, GAME, ENDING, ENDING2, RESTART]
SCENE: [LOGO, LOADING, TITLE, CREDITS, GAME, DEMO, ENDING, ENDING2, RESTART]
- keyword: EDIT
handler: cmd_edit

View File

@@ -8,6 +8,7 @@ title:
keyboard: "2. REDEFINIR TECLES"
joystick: "3. REDEFINIR MANDO"
projects: "4. PROJECTES"
press_to_play: "PREM PER JUGAR"
keys:
prompt0: "PREM UNA TECLA PER A ESQUERRA"
prompt1: "PREM UNA TECLA PER A DRETA"
@@ -103,6 +104,8 @@ achievements:
ui:
press_again_menu: "PREM DE NOU PER TORNAR AL MENÚ"
press_again_exit: "PREM DE NOU PER EIXIR"
gamepad_connected: "CONNECTAT"
gamepad_disconnected: "DESCONNECTAT"
border_enabled: "VORA ACTIVADA"
border_disabled: "VORA DESACTIVADA"
fullscreen_enabled: "PANTALLA COMPLETA ACTIVADA"

View File

@@ -8,6 +8,7 @@ title:
keyboard: "2. REDEFINE KEYBOARD"
joystick: "3. REDEFINE JOYSTICK"
projects: "4. PROJECTS"
press_to_play: "PRESS TO PLAY"
keys:
prompt0: "PRESS KEY FOR LEFT"
prompt1: "PRESS KEY FOR RIGHT"
@@ -103,6 +104,8 @@ achievements:
ui:
press_again_menu: "PRESS AGAIN TO RETURN TO MENU"
press_again_exit: "PRESS AGAIN TO EXIT"
gamepad_connected: "CONNECTED"
gamepad_disconnected: "DISCONNECTED"
border_enabled: "BORDER ENABLED"
border_disabled: "BORDER DISABLED"
fullscreen_enabled: "FULLSCREEN ENABLED"

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,27 @@
#include "audio.hpp"
#include "core/audio/audio.hpp"
#include <SDL3/SDL.h> // Para SDL_LogInfo, SDL_LogCategory, SDL_G...
#include <SDL3/SDL.h> // Para SDL_GetError, SDL_Init
#include <algorithm> // Para clamp
#include <iostream> // Para std::cout
// 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
#undef STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.h"
#include "external/stb_vorbis.c"
// stb_vorbis.c filtra les macros L, C i R (i PLAYBACK_*) al TU. Les netegem
// perquè xocarien amb noms de paràmetres de plantilla en altres headers.
#undef L
#undef C
#undef R
#undef PLAYBACK_MONO
#undef PLAYBACK_LEFT
#undef PLAYBACK_RIGHT
// clang-format on
#include "core/audio/jail_audio.hpp" // Para JA_FadeOutMusic, JA_Init, JA_PauseM...
#include "core/resources/resource_cache.hpp" // Para Resource
#include "game/options.hpp" // Para AudioOptions, audio, MusicOptions
#include "core/audio/audio_adapter.hpp" // Para AudioResource::getMusic/getSound
#include "core/audio/jail_audio.hpp" // Para JA_*
#include "game/options.hpp" // Para Options::audio
// Singleton
Audio* Audio::instance = nullptr;
@@ -22,7 +30,10 @@ Audio* Audio::instance = nullptr;
void Audio::init() { Audio::instance = new Audio(); }
// Libera la instancia
void Audio::destroy() { delete Audio::instance; }
void Audio::destroy() {
delete Audio::instance;
Audio::instance = nullptr;
}
// Obtiene la instancia
auto Audio::get() -> Audio* { return Audio::instance; }
@@ -38,10 +49,15 @@ Audio::~Audio() {
// Método principal
void Audio::update() {
JA_Update();
// Sincronizar estado: detectar cuando la música se para (ej. fade-out completado)
if (instance && instance->music_.state == MusicState::PLAYING && JA_GetMusicState() != JA_MUSIC_PLAYING) {
instance->music_.state = MusicState::STOPPED;
}
}
// Reproduce la música
void Audio::playMusic(const std::string& name, const int loop) { // NOLINT(readability-convert-member-functions-to-static)
// Reproduce la música por nombre (con crossfade opcional)
void Audio::playMusic(const std::string& name, const int loop, const int crossfade_ms) {
bool new_loop = (loop != 0);
// Si ya está sonando exactamente la misma pista y mismo modo loop, no hacemos nada
@@ -49,29 +65,45 @@ void Audio::playMusic(const std::string& name, const int loop) { // NOLINT(read
return;
}
// Intentar obtener recurso; si falla, no tocar estado
auto* resource = Resource::Cache::get()->getMusic(name);
if (resource == nullptr) {
// manejo de error opcional
return;
if (!music_enabled_) return;
auto* resource = AudioResource::getMusic(name);
if (resource == nullptr) return;
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
JA_CrossfadeMusic(resource, crossfade_ms, loop);
} else {
if (music_.state == MusicState::PLAYING) {
JA_StopMusic();
}
JA_PlayMusic(resource, loop);
}
// Si hay algo reproduciéndose, detenerlo primero (si el backend lo requiere)
if (music_.state == MusicState::PLAYING) {
JA_StopMusic(); // sustituir por la función de stop real del API si tiene otro nombre
}
// Llamada al motor para reproducir la nueva pista
JA_PlayMusic(resource, loop);
// Actualizar estado y metadatos después de iniciar con éxito
music_.name = name;
music_.loop = new_loop;
music_.state = MusicState::PLAYING;
}
// Reproduce la música por puntero (con crossfade opcional)
void Audio::playMusic(JA_Music_t* music, const int loop, const int crossfade_ms) {
if (!music_enabled_ || music == nullptr) return;
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
JA_CrossfadeMusic(music, crossfade_ms, loop);
} else {
if (music_.state == MusicState::PLAYING) {
JA_StopMusic();
}
JA_PlayMusic(music, loop);
}
music_.name.clear(); // nom desconegut quan es passa per punter
music_.loop = (loop != 0);
music_.state = MusicState::PLAYING;
}
// Pausa la música
void Audio::pauseMusic() { // NOLINT(readability-convert-member-functions-to-static)
void Audio::pauseMusic() {
if (music_enabled_ && music_.state == MusicState::PLAYING) {
JA_PauseMusic();
music_.state = MusicState::PAUSED;
@@ -79,7 +111,7 @@ void Audio::pauseMusic() { // NOLINT(readability-convert-member-functions-to-st
}
// Continua la música pausada
void Audio::resumeMusic() { // NOLINT(readability-convert-member-functions-to-static)
void Audio::resumeMusic() {
if (music_enabled_ && music_.state == MusicState::PAUSED) {
JA_ResumeMusic();
music_.state = MusicState::PLAYING;
@@ -87,7 +119,7 @@ void Audio::resumeMusic() { // NOLINT(readability-convert-member-functions-to-s
}
// Detiene la música
void Audio::stopMusic() { // NOLINT(readability-make-member-function-const)
void Audio::stopMusic() {
if (music_enabled_) {
JA_StopMusic();
music_.state = MusicState::STOPPED;
@@ -97,13 +129,13 @@ void Audio::stopMusic() { // NOLINT(readability-make-member-function-const)
// Reproduce un sonido por nombre
void Audio::playSound(const std::string& name, Group group) const {
if (sound_enabled_) {
JA_PlaySound(Resource::Cache::get()->getSound(name), 0, static_cast<int>(group));
JA_PlaySound(AudioResource::getSound(name), 0, static_cast<int>(group));
}
}
// Reproduce un sonido por puntero directo
void Audio::playSound(JA_Sound_t* sound, Group group) const {
if (sound_enabled_) {
if (sound_enabled_ && sound != nullptr) {
JA_PlaySound(sound, 0, static_cast<int>(group));
}
}
@@ -138,7 +170,7 @@ auto Audio::getRealMusicState() -> MusicState {
}
}
// Establece el volumen de los sonidos
// Establece el volumen de los sonidos (float 0.0..1.0)
void Audio::setSoundVolume(float sound_volume, Group group) const {
if (sound_enabled_) {
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
@@ -147,7 +179,7 @@ void Audio::setSoundVolume(float sound_volume, Group group) const {
}
}
// Establece el volumen de la música
// Establece el volumen de la música (float 0.0..1.0)
void Audio::setMusicVolume(float music_volume) const {
if (music_enabled_) {
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
@@ -172,24 +204,9 @@ void Audio::enable(bool value) {
// Inicializa SDL Audio
void Audio::initSDLAudio() {
if (!SDL_Init(SDL_INIT_AUDIO)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_AUDIO could not initialize! SDL Error: %s", SDL_GetError());
std::cout << "SDL_AUDIO could not initialize! SDL Error: " << SDL_GetError() << '\n';
} else {
JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2);
enable(Options::audio.enabled);
// Aplicar estado de música y sonido guardado en las opciones.
// enable() ya aplica los volúmenes, pero no toca music_enabled_/sound_enabled_.
// Si alguno está desactivado, hay que forzar el volumen a 0 en el backend.
if (!Options::audio.music.enabled) {
setMusicVolume(0.0F); // music_enabled_=true aún → llega a JA
enableMusic(false);
}
if (!Options::audio.sound.enabled) {
setSoundVolume(0.0F); // sound_enabled_=true aún → llega a JA
enableSound(false);
}
std::cout << "\n** AUDIO SYSTEM **\n";
std::cout << "Audio system initialized successfully\n";
}
}
}

View File

@@ -1,28 +1,35 @@
#pragma once
#include <cstdint> // Para int8_t, uint8_t
#include <string> // Para string
#include <utility> // Para move
// --- Clase Audio: gestor de audio (singleton) ---
// Implementació canònica, byte-idèntica entre projectes.
// Els volums es manegen internament com a float 0.01.0; la capa de
// presentació (menús, notificacions) usa les helpers toPercent/fromPercent
// per mostrar 0100 a l'usuari.
class Audio {
public:
// --- Enums ---
enum class Group : int {
enum class Group : std::int8_t {
ALL = -1, // Todos los grupos
GAME = 0, // Sonidos del juego
INTERFACE = 1 // Sonidos de la interfaz
};
enum class MusicState {
enum class MusicState : std::uint8_t {
PLAYING, // Reproduciendo música
PAUSED, // Música pausada
STOPPED, // Música detenida
};
// --- Constantes ---
static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo
static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo
static constexpr int FREQUENCY = 48000; // Frecuencia de audio
static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo (float 0..1)
static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo (float 0..1)
static constexpr float VOLUME_STEP = 0.05F; // Pas estàndard per a UI (5%)
static constexpr int FREQUENCY = 48000; // Frecuencia de audio
static constexpr int DEFAULT_CROSSFADE_MS = 1500; // Duració del crossfade per defecte (ms)
// --- Singleton ---
static void init(); // Inicializa el objeto Audio
@@ -34,21 +41,31 @@ class Audio {
static void update(); // Actualización del sistema de audio
// --- Control de música ---
void playMusic(const std::string& name, int loop = -1); // Reproducir música en bucle
void pauseMusic(); // Pausar reproducción de música
void resumeMusic(); // Continua la música pausada
void stopMusic(); // Detener completamente la música
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional)
void playMusic(struct JA_Music_t* music, int loop = -1, int crossfade_ms = 0); // Reproducir música por puntero (con crossfade opcional)
void pauseMusic(); // Pausar reproducción de música
void resumeMusic(); // Continua la música pausada
void stopMusic(); // Detener completamente la música
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
// --- Control de sonidos ---
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
void playSound(struct JA_Sound_t* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
void stopAllSounds() const; // Detener todos los sonidos
// --- Control de volumen ---
// --- Control de volumen (API interna: float 0.0..1.0) ---
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
void setMusicVolume(float volume) const; // Ajustar volumen de música
// --- Helpers de conversió per a la capa de presentació ---
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
static constexpr auto toPercent(float volume) -> int {
return static_cast<int>(volume * 100.0F + 0.5F);
}
static constexpr auto fromPercent(int percent) -> float {
return static_cast<float>(percent) / 100.0F;
}
// --- Configuración general ---
void enable(bool value); // Establecer estado general
void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general
@@ -94,4 +111,4 @@ class Audio {
bool enabled_{true}; // Estado general del audio
bool sound_enabled_{true}; // Estado de los efectos de sonido
bool music_enabled_{true}; // Estado de la música
};
};

View File

@@ -0,0 +1,13 @@
#include "core/audio/audio_adapter.hpp"
#include "core/resources/resource_cache.hpp"
namespace AudioResource {
JA_Music_t* getMusic(const std::string& name) {
return Resource::Cache::get()->getMusic(name);
}
JA_Sound_t* getSound(const std::string& name) {
return Resource::Cache::get()->getSound(name);
}
} // namespace AudioResource

View File

@@ -0,0 +1,17 @@
#pragma once
// --- Audio Resource Adapter ---
// Aquest fitxer exposa una interfície comuna a Audio per obtenir JA_Music_t* /
// JA_Sound_t* per nom. Cada projecte la implementa en audio_adapter.cpp
// delegant al seu singleton de recursos (Resource::get(), Resource::Cache::get(),
// etc.). Això permet que audio.hpp/audio.cpp siguin idèntics entre projectes.
#include <string> // Para string
struct JA_Music_t;
struct JA_Sound_t;
namespace AudioResource {
JA_Music_t* getMusic(const std::string& name);
JA_Sound_t* getSound(const std::string& name);
} // namespace AudioResource

View File

@@ -3,24 +3,41 @@
// --- Includes ---
#include <SDL3/SDL.h>
#include <stdint.h> // Para uint32_t, uint8_t
#include <stdio.h> // Para NULL, fseek, printf, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET
#include <stdio.h> // Para NULL, fseek, fclose, fopen, fread, ftell, FILE, SEEK_END, SEEK_SET
#include <stdlib.h> // Para free, malloc
#include <string.h> // Para strcpy, strlen
#include <iostream> // Para std::cout
#include <memory> // Para std::unique_ptr
#include <string> // Para std::string
#include <vector> // Para std::vector
#define STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.h" // Para stb_vorbis_decode_memory
#include "external/stb_vorbis.c" // Para stb_vorbis_open_memory i streaming
// Deleter stateless per a buffers reservats amb `SDL_malloc` / `SDL_LoadWAV*`.
// Compatible amb `std::unique_ptr<Uint8[], SDLFreeDeleter>` — zero size
// overhead gràcies a EBO, igual que un unique_ptr amb default_delete.
struct SDLFreeDeleter {
void operator()(Uint8* p) const noexcept {
if (p) SDL_free(p);
}
};
// --- Public Enums ---
enum JA_Channel_state { JA_CHANNEL_INVALID,
enum JA_Channel_state {
JA_CHANNEL_INVALID,
JA_CHANNEL_FREE,
JA_CHANNEL_PLAYING,
JA_CHANNEL_PAUSED,
JA_SOUND_DISABLED };
enum JA_Music_state { JA_MUSIC_INVALID,
JA_SOUND_DISABLED,
};
enum JA_Music_state {
JA_MUSIC_INVALID,
JA_MUSIC_PLAYING,
JA_MUSIC_PAUSED,
JA_MUSIC_STOPPED,
JA_MUSIC_DISABLED };
JA_MUSIC_DISABLED,
};
// --- Struct Definitions ---
#define JA_MAX_SIMULTANEOUS_CHANNELS 20
@@ -29,7 +46,9 @@ enum JA_Music_state { JA_MUSIC_INVALID,
struct JA_Sound_t {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
Uint32 length{0};
Uint8* buffer{NULL};
// Buffer descomprimit (PCM) propietat del sound. Reservat per SDL_LoadWAV
// via SDL_malloc; el deleter `SDLFreeDeleter` allibera amb SDL_free.
std::unique_ptr<Uint8[], SDLFreeDeleter> buffer;
};
struct JA_Channel_t {
@@ -43,18 +62,23 @@ struct JA_Channel_t {
struct JA_Music_t {
SDL_AudioSpec spec{SDL_AUDIO_S16, 2, 48000};
Uint32 length{0};
Uint8* buffer{nullptr};
char* filename{nullptr};
int pos{0};
int times{0};
// OGG comprimit en memòria. Propietat nostra; es copia des del buffer
// d'entrada una sola vegada en JA_LoadMusic i es descomprimix en chunks
// per streaming. Com que stb_vorbis guarda un punter persistent al
// `.data()` d'aquest vector, no el podem resize'jar un cop establert
// (una reallocation invalidaria el punter que el decoder conserva).
std::vector<Uint8> ogg_data;
stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del JA_Music_t
std::string filename;
int times{0}; // loops restants (-1 = infinit, 0 = un sol play)
SDL_AudioStream* stream{nullptr};
JA_Music_state state{JA_MUSIC_INVALID};
};
// --- Internal Global State ---
// Marcado 'inline' (C++17) para asegurar una única instancia.
// --- Internal Global State (inline, C++17) ---
inline JA_Music_t* current_music{nullptr};
inline JA_Channel_t channels[JA_MAX_SIMULTANEOUS_CHANNELS];
@@ -66,49 +90,142 @@ inline bool JA_musicEnabled{true};
inline bool JA_soundEnabled{true};
inline SDL_AudioDeviceID sdlAudioDevice{0};
inline bool fading{false};
inline int fade_start_time{0};
inline int fade_duration{0};
inline float fade_initial_volume{0.0f}; // Corregido de 'int' a 'float'
// --- Crossfade / Fade State ---
struct JA_FadeState {
bool active{false};
Uint64 start_time{0};
int duration_ms{0};
float initial_volume{0.0f};
};
struct JA_OutgoingMusic {
SDL_AudioStream* stream{nullptr};
JA_FadeState fade;
};
inline JA_OutgoingMusic outgoing_music;
inline JA_FadeState incoming_fade;
// --- Forward Declarations ---
inline void JA_StopMusic();
inline void JA_StopChannel(const int channel);
inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int loop = 0, const int group = 0);
inline void JA_CrossfadeMusic(JA_Music_t* music, int crossfade_ms, int loop = -1);
// --- Music streaming internals ---
// Bytes-per-sample per canal (sempre s16)
static constexpr int JA_MUSIC_BYTES_PER_SAMPLE = 2;
// Quants shorts decodifiquem per crida a get_samples_short_interleaved.
// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz.
static constexpr int JA_MUSIC_CHUNK_SHORTS = 8192;
// Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
static constexpr float JA_MUSIC_LOW_WATER_SECONDS = 0.5f;
// Decodifica un chunk del vorbis i el volca a l'stream. Retorna samples
// decodificats per canal (0 = EOF de l'stream vorbis).
inline int JA_FeedMusicChunk(JA_Music_t* music) {
if (!music || !music->vorbis || !music->stream) return 0;
short chunk[JA_MUSIC_CHUNK_SHORTS];
const int num_channels = music->spec.channels;
const int samples_per_channel = stb_vorbis_get_samples_short_interleaved(
music->vorbis,
num_channels,
chunk,
JA_MUSIC_CHUNK_SHORTS);
if (samples_per_channel <= 0) return 0;
const int bytes = samples_per_channel * num_channels * JA_MUSIC_BYTES_PER_SAMPLE;
SDL_PutAudioStreamData(music->stream, chunk, bytes);
return samples_per_channel;
}
// Reompli l'stream fins que tinga ≥ JA_MUSIC_LOW_WATER_SECONDS bufferats.
// En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar.
inline void JA_PumpMusic(JA_Music_t* music) {
if (!music || !music->vorbis || !music->stream) return;
const int bytes_per_second = music->spec.freq * music->spec.channels * JA_MUSIC_BYTES_PER_SAMPLE;
const int low_water_bytes = static_cast<int>(JA_MUSIC_LOW_WATER_SECONDS * static_cast<float>(bytes_per_second));
while (SDL_GetAudioStreamAvailable(music->stream) < low_water_bytes) {
const int decoded = JA_FeedMusicChunk(music);
if (decoded > 0) continue;
// EOF: si queden loops, rebobinar; si no, tallar i deixar drenar.
if (music->times != 0) {
stb_vorbis_seek_start(music->vorbis);
if (music->times > 0) music->times--;
} else {
break;
}
}
}
// Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream
// siga robat per outgoing_music (crossfade o fade-out). Imprescindible amb
// streaming: l'stream robat no es pot re-alimentar perquè perd la referència
// al seu vorbis decoder. No aplica loop — si el vorbis s'esgota abans, parem.
inline void JA_PreFillOutgoing(JA_Music_t* music, int duration_ms) {
if (!music || !music->vorbis || !music->stream) return;
const int bytes_per_second = music->spec.freq * music->spec.channels * JA_MUSIC_BYTES_PER_SAMPLE;
const int needed_bytes = static_cast<int>((static_cast<int64_t>(duration_ms) * bytes_per_second) / 1000);
while (SDL_GetAudioStreamAvailable(music->stream) < needed_bytes) {
const int decoded = JA_FeedMusicChunk(music);
if (decoded <= 0) break; // EOF: deixem drenar el que hi haja
}
}
// --- Core Functions ---
inline void JA_Update() {
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) {
if (fading) {
int time = SDL_GetTicks();
if (time > (fade_start_time + fade_duration)) {
fading = false;
JA_StopMusic();
return;
} else {
const int time_passed = time - fade_start_time;
const float percent = (float)time_passed / (float)fade_duration;
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * (1.0 - percent));
}
}
if (current_music->times != 0) {
if ((Uint32)SDL_GetAudioStreamAvailable(current_music->stream) < (current_music->length / 2)) {
SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length);
}
if (current_music->times > 0) current_music->times--;
// --- Outgoing music fade-out (crossfade o fade-out a silencio) ---
if (outgoing_music.stream && outgoing_music.fade.active) {
Uint64 now = SDL_GetTicks();
Uint64 elapsed = now - outgoing_music.fade.start_time;
if (elapsed >= (Uint64)outgoing_music.fade.duration_ms) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
outgoing_music.fade.active = false;
} else {
if (SDL_GetAudioStreamAvailable(current_music->stream) == 0) JA_StopMusic();
float percent = (float)elapsed / (float)outgoing_music.fade.duration_ms;
SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0f - percent));
}
}
// --- Current music ---
if (JA_musicEnabled && current_music && current_music->state == JA_MUSIC_PLAYING) {
// Fade-in (parte de un crossfade)
if (incoming_fade.active) {
Uint64 now = SDL_GetTicks();
Uint64 elapsed = now - incoming_fade.start_time;
if (elapsed >= (Uint64)incoming_fade.duration_ms) {
incoming_fade.active = false;
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
} else {
float percent = (float)elapsed / (float)incoming_fade.duration_ms;
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume * percent);
}
}
// Streaming: rellenem l'stream fins al low-water-mark i parem si el
// vorbis s'ha esgotat i no queden loops.
JA_PumpMusic(current_music);
if (current_music->times == 0 && SDL_GetAudioStreamAvailable(current_music->stream) == 0) {
JA_StopMusic();
}
}
// --- Sound channels ---
if (JA_soundEnabled) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i)
if (channels[i].state == JA_CHANNEL_PLAYING) {
if (channels[i].times != 0) {
if ((Uint32)SDL_GetAudioStreamAvailable(channels[i].stream) < (channels[i].sound->length / 2)) {
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer, channels[i].sound->length);
SDL_PutAudioStreamData(channels[i].stream, channels[i].sound->buffer.get(), channels[i].sound->length);
if (channels[i].times > 0) channels[i].times--;
}
} else {
@@ -119,69 +236,85 @@ inline void JA_Update() {
}
inline void JA_Init(const int freq, const SDL_AudioFormat format, const int num_channels) {
#ifdef _DEBUG
SDL_SetLogPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_DEBUG);
#endif
JA_audioSpec = {format, num_channels, freq};
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
sdlAudioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &JA_audioSpec);
if (sdlAudioDevice == 0) SDL_Log("Failed to initialize SDL audio!");
if (sdlAudioDevice == 0) std::cout << "Failed to initialize SDL audio!" << '\n';
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; ++i) channels[i].state = JA_CHANNEL_FREE;
for (int i = 0; i < JA_MAX_GROUPS; ++i) JA_soundVolume[i] = 0.5f;
}
inline void JA_Quit() {
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice); // Corregido: !sdlAudioDevice -> sdlAudioDevice
if (outgoing_music.stream) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
}
if (sdlAudioDevice) SDL_CloseAudioDevice(sdlAudioDevice);
sdlAudioDevice = 0;
}
// --- Music Functions ---
inline JA_Music_t* JA_LoadMusic(const Uint8* buffer, Uint32 length) {
JA_Music_t* music = new JA_Music_t();
if (!buffer || length == 0) return nullptr;
int chan, samplerate;
short* output;
music->length = stb_vorbis_decode_memory(buffer, length, &chan, &samplerate, &output) * chan * 2;
// Allocem el JA_Music_t primer per aprofitar el seu `std::vector<Uint8>`
// com a propietari del OGG comprimit. stb_vorbis guarda un punter
// persistent al buffer; com que ací no el resize'jem, el .data() és
// estable durant tot el cicle de vida del music.
auto* music = new JA_Music_t();
music->ogg_data.assign(buffer, buffer + length);
music->spec.channels = chan;
music->spec.freq = samplerate;
int error = 0;
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
static_cast<int>(length),
&error,
nullptr);
if (!music->vorbis) {
std::cout << "JA_LoadMusic: stb_vorbis_open_memory failed (error " << error << ")" << '\n';
delete music;
return nullptr;
}
const stb_vorbis_info info = stb_vorbis_get_info(music->vorbis);
music->spec.channels = info.channels;
music->spec.freq = static_cast<int>(info.sample_rate);
music->spec.format = SDL_AUDIO_S16;
music->buffer = static_cast<Uint8*>(SDL_malloc(music->length));
SDL_memcpy(music->buffer, output, music->length);
free(output);
music->pos = 0;
music->state = JA_MUSIC_STOPPED;
return music;
}
// Overload amb filename — els callers l'usen per poder comparar la música
// en curs amb JA_GetMusicFilename() i no rearrancar-la si ja és la mateixa.
inline JA_Music_t* JA_LoadMusic(Uint8* buffer, Uint32 length, const char* filename) {
JA_Music_t* music = JA_LoadMusic(static_cast<const Uint8*>(buffer), length);
if (music && filename) music->filename = filename;
return music;
}
inline JA_Music_t* JA_LoadMusic(const char* filename) {
// [RZC 28/08/22] Carreguem primer el arxiu en memòria i després el descomprimim. Es algo més rapid.
// Carreguem primer el arxiu en memòria i després el descomprimim.
FILE* f = fopen(filename, "rb");
if (!f) return NULL; // Añadida comprobación de apertura
if (!f) return nullptr;
fseek(f, 0, SEEK_END);
long fsize = ftell(f);
fseek(f, 0, SEEK_SET);
auto* buffer = static_cast<Uint8*>(malloc(fsize + 1));
if (!buffer) { // Añadida comprobación de malloc
if (!buffer) {
fclose(f);
return NULL;
return nullptr;
}
if (fread(buffer, fsize, 1, f) != 1) {
fclose(f);
free(buffer);
return NULL;
return nullptr;
}
fclose(f);
JA_Music_t* music = JA_LoadMusic(buffer, fsize);
if (music) { // Comprobar que JA_LoadMusic tuvo éxito
music->filename = static_cast<char*>(malloc(strlen(filename) + 1));
if (music->filename) {
strcpy(music->filename, filename);
}
JA_Music_t* music = JA_LoadMusic(static_cast<const Uint8*>(buffer), static_cast<Uint32>(fsize));
if (music) {
music->filename = filename;
}
free(buffer);
@@ -190,35 +323,43 @@ inline JA_Music_t* JA_LoadMusic(const char* filename) {
}
inline void JA_PlayMusic(JA_Music_t* music, const int loop = -1) {
if (!JA_musicEnabled || !music) return; // Añadida comprobación de music
if (!JA_musicEnabled || !music || !music->vorbis) return;
JA_StopMusic();
current_music = music;
current_music->pos = 0;
current_music->state = JA_MUSIC_PLAYING;
current_music->times = loop;
// Rebobinem l'stream de vorbis al principi. Cobreix tant play-per-primera-
// vegada com replays/canvis de track que tornen a la mateixa pista.
stb_vorbis_seek_start(current_music->vorbis);
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &JA_audioSpec);
if (!current_music->stream) { // Comprobar creación de stream
SDL_Log("Failed to create audio stream!");
if (!current_music->stream) {
std::cout << "Failed to create audio stream!" << '\n';
current_music->state = JA_MUSIC_STOPPED;
return;
}
if (!SDL_PutAudioStreamData(current_music->stream, current_music->buffer, current_music->length)) printf("[ERROR] SDL_PutAudioStreamData failed!\n");
SDL_SetAudioStreamGain(current_music->stream, JA_musicVolume);
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) printf("[ERROR] SDL_BindAudioStream failed!\n");
// Pre-cargem el buffer abans de bindejar per evitar un underrun inicial.
JA_PumpMusic(current_music);
if (!SDL_BindAudioStream(sdlAudioDevice, current_music->stream)) {
std::cout << "[ERROR] SDL_BindAudioStream failed!" << '\n';
}
}
inline char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) {
inline const char* JA_GetMusicFilename(const JA_Music_t* music = nullptr) {
if (!music) music = current_music;
if (!music) return nullptr; // Añadida comprobación
return music->filename;
if (!music || music->filename.empty()) return nullptr;
return music->filename.c_str();
}
inline void JA_PauseMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return; // Comprobación mejorada
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return;
current_music->state = JA_MUSIC_PAUSED;
SDL_UnbindAudioStream(current_music->stream);
@@ -226,32 +367,100 @@ inline void JA_PauseMusic() {
inline void JA_ResumeMusic() {
if (!JA_musicEnabled) return;
if (!current_music || current_music->state != JA_MUSIC_PAUSED) return; // Comprobación mejorada
if (!current_music || current_music->state != JA_MUSIC_PAUSED) return;
current_music->state = JA_MUSIC_PLAYING;
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
}
inline void JA_StopMusic() {
// Limpiar outgoing crossfade si existe
if (outgoing_music.stream) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
outgoing_music.fade.active = false;
}
incoming_fade.active = false;
if (!current_music || current_music->state == JA_MUSIC_INVALID || current_music->state == JA_MUSIC_STOPPED) return;
current_music->pos = 0;
current_music->state = JA_MUSIC_STOPPED;
if (current_music->stream) {
SDL_DestroyAudioStream(current_music->stream);
current_music->stream = nullptr;
}
// No liberamos filename aquí, se debería liberar en JA_DeleteMusic
// Deixem el handle de vorbis viu — es tanca en JA_DeleteMusic.
// Rebobinem perquè un futur JA_PlayMusic comence des del principi.
if (current_music->vorbis) {
stb_vorbis_seek_start(current_music->vorbis);
}
}
inline void JA_FadeOutMusic(const int milliseconds) {
if (!JA_musicEnabled) return;
if (current_music == NULL || current_music->state == JA_MUSIC_INVALID) return;
if (!current_music || current_music->state != JA_MUSIC_PLAYING) return;
fading = true;
fade_start_time = SDL_GetTicks();
fade_duration = milliseconds;
fade_initial_volume = JA_musicVolume;
// Destruir outgoing anterior si existe
if (outgoing_music.stream) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
}
// Pre-omplim l'stream amb `milliseconds` de so: un cop robat, ja no
// tindrà accés al vorbis decoder i només podrà drenar el que tinga.
JA_PreFillOutgoing(current_music, milliseconds);
// Robar el stream del current_music al outgoing
outgoing_music.stream = current_music->stream;
outgoing_music.fade = {true, SDL_GetTicks(), milliseconds, JA_musicVolume};
// Dejar current_music sin stream (ya lo tiene outgoing)
current_music->stream = nullptr;
current_music->state = JA_MUSIC_STOPPED;
if (current_music->vorbis) stb_vorbis_seek_start(current_music->vorbis);
incoming_fade.active = false;
}
inline void JA_CrossfadeMusic(JA_Music_t* music, const int crossfade_ms, const int loop) {
if (!JA_musicEnabled || !music || !music->vorbis) return;
// Destruir outgoing anterior si existe (crossfade durante crossfade)
if (outgoing_music.stream) {
SDL_DestroyAudioStream(outgoing_music.stream);
outgoing_music.stream = nullptr;
outgoing_music.fade.active = false;
}
// Robar el stream de la musica actual al outgoing para el fade-out.
// Pre-omplim amb `crossfade_ms` de so perquè no es quede en silenci
// abans d'acabar el fade (l'stream robat ja no pot alimentar-se).
if (current_music && current_music->state == JA_MUSIC_PLAYING && current_music->stream) {
JA_PreFillOutgoing(current_music, crossfade_ms);
outgoing_music.stream = current_music->stream;
outgoing_music.fade = {true, SDL_GetTicks(), crossfade_ms, JA_musicVolume};
current_music->stream = nullptr;
current_music->state = JA_MUSIC_STOPPED;
if (current_music->vorbis) stb_vorbis_seek_start(current_music->vorbis);
}
// Iniciar la nueva pista con gain=0 (el fade-in la sube gradualmente)
current_music = music;
current_music->state = JA_MUSIC_PLAYING;
current_music->times = loop;
stb_vorbis_seek_start(current_music->vorbis);
current_music->stream = SDL_CreateAudioStream(&current_music->spec, &JA_audioSpec);
if (!current_music->stream) {
std::cout << "Failed to create audio stream for crossfade!" << '\n';
current_music->state = JA_MUSIC_STOPPED;
return;
}
SDL_SetAudioStreamGain(current_music->stream, 0.0f);
JA_PumpMusic(current_music); // pre-carrega abans de bindejar
SDL_BindAudioStream(sdlAudioDevice, current_music->stream);
// Configurar fade-in
incoming_fade = {true, SDL_GetTicks(), crossfade_ms, 0.0f};
}
inline JA_Music_state JA_GetMusicState() {
@@ -267,9 +476,10 @@ inline void JA_DeleteMusic(JA_Music_t* music) {
JA_StopMusic();
current_music = nullptr;
}
SDL_free(music->buffer);
if (music->stream) SDL_DestroyAudioStream(music->stream);
free(music->filename); // filename se libera aquí
if (music->vorbis) stb_vorbis_close(music->vorbis);
// ogg_data (std::vector) i filename (std::string) s'alliberen sols
// al destructor de JA_Music_t.
delete music;
}
@@ -281,53 +491,41 @@ inline float JA_SetMusicVolume(float volume) {
return JA_musicVolume;
}
inline void JA_SetMusicPosition(float value) {
if (!current_music) return;
current_music->pos = value * current_music->spec.freq;
// Nota: Esta implementación de 'pos' no parece usarse en JA_Update para
// el streaming. El streaming siempre parece empezar desde el principio.
inline void JA_SetMusicPosition(float /*value*/) {
// No implementat amb el backend de streaming.
}
inline float JA_GetMusicPosition() {
if (!current_music) return 0;
return float(current_music->pos) / float(current_music->spec.freq);
// Nota: Ver `JA_SetMusicPosition`
return 0.0f;
}
inline void JA_EnableMusic(const bool value) {
if (!value && current_music && (current_music->state == JA_MUSIC_PLAYING)) JA_StopMusic();
JA_musicEnabled = value;
}
// --- Sound Functions ---
inline JA_Sound_t* JA_NewSound(Uint8* buffer, Uint32 length) {
JA_Sound_t* sound = new JA_Sound_t();
sound->buffer = buffer;
sound->length = length;
// Nota: spec se queda con los valores por defecto.
return sound;
}
inline JA_Sound_t* JA_LoadSound(uint8_t* buffer, uint32_t size) {
JA_Sound_t* sound = new JA_Sound_t();
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &sound->buffer, &sound->length)) {
SDL_Log("Failed to load WAV from memory: %s", SDL_GetError());
delete sound;
auto sound = std::make_unique<JA_Sound_t>();
Uint8* raw = nullptr;
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), 1, &sound->spec, &raw, &sound->length)) {
std::cout << "Failed to load WAV from memory: " << SDL_GetError() << '\n';
return nullptr;
}
return sound;
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
return sound.release();
}
inline JA_Sound_t* JA_LoadSound(const char* filename) {
JA_Sound_t* sound = new JA_Sound_t();
if (!SDL_LoadWAV(filename, &sound->spec, &sound->buffer, &sound->length)) {
SDL_Log("Failed to load WAV file: %s", SDL_GetError());
delete sound;
auto sound = std::make_unique<JA_Sound_t>();
Uint8* raw = nullptr;
if (!SDL_LoadWAV(filename, &sound->spec, &raw, &sound->length)) {
std::cout << "Failed to load WAV file: " << SDL_GetError() << '\n';
return nullptr;
}
return sound;
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
return sound.release();
}
inline int JA_PlaySound(JA_Sound_t* sound, const int loop = 0, const int group = 0) {
@@ -347,22 +545,22 @@ inline int JA_PlaySoundOnChannel(JA_Sound_t* sound, const int channel, const int
if (!JA_soundEnabled || !sound) return -1;
if (channel < 0 || channel >= JA_MAX_SIMULTANEOUS_CHANNELS) return -1;
JA_StopChannel(channel); // Detiene y limpia el canal si estaba en uso
JA_StopChannel(channel);
channels[channel].sound = sound;
channels[channel].times = loop;
channels[channel].pos = 0;
channels[channel].group = group; // Asignar grupo
channels[channel].group = group;
channels[channel].state = JA_CHANNEL_PLAYING;
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &JA_audioSpec);
if (!channels[channel].stream) {
SDL_Log("Failed to create audio stream for sound!");
std::cout << "Failed to create audio stream for sound!" << '\n';
channels[channel].state = JA_CHANNEL_FREE;
return -1;
}
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer, channels[channel].sound->length);
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer.get(), channels[channel].sound->length);
SDL_SetAudioStreamGain(channels[channel].stream, JA_soundVolume[group]);
SDL_BindAudioStream(sdlAudioDevice, channels[channel].stream);
@@ -374,7 +572,7 @@ inline void JA_DeleteSound(JA_Sound_t* sound) {
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if (channels[i].sound == sound) JA_StopChannel(i);
}
SDL_free(sound->buffer);
// buffer es destrueix automàticament via RAII (SDLFreeDeleter).
delete sound;
}
@@ -420,7 +618,7 @@ inline void JA_StopChannel(const int channel) {
channels[i].stream = nullptr;
channels[i].state = JA_CHANNEL_FREE;
channels[i].pos = 0;
channels[i].sound = NULL;
channels[i].sound = nullptr;
}
}
} else if (channel >= 0 && channel < JA_MAX_SIMULTANEOUS_CHANNELS) {
@@ -429,7 +627,7 @@ inline void JA_StopChannel(const int channel) {
channels[channel].stream = nullptr;
channels[channel].state = JA_CHANNEL_FREE;
channels[channel].pos = 0;
channels[channel].sound = NULL;
channels[channel].sound = nullptr;
}
}
}
@@ -441,8 +639,7 @@ inline JA_Channel_state JA_GetChannelState(const int channel) {
return channels[channel].state;
}
inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para todos los grupos
{
inline float JA_SetSoundVolume(float volume, const int group = -1) {
const float v = SDL_clamp(volume, 0.0f, 1.0f);
if (group == -1) {
@@ -452,10 +649,10 @@ inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para t
} else if (group >= 0 && group < JA_MAX_GROUPS) {
JA_soundVolume[group] = v;
} else {
return v; // Grupo inválido
return v;
}
// Aplicar volumen a canales activos
// Aplicar volum als canals actius.
for (int i = 0; i < JA_MAX_SIMULTANEOUS_CHANNELS; i++) {
if ((channels[i].state == JA_CHANNEL_PLAYING) || (channels[i].state == JA_CHANNEL_PAUSED)) {
if (group == -1 || channels[i].group == group) {
@@ -470,13 +667,13 @@ inline float JA_SetSoundVolume(float volume, const int group = -1) // -1 para t
inline void JA_EnableSound(const bool value) {
if (!value) {
JA_StopChannel(-1); // Detener todos los canales
JA_StopChannel(-1);
}
JA_soundEnabled = value;
}
inline float JA_SetVolume(float volume) {
float v = JA_SetMusicVolume(volume);
JA_SetSoundVolume(v, -1); // Aplicar a todos los grupos de sonido
JA_SetSoundVolume(v, -1);
return v;
}
}

View File

@@ -9,6 +9,7 @@
#include "core/locale/locale.hpp" // Para Locale
#include "core/rendering/render_info.hpp" // Para RenderInfo
#include "core/rendering/screen.hpp" // Para Screen
#include "core/system/global_events.hpp" // Para GlobalEvents::consumeGamepadButtonPressed
#include "game/options.hpp" // Para Options, options, OptionsVideo, Section
#include "game/scene_manager.hpp" // Para SceneManager
#include "game/ui/console.hpp" // Para Console
@@ -20,7 +21,8 @@ namespace GlobalInputs {
// Funciones internas
namespace {
void handleQuit() {
// En la escena GAME el comportamiento es siempre el mismo (con o sin modo kiosko)
// En la escena GAME el comportamiento es siempre el mismo (con o sin modo kiosko):
// Escape torna al menu principal. Això també és vàlid en la versió web.
if (SceneManager::current == SceneManager::Scene::GAME) {
const std::string CODE = "PRESS AGAIN TO RETURN TO MENU";
if (stringInVector(Notifier::get()->getCodes(), CODE)) {
@@ -41,6 +43,11 @@ namespace GlobalInputs {
return;
}
#ifdef __EMSCRIPTEN__
// A la versió web no es pot eixir del joc des de fora de l'escena GAME
// (el navegador gestiona la pestanya; Escape no tanca res).
return;
#else
// Comportamiento normal fuera del modo kiosko
const std::string CODE = "PRESS AGAIN TO EXIT";
if (stringInVector(Notifier::get()->getCodes(), CODE)) {
@@ -48,6 +55,7 @@ namespace GlobalInputs {
} else {
Notifier::get()->show({Locale::get()->get("ui.press_again_exit")}, Notifier::Style::DEFAULT, -1, true, CODE); // NOLINT(readability-static-accessed-through-instance)
}
#endif // __EMSCRIPTEN__
}
void handleSkipSection() {
@@ -145,6 +153,13 @@ namespace GlobalInputs {
// Detecta qué acción global ha sido presionada (si alguna)
auto getPressedAction() -> InputAction { // NOLINT(readability-function-cognitive-complexity)
// Qualsevol botó del comandament actua com a ACCEPT (saltar escenes
// d'attract mode: logo, loading, credits, demo, ending...). El botó
// BACK queda filtrat prèviament a GlobalEvents per no colidir amb EXIT
// (excepte en emscripten, on BACK no pot sortir i sí pot saltar).
if (GlobalEvents::consumeGamepadButtonPressed()) {
return InputAction::ACCEPT;
}
if (Input::get()->checkAction(InputAction::EXIT, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::EXIT;
}

View File

@@ -10,6 +10,41 @@
#include "game/options.hpp" // Para Options::controls
// Emscripten-only: SDL 3.4+ ja no casa el GUID dels mandos de Chrome Android
// amb gamecontrollerdb (el gamepad.id d'Android no porta Vendor/Product, el
// parser extreu valors escombraries, el GUID resultant no està a la db i el
// gamepad queda obert amb un mapping incorrecte). Com el W3C Gamepad API
// garanteix el layout estàndard quan el navegador reporta mapping=="standard",
// injectem un mapping SDL amb eixe layout per al GUID del joystick abans
// d'obrir-lo com gamepad. Fora d'Emscripten és un no-op.
static void installWebStandardMapping(SDL_JoystickID jid) {
#ifdef __EMSCRIPTEN__
SDL_GUID guid = SDL_GetJoystickGUIDForID(jid);
char guidStr[33];
SDL_GUIDToString(guid, guidStr, sizeof(guidStr));
const char* name = SDL_GetJoystickNameForID(jid);
if (!name || !*name) name = "Standard Gamepad";
char mapping[512];
SDL_snprintf(mapping, sizeof(mapping),
"%s,%s,"
"a:b0,b:b1,x:b2,y:b3,"
"leftshoulder:b4,rightshoulder:b5,"
"lefttrigger:b6,righttrigger:b7,"
"back:b8,start:b9,"
"leftstick:b10,rightstick:b11,"
"dpup:b12,dpdown:b13,dpleft:b14,dpright:b15,"
"guide:b16,"
"leftx:a0,lefty:a1,rightx:a2,righty:a3,"
"platform:Emscripten",
guidStr,
name);
SDL_AddGamepadMapping(mapping);
#else
(void)jid;
#endif
}
// Singleton
Input* Input::instance = nullptr;
@@ -390,12 +425,21 @@ void Input::update() { // NOLINT(readability-convert-member-functions-to-static
// --- MANDOS ---
for (const auto& gamepad : gamepads_) {
for (auto& binding : gamepad->bindings) {
bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0;
for (auto& [action, state] : gamepad->bindings) {
bool is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(state.button))) != 0;
// JUMP accepta qualsevol dels 4 botons frontals (South/East/North/West)
if (action == Action::JUMP) {
is_down_now = is_down_now ||
(SDL_GetGamepadButton(gamepad->pad, SDL_GAMEPAD_BUTTON_SOUTH) != 0) ||
(SDL_GetGamepadButton(gamepad->pad, SDL_GAMEPAD_BUTTON_EAST) != 0) ||
(SDL_GetGamepadButton(gamepad->pad, SDL_GAMEPAD_BUTTON_NORTH) != 0) ||
(SDL_GetGamepadButton(gamepad->pad, SDL_GAMEPAD_BUTTON_WEST) != 0);
}
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
binding.second.just_pressed = button_is_down_now && !binding.second.is_held;
binding.second.is_held = button_is_down_now;
state.just_pressed = is_down_now && !state.is_held;
state.is_held = is_down_now;
}
}
}
@@ -411,6 +455,7 @@ auto Input::handleEvent(const SDL_Event& event) -> std::string { // NOLINT(read
}
auto Input::addGamepad(int device_index) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
installWebStandardMapping(device_index);
SDL_Gamepad* pad = SDL_OpenGamepad(device_index);
if (pad == nullptr) {
std::cerr << "Error al abrir el gamepad: " << SDL_GetError() << '\n';
@@ -421,7 +466,13 @@ auto Input::addGamepad(int device_index) -> std::string { // NOLINT(readability
auto name = gamepad->name;
std::cout << "Gamepad connected (" << name << ")" << '\n';
gamepads_.push_back(std::move(gamepad));
return name + " CONNECTED";
// Aplica els bindings d'Options al nou gamepad (en hot-plug/wasm el ctor
// ja ha cridat applyGamepadBindingsFromOptions però llavors gamepads_
// estava buit i no s'ha fet res).
applyGamepadBindingsFromOptions();
return name;
}
auto Input::removeGamepad(SDL_JoystickID id) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
@@ -433,7 +484,7 @@ auto Input::removeGamepad(SDL_JoystickID id) -> std::string { // NOLINT(readabi
std::string name = (*it)->name;
std::cout << "Gamepad disconnected (" << name << ")" << '\n';
gamepads_.erase(it);
return name + " DISCONNECTED";
return name;
}
std::cerr << "No se encontró el gamepad con ID " << id << '\n';
return {};

View File

@@ -51,16 +51,34 @@ class Input {
std::string path; // Ruta del dispositivo
std::unordered_map<Action, ButtonState> bindings; // Mapa de acciones a estados de botón
// Recorta el nombre del mando hasta el primer '(' o '[' y elimina espacios finales.
// Evita nombres como "Retroid Controller (vendor: 1001) ..." en las notificaciones.
static auto trimName(const char* raw) -> std::string {
std::string s(raw != nullptr ? raw : "");
const auto pos = s.find_first_of("([");
if (pos != std::string::npos) { s.erase(pos); }
while (!s.empty() && s.back() == ' ') { s.pop_back(); }
return s;
}
explicit Gamepad(SDL_Gamepad* gamepad)
: pad(gamepad),
instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))),
name(std::string(SDL_GetGamepadName(gamepad))),
name(trimName(SDL_GetGamepadName(gamepad))),
path(std::string(SDL_GetGamepadPath(pad))),
bindings{
// Movimiento del jugador
{Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}},
{Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
{Action::JUMP, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}}} {}
{Action::JUMP, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}},
// Botó BACK del mando → sortir escena / tancar joc
{Action::EXIT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_BACK)}},
{Action::CANCEL, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_BACK)}},
// Botó START del mando → pausa
{Action::PAUSE, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_START)}},
// Shoulders → paleta següent / mode d'ordenació de paleta següent
{Action::NEXT_PALETTE, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)}},
{Action::NEXT_PALETTE_SORT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER)}}} {}
~Gamepad() {
if (pad != nullptr) {

View File

@@ -119,9 +119,10 @@ void RenderInfo::render() const {
// Fuente: preferir la de la consola si está disponible
auto text_obj = (Console::get() != nullptr) ? Console::get()->getText() : Screen::get()->getText();
// Posición Y: debajo de la consola + offset animado propio
// Posición Y: debajo de la consola + altura animada de la pila de notificaciones + offset animado propio
const int CONSOLE_Y = (Console::get() != nullptr) ? Console::get()->getVisibleHeight() : 0;
const int Y = CONSOLE_Y + static_cast<int>(y_);
const int NOTIFIER_Y = (Notifier::get() != nullptr) ? Notifier::get()->getVisibleHeight() : 0;
const int Y = CONSOLE_Y + NOTIFIER_Y + static_cast<int>(y_);
// Rectángulo de fondo: ancho completo, alto ajustado al texto
const SDL_FRect RECT = {
@@ -141,17 +142,15 @@ void RenderInfo::render() const {
MSG_COLOR);
}
// Activa o desactiva el overlay y notifica a Notifier del cambio de offset
// Activa o desactiva el overlay (la posición Y se calcula pull-side en render())
void RenderInfo::toggle() {
switch (status_) {
case Status::HIDDEN:
status_ = Status::RISING;
Screen::get()->updateZoomFactor();
if (Notifier::get() != nullptr) { Notifier::get()->addYOffset(HEIGHT); }
break;
case Status::ACTIVE:
status_ = Status::VANISHING;
if (Notifier::get() != nullptr) { Notifier::get()->removeYOffset(HEIGHT); }
break;
default:
break;

View File

@@ -1,6 +1,10 @@
#include "core/rendering/screen.hpp"
#include <SDL3/SDL.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#endif
#include <algorithm> // Para max, min, transform
#include <cctype> // Para toupper
@@ -11,21 +15,80 @@
#include <iterator> // Para istreambuf_iterator, operator==
#include <string> // Para char_traits, string, operator+, operator==
#include "core/input/mouse.hpp" // Para updateCursorVisibility
#include "core/rendering/render_info.hpp" // Para RenderInfo
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader
#include "core/rendering/surface.hpp" // Para Surface, readPalFile
#include "core/rendering/text.hpp" // Para Text
#include "core/resources/resource_cache.hpp" // Para Resource
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
#include "core/resources/resource_list.hpp" // Para Asset, AssetType
#include "game/options.hpp" // Para Options, options, OptionsVideo, Border
#include "game/ui/console.hpp" // Para Console
#include "game/ui/notifier.hpp" // Para Notifier
#include "core/input/mouse.hpp" // Para updateCursorVisibility
#include "core/rendering/render_info.hpp" // Para RenderInfo
#ifndef __EMSCRIPTEN__
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader (no suportat a WebGL2)
#endif
#include "core/rendering/surface.hpp" // Para Surface, readPalFile
#include "core/rendering/text.hpp" // Para Text
#include "core/resources/resource_cache.hpp" // Para Resource
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
#include "core/resources/resource_list.hpp" // Para Asset, AssetType
#include "game/options.hpp" // Para Options, options, OptionsVideo, Border
#include "game/ui/console.hpp" // Para Console
#include "game/ui/notifier.hpp" // Para Notifier
// [SINGLETON]
Screen* Screen::screen = nullptr;
#ifdef __EMSCRIPTEN__
// ============================================================================
// Restauració del canvas en wasm/Emscripten
// ============================================================================
//
// Problema: SDL3 + Emscripten no notifica de manera fiable els canvis de mida
// del canvas HTML. En concret:
// - SDL_EVENT_WINDOW_LEAVE_FULLSCREEN no s'emet quan l'usuari surt de
// fullscreen amb Esc/F11 (libsdl-org/SDL#13300).
// - SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED i SDL_EVENT_DISPLAY_ORIENTATION
// tampoc es disparen de manera fiable (libsdl-org/SDL#11389).
// - Resultat: en sortir de fullscreen el canvas queda a la mida correcta
// però SDL encara creu que està en fullscreen amb la resolució anterior,
// i el joc es veu minúscul fins que l'usuari força un refresh manual
// (p. ex., canviant el mode d'escalat amb F7).
//
// Solució: registrem callbacks natius d'Emscripten (fullscreenchange,
// orientationchange) que re-sincronitzen SDL amb l'estat real del navegador.
// NO registrem resize callback: en mòbil el scroll fa que el navegador oculti/
// mostri la barra d'URL, disparant un resize del DOM per cada scroll.
// Tots delegen en Screen::handleCanvasResized(), que crida setVideoMode()
// amb l'estat de fullscreen actualitzat — això és el que realment restaura
// la finestra SDL perquè dins setVideoMode es crida SDL_SetWindowFullscreen,
// que és imprescindible per treure SDL del seu estat intern de fullscreen.
//
// Els callbacks diferixen la feina amb emscripten_async_call(0ms) perquè
// quan l'event es dispara el navegador encara no ha acabat de redimensionar
// el canvas: llegir la mida en aquest instant donaria un valor obsolet.
// Posposar al següent tick del event loop garanteix que el canvas ja està
// estable quan actuem.
//
// Referències:
// - https://github.com/libsdl-org/SDL/issues/13300
// - https://github.com/libsdl-org/SDL/issues/11389
// - https://discourse.libsdl.org/t/sdl-emscripten-allow-resize-events-on-fullscreen-windows/66279
// ============================================================================
namespace {
void deferredCanvasResize(void* /*user_data*/) {
if (Screen::get() != nullptr) { Screen::get()->handleCanvasResized(); }
}
auto onEmFullscreenChange(int /*event_type*/, const EmscriptenFullscreenChangeEvent* event, void* /*user_data*/) -> EM_BOOL {
// Actualitzem Options::video.fullscreen amb l'estat real del navegador
// abans de diferir la restauració: quan l'usuari surt amb Esc no passem
// per setVideoMode() i l'estat intern quedaria desincronitzat.
Options::video.fullscreen = (event != nullptr && event->isFullscreen != 0);
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
auto onEmOrientationChange(int /*event_type*/, const EmscriptenOrientationChangeEvent* /*event*/, void* /*user_data*/) -> EM_BOOL {
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
} // namespace
#endif
// [SINGLETON] Crearemos el objeto con esta función estática
void Screen::init() {
Screen::screen = new Screen();
@@ -166,6 +229,20 @@ void Screen::toggleVideoMode() {
setVideoMode(Options::video.fullscreen);
}
// Re-sincronitza SDL amb l'estat real del canvas del navegador. L'invoquen els
// callbacks natius d'Emscripten definits a dalt (vegeu el bloc de documentació
// just després dels includes) quan es detecta un fullscreenchange, resize o
// orientationchange. Delegar a setVideoMode() és el que realment restaura la
// finestra: per sota crida SDL_SetWindowFullscreen, imprescindible perquè
// SDL tregui el seu estat intern de fullscreen quan l'usuari ha sortit amb
// Esc sense passar pel nostre toggleVideoMode(). No fem res fora d'emscripten
// perquè en desktop SDL ja emet SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED correctament.
void Screen::handleCanvasResized() {
#ifdef __EMSCRIPTEN__
setVideoMode(Options::video.fullscreen);
#endif
}
// Reduce el tamaño de la ventana
auto Screen::decWindowZoom() -> bool {
if (static_cast<int>(Options::video.fullscreen) == 0) {
@@ -293,16 +370,16 @@ void Screen::adjustWindowSize() {
window_width_ = Options::game.width + (Options::video.border.enabled ? Options::video.border.width * 2 : 0);
window_height_ = Options::game.height + (Options::video.border.enabled ? Options::video.border.height * 2 : 0);
// Reservamos memoria una sola vez.
// Si el buffer es más pequeño que la superficie, crash asegurado.
border_pixel_buffer_.resize(static_cast<size_t>(window_width_ * window_height_));
// border_surface_ sempre té el tamany complet del borde (game + 2*border_w/h),
// independentment de si el borde està visible o no. El buffer ARGB que l'ombra
// ha de ser ALMENYS tan gran com la surface; si no, toARGBBuffer() escriu fora
// de bounds i corromp el heap (bug latent a desktop fins a disparar-lo un toggleBorder).
const size_t FULL_BORDER_BUFFER_SIZE =
static_cast<size_t>(Options::game.width + (Options::video.border.width * 2)) *
static_cast<size_t>(Options::game.height + (Options::video.border.height * 2));
border_pixel_buffer_.resize(FULL_BORDER_BUFFER_SIZE);
game_pixel_buffer_.resize(static_cast<size_t>(Options::game.width * Options::game.height));
// border_pixel_buffer_ es el buffer que se sube a la GPU (tamaño total ventana).
if (Options::video.border.enabled) {
border_pixel_buffer_.resize(static_cast<size_t>(window_width_ * window_height_));
}
// Lógica de centrado y redimensionado de ventana SDL
if (static_cast<int>(Options::video.fullscreen) == 0) {
int old_w;
@@ -605,6 +682,10 @@ void Screen::nextShader() {
// El device GPU se crea siempre (independientemente de postfx) para evitar
// conflictos SDL_Renderer/SDL_GPU al hacer toggle F4 en Windows/Vulkan.
void Screen::initShaders() {
#ifdef __EMSCRIPTEN__
// A WebGL2 no hi ha SDL3 GPU, el render va per SDL_Renderer sense shaders.
shader_backend_.reset();
#else
SDL_Texture* tex = Options::video.border.enabled ? border_texture_ : game_texture_;
if (!shader_backend_) {
@@ -633,6 +714,7 @@ void Screen::initShaders() {
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
applyCurrentCrtPiPreset();
}
#endif
}
// Obtiene información sobre la pantalla
@@ -736,10 +818,25 @@ auto Screen::initSDLVideo() -> bool {
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
SDL_SetRenderVSync(renderer_, Options::video.vertical_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
registerEmscriptenEventCallbacks();
std::cout << "Video system initialized successfully\n";
return true;
}
// Registra els callbacks natius d'Emscripten que restauren el canvas quan
// SDL3 no emet els events equivalents. Fora d'Emscripten és un no-op.
// Vegeu el bloc de documentació a dalt del fitxer per al context complet.
void Screen::registerEmscriptenEventCallbacks() { // NOLINT(readability-convert-member-functions-to-static)
#ifdef __EMSCRIPTEN__
// NO registrem resize callback. En mòbil, fer scroll fa que el navegador
// oculti/mostri la barra d'URL disparant un resize del DOM per cada scroll,
// i això portaria a cridar setVideoMode innecessàriament. Alineat amb CC i CCAE.
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
#endif
}
// Crea el objeto de texto
void Screen::createText() { // NOLINT(readability-convert-member-functions-to-static)
// Carga la surface de la fuente directamente del archivo

View File

@@ -38,6 +38,7 @@ class Screen {
// Video y ventana
void setVideoMode(bool mode); // Establece el modo de video
void toggleVideoMode(); // Cambia entre pantalla completa y ventana
void handleCanvasResized(); // Restaura el canvas quan SDL3 no reporta el canvi (emscripten only: sortida de fullscreen amb Esc, rotació, resize); no-op fora d'emscripten
void toggleIntegerScale(); // Alterna entre activar y desactivar el escalado entero
void toggleVSync(); // Alterna entre activar y desactivar el V-Sync
auto decWindowZoom() -> bool; // Reduce el tamaño de la ventana
@@ -125,18 +126,19 @@ class Screen {
static Screen* screen;
// Métodos privados
void renderNotifications() const; // Dibuja las notificaciones
void adjustWindowSize(); // Calcula el tamaño de la ventana
void adjustRenderLogicalSize(); // Ajusta el tamaño lógico del renderizador
void surfaceToTexture(); // Copia la surface a la textura
void textureToRenderer(); // Copia la textura al renderizador
void renderOverlays(); // Renderiza todos los overlays
void initShaders(); // Inicializa los shaders
void applyCurrentPostFXPreset(); // Aplica los parámetros del preset PostFX actual al backend
void applyCurrentCrtPiPreset(); // Aplica los parámetros del preset CrtPi actual al backend
void getDisplayInfo(); // Obtiene información sobre la pantalla
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
void createText(); // Crea el objeto de texto
void renderNotifications() const; // Dibuja las notificaciones
void adjustWindowSize(); // Calcula el tamaño de la ventana
void adjustRenderLogicalSize(); // Ajusta el tamaño lógico del renderizador
void surfaceToTexture(); // Copia la surface a la textura
void textureToRenderer(); // Copia la textura al renderizador
void renderOverlays(); // Renderiza todos los overlays
void initShaders(); // Inicializa los shaders
void applyCurrentPostFXPreset(); // Aplica los parámetros del preset PostFX actual al backend
void applyCurrentCrtPiPreset(); // Aplica los parámetros del preset CrtPi actual al backend
void getDisplayInfo(); // Obtiene información sobre la pantalla
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
void registerEmscriptenEventCallbacks(); // Registra els callbacks natius per restaurar el canvas en wasm (no-op fora d'emscripten)
void createText(); // Crea el objeto de texto
// Constructor y destructor
Screen();

View File

@@ -8,11 +8,11 @@
#include <iostream> // std::cout
#ifndef __APPLE__
#include "core/rendering/sdl3gpu/crtpi_frag_spv.h"
#include "core/rendering/sdl3gpu/downscale_frag_spv.h"
#include "core/rendering/sdl3gpu/postfx_frag_spv.h"
#include "core/rendering/sdl3gpu/postfx_vert_spv.h"
#include "core/rendering/sdl3gpu/upscale_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/crtpi_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/downscale_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/postfx_frag_spv.h"
#include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h"
#include "core/rendering/sdl3gpu/spv/upscale_frag_spv.h"
#endif
#ifdef __APPLE__

View File

@@ -0,0 +1,2 @@
DisableFormat: true
SortIncludes: Never

View File

@@ -0,0 +1,4 @@
# source/core/rendering/sdl3gpu/spv/.clang-tidy
Checks: '-*'
WarningsAsErrors: ''
HeaderFilterRegex: ''

View File

@@ -219,7 +219,7 @@ AnimatedSprite::AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
: MovingSprite(std::move(surface), pos) {
// animations_ queda buit (protegit per el guard de animate())
if (surface_) {
clip_ = {.x = 0, .y = 0, .w = surface_->getWidth(), .h = surface_->getHeight()};
clip_ = {.x = 0, .y = 0, .w = static_cast<float>(surface_->getWidth()), .h = static_cast<float>(surface_->getHeight())};
}
}

View File

@@ -35,8 +35,8 @@ auto DissolveSprite::computePixelRank(int col, int row, int frame_h, DissolveDir
DissolveSprite::DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
: AnimatedSprite(std::move(surface), pos) {
if (surface_) {
const int W = static_cast<int>(surface_->getWidth());
const int H = static_cast<int>(surface_->getHeight());
const int W = surface_->getWidth();
const int H = surface_->getHeight();
surface_display_ = std::make_shared<Surface>(W, H);
surface_display_->setTransparentColor(surface_->getTransparentColor());
surface_display_->clear(surface_->getTransparentColor());
@@ -47,8 +47,8 @@ DissolveSprite::DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
DissolveSprite::DissolveSprite(const AnimationResource& data)
: AnimatedSprite(data) {
if (surface_) {
const int W = static_cast<int>(surface_->getWidth());
const int H = static_cast<int>(surface_->getHeight());
const int W = surface_->getWidth();
const int H = surface_->getHeight();
surface_display_ = std::make_shared<Surface>(W, H);
surface_display_->setTransparentColor(surface_->getTransparentColor());
// Inicialitza tots els píxels com a transparents
@@ -75,8 +75,8 @@ void DissolveSprite::rebuildDisplaySurface() {
auto src_data = surface_->getSurfaceData();
auto dst_data = surface_display_->getSurfaceData();
const int SRC_W = static_cast<int>(src_data->width);
const int DST_W = static_cast<int>(dst_data->width);
const int SRC_W = src_data->width;
const int DST_W = dst_data->width;
const Uint8 TRANSPARENT = surface_->getTransparentColor();
// Esborra frame anterior si ha canviat

View File

@@ -19,7 +19,7 @@ Sprite::Sprite() = default;
Sprite::Sprite(std::shared_ptr<Surface> surface)
: surface_(std::move(surface)),
pos_{0.0F, 0.0F, surface_->getWidth(), surface_->getHeight()},
pos_{0.0F, 0.0F, static_cast<float>(surface_->getWidth()), static_cast<float>(surface_->getHeight())},
clip_(pos_) {}
// Muestra el sprite por pantalla

View File

@@ -104,7 +104,7 @@ Surface::Surface(const std::string& file_path)
}
// Carga una superficie desde un archivo
auto Surface::loadSurface(const std::string& file_path) -> SurfaceData { // NOLINT(readability-convert-member-functions-to-static)
auto Surface::loadSurface(const std::string& file_path) -> SurfaceData {
// Load file using ResourceHelper (supports both filesystem and pack)
std::vector<Uint8> buffer = Resource::Helper::loadFile(file_path);
if (buffer.empty()) {
@@ -129,7 +129,7 @@ auto Surface::loadSurface(const std::string& file_path) -> SurfaceData { // NOL
// Crear y devolver directamente el objeto SurfaceData
printWithDots("Surface : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
return {static_cast<float>(w), static_cast<float>(h), pixels};
return {static_cast<int>(w), static_cast<int>(h), pixels};
}
// Carga una paleta desde un archivo
@@ -149,7 +149,7 @@ void Surface::setColor(int index, Uint32 color) {
// Rellena la superficie con un color
void Surface::clear(Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
const size_t TOTAL_PIXELS = surface_data_->width * surface_data_->height;
const size_t TOTAL_PIXELS = static_cast<size_t>(surface_data_->width) * static_cast<size_t>(surface_data_->height);
Uint8* data_ptr = surface_data_->data.get();
std::fill(data_ptr, data_ptr + TOTAL_PIXELS, color);
}
@@ -165,19 +165,19 @@ void Surface::putPixel(int x, int y, Uint8 color) { // NOLINT(readability-conve
}
// Obtiene el color de un pixel de la surface_data
auto Surface::getPixel(int x, int y) -> Uint8 { return surface_data_->data.get()[x + (y * static_cast<int>(surface_data_->width))]; }
auto Surface::getPixel(int x, int y) -> Uint8 { return surface_data_->data.get()[x + (y * surface_data_->width)]; }
// Dibuja un rectangulo relleno
void Surface::fillRect(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
// Limitar los valores del rectángulo al tamaño de la superficie
float x_start = std::max(0.0F, rect->x);
float y_start = std::max(0.0F, rect->y);
float x_end = std::min(rect->x + rect->w, surface_data_->width);
float y_end = std::min(rect->y + rect->h, surface_data_->height);
float x_end = std::min(rect->x + rect->w, static_cast<float>(surface_data_->width));
float y_end = std::min(rect->y + rect->h, static_cast<float>(surface_data_->height));
// Rellenar fila a fila con memset (memoria contigua por fila)
Uint8* data_ptr = surface_data_->data.get();
const int SURF_WIDTH = static_cast<int>(surface_data_->width);
const int SURF_WIDTH = surface_data_->width;
const int ROW_WIDTH = static_cast<int>(x_end) - static_cast<int>(x_start);
for (int y = static_cast<int>(y_start); y < static_cast<int>(y_end); ++y) {
std::memset(data_ptr + (y * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
@@ -189,12 +189,12 @@ void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(re
// Limitar los valores del rectángulo al tamaño de la superficie
float x_start = std::max(0.0F, rect->x);
float y_start = std::max(0.0F, rect->y);
float x_end = std::min(rect->x + rect->w, surface_data_->width);
float y_end = std::min(rect->y + rect->h, surface_data_->height);
float x_end = std::min(rect->x + rect->w, static_cast<float>(surface_data_->width));
float y_end = std::min(rect->y + rect->h, static_cast<float>(surface_data_->height));
// Dibujar bordes horizontales con memset (líneas contiguas en memoria)
Uint8* data_ptr = surface_data_->data.get();
const int SURF_WIDTH = static_cast<int>(surface_data_->width);
const int SURF_WIDTH = surface_data_->width;
const int ROW_WIDTH = static_cast<int>(x_end) - static_cast<int>(x_start);
std::memset(data_ptr + (static_cast<int>(y_start) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
std::memset(data_ptr + ((static_cast<int>(y_end) - 1) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
@@ -211,68 +211,38 @@ void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(re
}
}
// Dibuja una linea
// Dibuja una linea (Bresenham en enteros)
void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
// Calcula las diferencias
float dx = std::abs(x2 - x1);
float dy = std::abs(y2 - y1);
int ix1 = static_cast<int>(std::lround(x1));
int iy1 = static_cast<int>(std::lround(y1));
const int IX2 = static_cast<int>(std::lround(x2));
const int IY2 = static_cast<int>(std::lround(y2));
// Determina la dirección del incremento
float sx = (x1 < x2) ? 1 : -1;
float sy = (y1 < y2) ? 1 : -1;
const int DX = std::abs(IX2 - ix1);
const int DY = std::abs(IY2 - iy1);
const int SX = (ix1 < IX2) ? 1 : -1;
const int SY = (iy1 < IY2) ? 1 : -1;
float err = dx - dy;
const int SURF_W = surface_data_->width;
const int SURF_H = surface_data_->height;
Uint8* data_ptr = surface_data_->data.get();
int err = DX - DY;
while (true) {
// Asegúrate de no dibujar fuera de los límites de la superficie
if (x1 >= 0 && x1 < surface_data_->width && y1 >= 0 && y1 < surface_data_->height) {
surface_data_->data.get()[static_cast<size_t>(x1 + (y1 * surface_data_->width))] = color;
if (ix1 >= 0 && ix1 < SURF_W && iy1 >= 0 && iy1 < SURF_H) {
data_ptr[ix1 + (iy1 * SURF_W)] = color;
}
// Si alcanzamos el punto final, salimos
if (x1 == x2 && y1 == y2) {
if (ix1 == IX2 && iy1 == IY2) {
break;
}
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x1 += sx;
if (e2 > -DY) {
err -= DY;
ix1 += SX;
}
if (e2 < dx) {
err += dx;
y1 += sy;
}
}
}
void Surface::render(float dx, float dy, float sx, float sy, float w, float h) { // NOLINT(readability-make-member-function-const)
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
// Limitar la región para evitar accesos fuera de rango en origen
w = std::min(w, surface_data_->width - sx);
h = std::min(h, surface_data_->height - sy);
// Limitar la región para evitar accesos fuera de rango en destino
w = std::min(w, surface_data->width - dx);
h = std::min(h, surface_data->height - dy);
const Uint8* src_ptr = surface_data_->data.get();
Uint8* dst_ptr = surface_data->data.get();
for (int iy = 0; iy < h; ++iy) {
for (int ix = 0; ix < w; ++ix) {
// Verificar que las coordenadas de destino están dentro de los límites
if (int dest_x = dx + ix; dest_x >= 0 && dest_x < surface_data->width) {
if (int dest_y = dy + iy; dest_y >= 0 && dest_y < surface_data->height) {
int src_x = sx + ix;
int src_y = sy + iy;
Uint8 color = src_ptr[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
if (color != static_cast<Uint8>(transparent_color_)) {
dst_ptr[static_cast<size_t>(dest_x + (dest_y * surface_data->width))] = sub_palette_[color];
}
}
}
if (e2 < DX) {
err += DX;
iy1 += SY;
}
}
}
@@ -283,18 +253,14 @@ void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) { //
// Determina la región de origen (clip) a renderizar
float sx = (src_rect != nullptr) ? src_rect->x : 0;
float sy = (src_rect != nullptr) ? src_rect->y : 0;
float w = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
float h = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
float w = (src_rect != nullptr) ? src_rect->w : static_cast<float>(surface_data_->width);
float h = (src_rect != nullptr) ? src_rect->h : static_cast<float>(surface_data_->height);
// Limitar la región para evitar accesos fuera de rango en origen
w = std::min(w, surface_data_->width - sx);
h = std::min(h, surface_data_->height - sy);
w = std::min(w, surface_data_dest->width - x);
h = std::min(h, surface_data_dest->height - y);
// Limitar la región para evitar accesos fuera de rango en destino
w = std::min(w, surface_data_dest->width - x);
h = std::min(h, surface_data_dest->height - y);
// Limitar la región para evitar accesos fuera de rango (origen y destino)
w = std::min(w, static_cast<float>(surface_data_->width) - sx);
h = std::min(h, static_cast<float>(surface_data_->height) - sy);
w = std::min(w, static_cast<float>(surface_data_dest->width - x));
h = std::min(h, static_cast<float>(surface_data_dest->height - y));
// Renderiza píxel por píxel aplicando el flip si es necesario
const Uint8* src_ptr = surface_data_->data.get();
@@ -322,7 +288,7 @@ void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) { //
}
// Helper para calcular coordenadas con flip
void Surface::calculateFlippedCoords(int ix, int iy, float sx, float sy, float w, float h, SDL_FlipMode flip, int& src_x, int& src_y) {
void Surface::calculateFlippedCoords(int ix, int iy, int sx, int sy, int w, int h, SDL_FlipMode flip, int& src_x, int& src_y) {
src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
}
@@ -418,11 +384,11 @@ void Surface::renderWithColorReplace(int x, int y, Uint8 source_color, Uint8 tar
continue; // Saltar píxeles fuera del rango del destino
}
// Copia el píxel si no es transparente
// Copia el píxel si no es transparente; aplica sub_palette_ como el resto de render*
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
if (color != static_cast<Uint8>(transparent_color_)) {
surface_data->data[dest_x + (dest_y * surface_data->width)] =
(color == source_color) ? target_color : color;
(color == source_color) ? target_color : sub_palette_[color];
}
}
}
@@ -452,14 +418,14 @@ static auto computeFadeDensity(int screen_y, int fade_h, int canvas_height) -> f
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, SDL_FRect* src_rect) const {
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0;
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : static_cast<int>(surface_data_->width);
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : static_cast<int>(surface_data_->height);
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : surface_data_->width;
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : surface_data_->height;
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
for (int row = 0; row < SH; row++) {
const int SCREEN_Y = y + row;
if (SCREEN_Y < 0 || SCREEN_Y >= static_cast<int>(surface_data_dest->height)) {
if (SCREEN_Y < 0 || SCREEN_Y >= surface_data_dest->height) {
continue;
}
@@ -467,11 +433,11 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
for (int col = 0; col < SW; col++) {
const int SCREEN_X = x + col;
if (SCREEN_X < 0 || SCREEN_X >= static_cast<int>(surface_data_dest->width)) {
if (SCREEN_X < 0 || SCREEN_X >= surface_data_dest->width) {
continue;
}
const Uint8 COLOR = surface_data_->data[((SY + row) * static_cast<int>(surface_data_->width)) + (SX + col)];
const Uint8 COLOR = surface_data_->data[((SY + row) * surface_data_->width) + (SX + col)];
if (COLOR == static_cast<Uint8>(transparent_color_)) {
continue;
}
@@ -480,7 +446,7 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
continue; // Pixel tapat per la zona de fade
}
surface_data_dest->data[SCREEN_X + (SCREEN_Y * static_cast<int>(surface_data_dest->width))] = sub_palette_[COLOR];
surface_data_dest->data[SCREEN_X + (SCREEN_Y * surface_data_dest->width)] = sub_palette_[COLOR];
}
}
}
@@ -489,14 +455,14 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect) const {
const int SX = (src_rect != nullptr) ? static_cast<int>(src_rect->x) : 0;
const int SY = (src_rect != nullptr) ? static_cast<int>(src_rect->y) : 0;
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : static_cast<int>(surface_data_->width);
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : static_cast<int>(surface_data_->height);
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : surface_data_->width;
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : surface_data_->height;
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
for (int row = 0; row < SH; row++) {
const int SCREEN_Y = y + row;
if (SCREEN_Y < 0 || SCREEN_Y >= static_cast<int>(surface_data_dest->height)) {
if (SCREEN_Y < 0 || SCREEN_Y >= surface_data_dest->height) {
continue;
}
@@ -504,11 +470,11 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
for (int col = 0; col < SW; col++) {
const int SCREEN_X = x + col;
if (SCREEN_X < 0 || SCREEN_X >= static_cast<int>(surface_data_dest->width)) {
if (SCREEN_X < 0 || SCREEN_X >= surface_data_dest->width) {
continue;
}
const Uint8 COLOR = surface_data_->data[((SY + row) * static_cast<int>(surface_data_->width)) + (SX + col)];
const Uint8 COLOR = surface_data_->data[((SY + row) * surface_data_->width) + (SX + col)];
if (COLOR == static_cast<Uint8>(transparent_color_)) {
continue;
}
@@ -518,7 +484,7 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
}
const Uint8 OUT_COLOR = (COLOR == source_color) ? target_color : sub_palette_[COLOR];
surface_data_dest->data[SCREEN_X + (SCREEN_Y * static_cast<int>(surface_data_dest->width))] = OUT_COLOR;
surface_data_dest->data[SCREEN_X + (SCREEN_Y * surface_data_dest->width)] = OUT_COLOR;
}
}
}
@@ -527,8 +493,8 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
void Surface::toARGBBuffer(Uint32* buffer) const {
if (!surface_data_ || !surface_data_->data || (buffer == nullptr)) { return; }
const int WIDTH = static_cast<int>(surface_data_->width);
const int HEIGHT = static_cast<int>(surface_data_->height);
const int WIDTH = surface_data_->width;
const int HEIGHT = surface_data_->height;
const Uint8* src = surface_data_->data.get();
// Obtenemos el tamaño de la paleta para evitar accesos fuera de rango
@@ -639,11 +605,8 @@ void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FR
// Realiza un efecto de fundido en la paleta principal
auto Surface::fadePalette() -> bool { // NOLINT(readability-convert-member-functions-to-static)
// Verificar que el tamaño mínimo de palette_ sea adecuado
static constexpr int PALETTE_SIZE = 19;
if (sizeof(palette_) / sizeof(palette_[0]) < PALETTE_SIZE) {
throw std::runtime_error("Palette size is insufficient for fadePalette operation.");
}
static_assert(std::tuple_size_v<Palette> >= PALETTE_SIZE, "Palette size is insufficient for fadePalette operation.");
// Desplazar colores (pares e impares)
for (int i = 18; i > 1; --i) {
@@ -673,11 +636,8 @@ auto Surface::fadeSubPalette(Uint32 delay) -> bool { // NOLINT(readability-conv
// Actualizar el último tick
last_tick_ = current_tick;
// Verificar que el tamaño mínimo de sub_palette_ sea adecuado
static constexpr int SUB_PALETTE_SIZE = 19;
if (sizeof(sub_palette_) / sizeof(sub_palette_[0]) < SUB_PALETTE_SIZE) {
throw std::runtime_error("Palette size is insufficient for fadePalette operation.");
}
static_assert(std::tuple_size_v<SubPalette> >= SUB_PALETTE_SIZE, "Sub-palette size is insufficient for fadeSubPalette operation.");
// Desplazar colores (pares e impares)
for (int i = 18; i > 1; --i) {

View File

@@ -22,8 +22,8 @@ auto readPalFile(const std::string& file_path) -> Palette;
struct SurfaceData {
std::shared_ptr<Uint8[]> data; // Usa std::shared_ptr para gestión automática
float width; // Ancho de la imagen
float height; // Alto de la imagen
int width; // Ancho de la imagen
int height; // Alto de la imagen
// Constructor por defecto
SurfaceData()
@@ -32,13 +32,13 @@ struct SurfaceData {
height(0) {}
// Constructor que inicializa dimensiones y asigna memoria
SurfaceData(float w, float h)
: data(std::shared_ptr<Uint8[]>(new Uint8[static_cast<size_t>(w * h)](), std::default_delete<Uint8[]>())),
SurfaceData(int w, int h)
: data(std::shared_ptr<Uint8[]>(new Uint8[static_cast<size_t>(w) * static_cast<size_t>(h)](), std::default_delete<Uint8[]>())),
width(w),
height(h) {}
// Constructor para inicializar directamente con datos
SurfaceData(float w, float h, std::shared_ptr<Uint8[]> pixels)
SurfaceData(int w, int h, std::shared_ptr<Uint8[]> pixels)
: data(std::move(pixels)),
width(w),
height(h) {}
@@ -56,6 +56,9 @@ struct SurfaceData {
class Surface {
private:
// shared_ptr porque render() accede al SurfaceData propio y al del renderer
// surface (ver getRendererSurface()) de forma efímera; con self-blit ambos
// pueden alias y el refcount evita free accidental durante el recorrido.
std::shared_ptr<SurfaceData> surface_data_; // Datos a dibujar
Palette palette_; // Paleta para volcar la SurfaceData a una Textura
SubPalette sub_palette_; // Paleta para reindexar colores
@@ -77,7 +80,6 @@ class Surface {
void loadPalette(const Palette& palette);
// Copia una región de la SurfaceData de origen a la SurfaceData de destino
void render(float dx, float dy, float sx, float sy, float w, float h);
void render(int x, int y, SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
void render(SDL_FRect* src_rect = nullptr, SDL_FRect* dst_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
@@ -127,11 +129,10 @@ class Surface {
// Metodos para gestionar surface_data_
[[nodiscard]] auto getSurfaceData() const -> std::shared_ptr<SurfaceData> { return surface_data_; }
void setSurfaceData(std::shared_ptr<SurfaceData> new_data) { surface_data_ = std::move(new_data); }
// Obtien ancho y alto
[[nodiscard]] auto getWidth() const -> float { return surface_data_->width; }
[[nodiscard]] auto getHeight() const -> float { return surface_data_->height; }
[[nodiscard]] auto getWidth() const -> int { return surface_data_->width; }
[[nodiscard]] auto getHeight() const -> int { return surface_data_->height; }
// Color transparente
[[nodiscard]] auto getTransparentColor() const -> Uint8 { return transparent_color_; }
@@ -146,7 +147,7 @@ class Surface {
private:
// Helper para calcular coordenadas con flip
static void calculateFlippedCoords(int ix, int iy, float sx, float sy, float w, float h, SDL_FlipMode flip, int& src_x, int& src_y);
static void calculateFlippedCoords(int ix, int iy, int sx, int sy, int w, int h, SDL_FlipMode flip, int& src_x, int& src_y);
// Helper para copiar un pixel si no es transparente
void copyPixelIfNotTransparent(Uint8* dest_data, int dest_x, int dest_y, int dest_width, int src_x, int src_y) const;

View File

@@ -38,10 +38,10 @@ namespace Resource {
// [SINGLETON] Con este método obtenemos el objeto cache y podemos trabajar con él
auto Cache::get() -> Cache* { return Cache::cache; }
// Constructor
// Constructor — no dispara la carga. Director llama a beginLoad() + loadStep()
// desde iterate() para que el bucle SDL3 esté vivo durante la carga.
Cache::Cache()
: loading_text_(Screen::get()->getText()) {
load();
}
// Vacia todos los vectores de recursos
@@ -53,12 +53,11 @@ namespace Resource {
text_files_.clear();
texts_.clear();
animations_.clear();
rooms_.clear();
}
// Carga todos los recursos
// Carga todos los recursos de golpe (usado solo por reload() en hot-reload de debug)
void Cache::load() {
// Nota: el overlay de debug (RenderInfo) se inicializa después de esta carga,
// por lo que updateZoomFactor() se llamará correctamente en RenderInfo::init().
calculateTotal();
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK));
std::cout << "\n** LOADING RESOURCES" << '\n';
@@ -73,7 +72,162 @@ namespace Resource {
std::cout << "\n** RESOURCES LOADED" << '\n';
}
// Recarga todos los recursos
// Prepara el loader incremental. Director lo llama una vez tras Cache::init().
void Cache::beginLoad() {
calculateTotal();
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK));
std::cout << "\n** LOADING RESOURCES (incremental)" << '\n';
stage_ = LoadStage::SOUNDS;
stage_index_ = 0;
}
auto Cache::isLoadDone() const -> bool {
return stage_ == LoadStage::DONE;
}
// Carga assets hasta agotar el presupuesto de tiempo o completar todas las etapas.
// Devuelve true cuando ya no queda nada por cargar.
auto Cache::loadStep(int budget_ms) -> bool {
if (stage_ == LoadStage::DONE) return true;
const Uint64 start_ns = SDL_GetTicksNS();
const Uint64 budget_ns = static_cast<Uint64>(budget_ms) * 1'000'000ULL;
auto listOf = [](List::Type t) { return List::get()->getListByType(t); };
while (stage_ != LoadStage::DONE) {
switch (stage_) {
case LoadStage::SOUNDS: {
auto list = listOf(List::Type::SOUND);
if (stage_index_ == 0) {
std::cout << "\n>> SOUND FILES" << '\n';
sounds_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::MUSICS;
stage_index_ = 0;
break;
}
loadOneSound(stage_index_++);
break;
}
case LoadStage::MUSICS: {
auto list = listOf(List::Type::MUSIC);
if (stage_index_ == 0) {
std::cout << "\n>> MUSIC FILES" << '\n';
musics_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::SURFACES;
stage_index_ = 0;
break;
}
loadOneMusic(stage_index_++);
break;
}
case LoadStage::SURFACES: {
auto list = listOf(List::Type::BITMAP);
if (stage_index_ == 0) {
std::cout << "\n>> SURFACES" << '\n';
surfaces_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::SURFACES_POST;
stage_index_ = 0;
break;
}
loadOneSurface(stage_index_++);
break;
}
case LoadStage::SURFACES_POST: {
finalizeSurfaces();
stage_ = LoadStage::PALETTES;
stage_index_ = 0;
break;
}
case LoadStage::PALETTES: {
auto list = listOf(List::Type::PALETTE);
if (stage_index_ == 0) {
std::cout << "\n>> PALETTES" << '\n';
palettes_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::TEXT_FILES;
stage_index_ = 0;
break;
}
loadOnePalette(stage_index_++);
break;
}
case LoadStage::TEXT_FILES: {
auto list = listOf(List::Type::FONT);
if (stage_index_ == 0) {
std::cout << "\n>> TEXT FILES" << '\n';
text_files_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::ANIMATIONS;
stage_index_ = 0;
break;
}
loadOneTextFile(stage_index_++);
break;
}
case LoadStage::ANIMATIONS: {
auto list = listOf(List::Type::ANIMATION);
if (stage_index_ == 0) {
std::cout << "\n>> ANIMATIONS" << '\n';
animations_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::ROOMS;
stage_index_ = 0;
break;
}
loadOneAnimation(stage_index_++);
break;
}
case LoadStage::ROOMS: {
auto list = listOf(List::Type::ROOM);
if (stage_index_ == 0) {
std::cout << "\n>> ROOMS" << '\n';
rooms_.clear();
}
if (stage_index_ >= list.size()) {
stage_ = LoadStage::TEXTS;
stage_index_ = 0;
break;
}
loadOneRoom(stage_index_++);
break;
}
case LoadStage::TEXTS: {
// createText itera sobre una lista fija de 5 fuentes
constexpr size_t TEXT_COUNT = 5;
if (stage_index_ == 0) {
std::cout << "\n>> CREATING TEXT_OBJECTS" << '\n';
texts_.clear();
}
if (stage_index_ >= TEXT_COUNT) {
stage_ = LoadStage::DONE;
stage_index_ = 0;
std::cout << "\n** RESOURCES LOADED" << '\n';
break;
}
createOneText(stage_index_++);
break;
}
case LoadStage::DONE:
break;
}
if ((SDL_GetTicksNS() - start_ns) >= budget_ns) break;
}
return stage_ == LoadStage::DONE;
}
// Recarga todos los recursos (síncrono, solo para hot-reload de debug)
void Cache::reload() {
clear();
load();
@@ -217,93 +371,96 @@ namespace Resource {
throw;
}
// Carga los sonidos
void Cache::loadSounds() { // NOLINT(readability-convert-member-functions-to-static)
std::cout << "\n>> SOUND FILES" << '\n';
// Lista fija de text objects. Compartida entre createText() y createOneText(i).
namespace {
struct TextObjectInfo {
std::string key; // Identificador del recurso
std::string texture_file; // Nombre del archivo de textura
std::string text_file; // Nombre del archivo de texto
};
const std::vector<TextObjectInfo>& getTextObjectInfos() {
static const std::vector<TextObjectInfo> info = {
{.key = "aseprite", .texture_file = "aseprite.gif", .text_file = "aseprite.fnt"},
{.key = "gauntlet", .texture_file = "gauntlet.gif", .text_file = "gauntlet.fnt"},
{.key = "smb2", .texture_file = "smb2.gif", .text_file = "smb2.fnt"},
{.key = "subatomic", .texture_file = "subatomic.gif", .text_file = "subatomic.fnt"},
{.key = "8bithud", .texture_file = "8bithud.gif", .text_file = "8bithud.fnt"}};
return info;
}
} // namespace
// --- Helpers incrementales (un asset por llamada) ---
void Cache::loadOneSound(size_t index) {
auto list = List::get()->getListByType(List::Type::SOUND);
sounds_.clear();
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
JA_Sound_t* sound = nullptr;
for (const auto& l : list) {
try {
auto name = getFileName(l);
JA_Sound_t* sound = nullptr;
// Try loading from resource pack first
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
sound = JA_LoadSound(audio_data.data(), static_cast<Uint32>(audio_data.size()));
}
// Fallback to file path if memory loading failed
if (sound == nullptr) {
sound = JA_LoadSound(l.c_str());
}
if (sound == nullptr) {
throw std::runtime_error("Failed to decode audio file");
}
sounds_.emplace_back(SoundResource{.name = name, .sound = sound});
printWithDots("Sound : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("SOUND", l, e);
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
sound = JA_LoadSound(audio_data.data(), static_cast<Uint32>(audio_data.size()));
}
if (sound == nullptr) {
sound = JA_LoadSound(l.c_str());
}
if (sound == nullptr) {
throw std::runtime_error("Failed to decode audio file");
}
sounds_.emplace_back(SoundResource{.name = name, .sound = sound});
printWithDots("Sound : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("SOUND", l, e);
}
}
// Carga las musicas
void Cache::loadMusics() { // NOLINT(readability-convert-member-functions-to-static)
std::cout << "\n>> MUSIC FILES" << '\n';
void Cache::loadOneMusic(size_t index) {
auto list = List::get()->getListByType(List::Type::MUSIC);
musics_.clear();
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
JA_Music_t* music = nullptr;
for (const auto& l : list) {
try {
auto name = getFileName(l);
JA_Music_t* music = nullptr;
// Try loading from resource pack first
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
music = JA_LoadMusic(audio_data.data(), static_cast<Uint32>(audio_data.size()));
}
// Fallback to file path if memory loading failed
if (music == nullptr) {
music = JA_LoadMusic(l.c_str());
}
if (music == nullptr) {
throw std::runtime_error("Failed to decode music file");
}
musics_.emplace_back(MusicResource{.name = name, .music = music});
printWithDots("Music : ", name, "[ LOADED ]");
updateLoadingProgress(1);
} catch (const std::exception& e) {
throwLoadError("MUSIC", l, e);
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
music = JA_LoadMusic(audio_data.data(), static_cast<Uint32>(audio_data.size()));
}
if (music == nullptr) {
music = JA_LoadMusic(l.c_str());
}
if (music == nullptr) {
throw std::runtime_error("Failed to decode music file");
}
musics_.emplace_back(MusicResource{.name = name, .music = music});
printWithDots("Music : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("MUSIC", l, e);
}
}
// Carga las texturas
void Cache::loadSurfaces() { // NOLINT(readability-convert-member-functions-to-static)
std::cout << "\n>> SURFACES" << '\n';
void Cache::loadOneSurface(size_t index) {
auto list = List::get()->getListByType(List::Type::BITMAP);
surfaces_.clear();
for (const auto& l : list) {
try {
auto name = getFileName(l);
surfaces_.emplace_back(SurfaceResource{.name = name, .surface = std::make_shared<Surface>(l)});
surfaces_.back().surface->setTransparentColor(0);
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("BITMAP", l, e);
}
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
surfaces_.emplace_back(SurfaceResource{.name = name, .surface = std::make_shared<Surface>(l)});
surfaces_.back().surface->setTransparentColor(0);
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("BITMAP", l, e);
}
}
void Cache::finalizeSurfaces() {
// Reconfigura el color transparente de algunas surfaces
getSurface("loading_screen_color.gif")->setTransparentColor();
getSurface("ending1.gif")->setTransparentColor();
@@ -314,104 +471,132 @@ namespace Resource {
getSurface("standard.gif")->setTransparentColor(16);
}
// Carga las paletas
void Cache::loadPalettes() { // NOLINT(readability-convert-member-functions-to-static)
void Cache::loadOnePalette(size_t index) {
auto list = List::get()->getListByType(List::Type::PALETTE);
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("PALETTE", l, e);
}
}
void Cache::loadOneTextFile(size_t index) {
auto list = List::get()->getListByType(List::Type::FONT);
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("FONT", l, e);
}
}
void Cache::loadOneAnimation(size_t index) {
auto list = List::get()->getListByType(List::Type::ANIMATION);
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
auto yaml_bytes = Helper::loadFile(l);
if (yaml_bytes.empty()) {
throw std::runtime_error("File is empty or could not be loaded");
}
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = yaml_bytes});
printWithDots("Animation : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("ANIMATION", l, e);
}
}
void Cache::loadOneRoom(size_t index) {
auto list = List::get()->getListByType(List::Type::ROOM);
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
rooms_.emplace_back(RoomResource{.name = name, .room = std::make_shared<Room::Data>(Room::loadYAML(l))});
printWithDots("Room : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("ROOM", l, e);
}
}
void Cache::createOneText(size_t index) {
const auto& infos = getTextObjectInfos();
const auto& res_info = infos[index];
texts_.emplace_back(TextResource{
.name = res_info.key,
.text = std::make_shared<Text>(getSurface(res_info.texture_file), getTextFile(res_info.text_file))});
printWithDots("Text : ", res_info.key, "[ DONE ]");
}
// --- Bucles completos (solo usados por reload() síncrono) ---
void Cache::loadSounds() {
std::cout << "\n>> SOUND FILES" << '\n';
auto list = List::get()->getListByType(List::Type::SOUND);
sounds_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOneSound(i);
}
void Cache::loadMusics() {
std::cout << "\n>> MUSIC FILES" << '\n';
auto list = List::get()->getListByType(List::Type::MUSIC);
musics_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOneMusic(i);
}
void Cache::loadSurfaces() {
std::cout << "\n>> SURFACES" << '\n';
auto list = List::get()->getListByType(List::Type::BITMAP);
surfaces_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOneSurface(i);
finalizeSurfaces();
}
void Cache::loadPalettes() {
std::cout << "\n>> PALETTES" << '\n';
auto list = List::get()->getListByType(List::Type::PALETTE);
palettes_.clear();
for (const auto& l : list) {
try {
auto name = getFileName(l);
palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("PALETTE", l, e);
}
}
for (size_t i = 0; i < list.size(); ++i) loadOnePalette(i);
}
// Carga los ficheros de texto
void Cache::loadTextFiles() { // NOLINT(readability-convert-member-functions-to-static)
void Cache::loadTextFiles() {
std::cout << "\n>> TEXT FILES" << '\n';
auto list = List::get()->getListByType(List::Type::FONT);
text_files_.clear();
for (const auto& l : list) {
try {
auto name = getFileName(l);
text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("FONT", l, e);
}
}
for (size_t i = 0; i < list.size(); ++i) loadOneTextFile(i);
}
// Carga las animaciones
void Cache::loadAnimations() { // NOLINT(readability-convert-member-functions-to-static)
void Cache::loadAnimations() {
std::cout << "\n>> ANIMATIONS" << '\n';
auto list = List::get()->getListByType(List::Type::ANIMATION);
animations_.clear();
for (const auto& l : list) {
try {
auto name = getFileName(l);
// Cargar bytes del archivo YAML sin parsear (carga lazy)
auto yaml_bytes = Helper::loadFile(l);
if (yaml_bytes.empty()) {
throw std::runtime_error("File is empty or could not be loaded");
}
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = yaml_bytes});
printWithDots("Animation : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("ANIMATION", l, e);
}
}
for (size_t i = 0; i < list.size(); ++i) loadOneAnimation(i);
}
// Carga las habitaciones desde archivos YAML
void Cache::loadRooms() { // NOLINT(readability-convert-member-functions-to-static)
void Cache::loadRooms() {
std::cout << "\n>> ROOMS" << '\n';
auto list = List::get()->getListByType(List::Type::ROOM);
rooms_.clear();
for (const auto& l : list) {
try {
auto name = getFileName(l);
rooms_.emplace_back(RoomResource{.name = name, .room = std::make_shared<Room::Data>(Room::loadYAML(l))});
printWithDots("Room : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("ROOM", l, e);
}
}
for (size_t i = 0; i < list.size(); ++i) loadOneRoom(i);
}
void Cache::createText() { // NOLINT(readability-convert-member-functions-to-static)
struct ResourceInfo {
std::string key; // Identificador del recurso
std::string texture_file; // Nombre del archivo de textura
std::string text_file; // Nombre del archivo de texto
};
void Cache::createText() {
std::cout << "\n>> CREATING TEXT_OBJECTS" << '\n';
std::vector<ResourceInfo> resources = {
{.key = "aseprite", .texture_file = "aseprite.gif", .text_file = "aseprite.fnt"},
{.key = "gauntlet", .texture_file = "gauntlet.gif", .text_file = "gauntlet.fnt"},
{.key = "smb2", .texture_file = "smb2.gif", .text_file = "smb2.fnt"},
{.key = "subatomic", .texture_file = "subatomic.gif", .text_file = "subatomic.fnt"},
{.key = "8bithud", .texture_file = "8bithud.gif", .text_file = "8bithud.fnt"}};
for (const auto& res_info : resources) {
texts_.emplace_back(TextResource{.name = res_info.key, .text = std::make_shared<Text>(getSurface(res_info.texture_file), getTextFile(res_info.text_file))});
printWithDots("Text : ", res_info.key, "[ DONE ]");
}
texts_.clear();
const auto& infos = getTextObjectInfos();
for (size_t i = 0; i < infos.size(); ++i) createOneText(i);
}
// Vacía el vector de sonidos
@@ -460,14 +645,20 @@ namespace Resource {
// Muestra el progreso de carga
void Cache::renderProgress() {
Screen::get()->start();
Screen::get()->clearSurface(static_cast<Uint8>(PaletteColor::BLACK));
// Si show=false: pantalla negra y salir
if (!Options::loading.show) {
Screen::get()->render();
return;
}
constexpr float X_PADDING = 60.0F;
constexpr float Y_PADDING = 10.0F;
constexpr float BAR_HEIGHT = 5.0F;
const float BAR_POSITION = Options::game.height - BAR_HEIGHT - Y_PADDING;
Screen::get()->start();
Screen::get()->clearSurface(static_cast<Uint8>(PaletteColor::BLACK));
auto surface = Screen::get()->getRendererSurface();
const auto LOADING_TEXT_COLOR = static_cast<Uint8>(PaletteColor::BRIGHT_WHITE);
const auto BAR_COLOR = static_cast<Uint8>(PaletteColor::WHITE);
@@ -501,33 +692,30 @@ namespace Resource {
SDL_FRect rect_full = {.x = X_PADDING, .y = BAR_POSITION, .w = FULL_BAR_WIDTH, .h = BAR_HEIGHT};
surface->fillRect(&rect_full, BAR_COLOR);
// Mostra el nom del recurs (o missatge d'espera si ja ha acabat i wait_for_input=true)
const bool WAITING_FOR_INPUT = isLoadDone() && Options::loading.wait_for_input;
const std::string OVER_BAR_TEXT = WAITING_FOR_INPUT ? "PRESS ANY KEY TO CONTINUE" : current_loading_name_;
if ((Options::loading.show_resource_name || WAITING_FOR_INPUT) && !OVER_BAR_TEXT.empty()) {
const float TEXT_Y = BAR_POSITION - static_cast<float>(TEXT_HEIGHT) - 2.0F;
loading_text_->writeColored(
CENTER_X - (loading_text_->length(OVER_BAR_TEXT) / 2),
static_cast<int>(TEXT_Y),
OVER_BAR_TEXT,
LOADING_TEXT_COLOR);
}
Screen::get()->render();
}
// Comprueba los eventos de la pantalla de carga
void Cache::checkEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_EVENT_QUIT:
exit(0);
break;
case SDL_EVENT_KEY_DOWN:
if (event.key.key == SDLK_ESCAPE) {
exit(0);
}
break;
}
}
// Guarda el nombre del recurso que se está a punto de cargar. El repintado
// lo hace el BootLoader (una vez por frame) — aquí solo se actualiza el estado.
void Cache::setCurrentLoading(const std::string& name) {
current_loading_name_ = name;
}
// Actualiza el progreso de carga
void Cache::updateLoadingProgress(int steps) {
// Incrementa el contador de recursos cargados
void Cache::updateLoadingProgress() {
count_.add(1);
if (count_.loaded % steps == 0 || count_.loaded == count_.total) {
renderProgress();
}
checkEvents();
}
} // namespace Resource

View File

@@ -25,7 +25,13 @@ namespace Resource {
auto getRoom(const std::string& name) -> std::shared_ptr<Room::Data>;
auto getRooms() -> std::vector<RoomResource>&;
void reload(); // Recarga todos los recursos
// --- Incremental loading (Director drives this from iterate()) ---
void beginLoad(); // Prepara el estado del loader incremental
auto loadStep(int budget_ms) -> bool; // Carga assets durante budget_ms; devuelve true si ha terminado
void renderProgress(); // Dibuja la barra de progreso (usada por BootLoader)
[[nodiscard]] auto isLoadDone() const -> bool;
void reload(); // Recarga todos los recursos (síncrono, usado en hot-reload de debug)
#ifdef _DEBUG
void reloadRoom(const std::string& name); // Recarga una habitación desde disco
#endif
@@ -47,7 +53,21 @@ namespace Resource {
}
};
// Métodos de carga de recursos
// Etapas del loader incremental
enum class LoadStage {
SOUNDS,
MUSICS,
SURFACES,
SURFACES_POST, // Ajuste de transparent colors tras cargar todas las surfaces
PALETTES,
TEXT_FILES,
ANIMATIONS,
ROOMS,
TEXTS,
DONE
};
// Métodos de carga de recursos (bucle completo, usados por reload() síncrono)
void loadSounds();
void loadMusics();
void loadSurfaces();
@@ -57,17 +77,27 @@ namespace Resource {
void loadRooms();
void createText();
// Helpers incrementales: cargan un único asset de la categoría correspondiente
void loadOneSound(size_t index);
void loadOneMusic(size_t index);
void loadOneSurface(size_t index);
void finalizeSurfaces(); // Ajuste de transparent colors tras cargar surfaces
void loadOnePalette(size_t index);
void loadOneTextFile(size_t index);
void loadOneAnimation(size_t index);
void loadOneRoom(size_t index);
void createOneText(size_t index);
// Métodos de limpieza
void clear();
void clearSounds();
void clearMusics();
// Métodos de gestión de carga
void load();
void load(); // Carga completa síncrona (usado solo por reload())
void calculateTotal();
void renderProgress();
static void checkEvents();
void updateLoadingProgress(int steps = 5);
void updateLoadingProgress();
void setCurrentLoading(const std::string& name); // Desa el nom del recurs en curs
// Helper para mensajes de error de carga
[[noreturn]] static void throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e);
@@ -91,6 +121,11 @@ namespace Resource {
ResourceCount count_{}; // Contador de recursos
std::shared_ptr<Text> loading_text_; // Texto para la pantalla de carga
std::string current_loading_name_; // Nom del recurs que s'està a punt de carregar
// Estado del loader incremental
LoadStage stage_{LoadStage::DONE}; // Arranca en DONE hasta que beginLoad() lo cambie
size_t stage_index_{0}; // Cursor dentro de la categoría actual
};
} // namespace Resource

View File

@@ -23,6 +23,7 @@
#include "game/gameplay/cheevos.hpp" // Para Cheevos
#include "game/options.hpp" // Para Options, options, OptionsVideo
#include "game/scene_manager.hpp" // Para SceneManager
#include "game/scenes/boot_loader.hpp" // Para BootLoader
#include "game/scenes/credits.hpp" // Para Credits
#include "game/scenes/ending.hpp" // Para Ending
#include "game/scenes/ending2.hpp" // Para Ending2
@@ -40,7 +41,7 @@
#include "game/editor/map_editor.hpp" // Para MapEditor
#endif
#ifndef _WIN32
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
#include <pwd.h>
#endif
@@ -48,12 +49,17 @@
Director::Director() {
std::cout << "Game start" << '\n';
#ifdef __EMSCRIPTEN__
// En Emscripten els assets estan al root del filesystem virtual (/data, /config)
executable_path_ = "";
#else
// Obtiene la ruta del ejecutable
std::string base = SDL_GetBasePath();
if (!base.empty() && base.back() == '/') {
base.pop_back();
}
executable_path_ = base;
#endif
// Crea la carpeta del sistema donde guardar datos
createSystemFolder("jailgames");
@@ -83,7 +89,7 @@ Director::Director() {
// Preparar ruta al pack (en macOS bundle está en Contents/Resources/)
std::string pack_path = executable_path_ + PREFIX + "/resources.pack";
#ifdef RELEASE_BUILD
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
// ============================================================
// RELEASE BUILD: Pack-first architecture
// ============================================================
@@ -141,6 +147,18 @@ Director::Director() {
Options::setConfigFile(Resource::List::get()->get("config.yaml")); // NOLINT(readability-static-accessed-through-instance)
Options::loadFromFile();
#ifdef __EMSCRIPTEN__
// A la versió web el navegador gestiona la finestra: forcem zoom x3
// perquè la textura 256x192 no es vegi minúscula al canvas HTML,
// i desactivem el borde per aprofitar al màxim l'espai del canvas.
Options::video.fullscreen = false;
Options::video.integer_scale = true;
Options::window.zoom = 4;
Options::video.border.enabled = true;
Options::video.border.height = 8;
Options::video.border.width = 8;
#endif
// Configura la ruta y carga los presets de PostFX
Options::setPostFXFile(Resource::List::get()->get("postfx.yaml")); // NOLINT(readability-static-accessed-through-instance)
Options::loadPostFXFromFile();
@@ -160,15 +178,15 @@ Director::Director() {
// Crea los objetos
Screen::init();
// Initialize resources (works for both release and development)
// Inicializa el singleton del cache sin disparar la carga. La carga real
// la hace Director::iterate() llamando a Cache::loadStep() en cada frame,
// de forma que la ventana, los eventos y la barra de progreso están vivos
// desde el primer tick.
Resource::Cache::init();
Notifier::init("", "8bithud");
RenderInfo::init();
Console::init("8bithud");
Screen::get()->setNotificationsEnabled(true);
Resource::Cache::get()->beginLoad();
// Special handling for gamecontrollerdb.txt - SDL needs filesystem path
#ifdef RELEASE_BUILD
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
// In release, construct the path manually (not from Asset which has empty executable_path)
std::string gamecontroller_db = executable_path_ + PREFIX + "/gamecontrollerdb.txt";
Input::init(gamecontroller_db);
@@ -181,18 +199,41 @@ Director::Director() {
Input::get()->applyKeyboardBindingsFromOptions();
Input::get()->applyGamepadBindingsFromOptions();
std::cout << "\n"; // Fin de inicialización mínima de sistemas
// Construeix l'escena inicial (BootLoader). finishBoot() la canviarà a
// LOGO (o la que digui Debug) quan Cache::loadStep() complete la càrrega.
SceneManager::current = SceneManager::Scene::BOOT_LOADER;
switchToActiveScene();
}
// Inicialitzacions que depenen del cache poblat. Es crida des d'iterate()
// just quan Cache::loadStep() retorna true, amb la finestra i el bucle ja vius.
void Director::finishBoot() {
Notifier::init("", "8bithud");
RenderInfo::init();
Console::init("8bithud");
Screen::get()->setNotificationsEnabled(true);
#ifdef _DEBUG
Debug::init();
#ifdef __EMSCRIPTEN__
// A wasm el debug.yaml viu a SYSTEM_FOLDER (MEMFS no persistent) i no està
// disponible. Saltem el loadFromFile i entrem directament a la GAME.
SceneManager::current = SceneManager::Scene::GAME;
#else
Debug::get()->setDebugFile(Resource::List::get()->get("debug.yaml"));
Debug::get()->loadFromFile();
SceneManager::current = Debug::get()->getInitialScene();
#endif
MapEditor::init();
#else
// En release, pasamos a LOGO siempre tras la carga
SceneManager::current = SceneManager::Scene::LOGO;
#endif
std::cout << "\n"; // Fin de inicialización de sistemas
// Inicializa el sistema de localización (antes de Cheevos que usa textos traducidos)
#ifdef RELEASE_BUILD
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
{
// En release el locale está en el pack, no en el filesystem
std::string locale_key = Resource::List::get()->get(Options::language + ".yaml"); // NOLINT(readability-static-accessed-through-instance)
@@ -205,7 +246,7 @@ Director::Director() {
#endif
// Special handling for cheevos.bin - also needs filesystem path
#ifdef RELEASE_BUILD
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
std::string cheevos_path = system_folder_ + "/cheevos.bin";
Cheevos::init(cheevos_path);
#else
@@ -217,6 +258,11 @@ Director::~Director() {
// Guarda las opciones a un fichero
Options::saveToFile();
// Destruir l'escena activa ABANS dels singletons. Si no, el unique_ptr membre
// destrueix l'escena al final del destructor — quan Audio, Screen, Resource...
// ja són morts — i qualsevol accés en els seus destructors és un UAF.
active_scene_.reset();
// Destruye los singletones
Cheevos::destroy();
Locale::destroy();
@@ -241,6 +287,12 @@ Director::~Director() {
// Crea la carpeta del sistema donde guardar datos
void Director::createSystemFolder(const std::string& folder) { // NOLINT(readability-convert-member-functions-to-static)
#ifdef __EMSCRIPTEN__
// En Emscripten utilitzem MEMFS (no persistent entre sessions).
// No cal crear directoris: MEMFS els crea automàticament en escriure-hi.
system_folder_ = "/config/" + folder;
return;
#else
#ifdef _WIN32
system_folder_ = std::string(getenv("APPDATA")) + "/" + folder;
#elif __APPLE__
@@ -292,6 +344,7 @@ void Director::createSystemFolder(const std::string& folder) { // NOLINT(readab
}
}
}
#endif // __EMSCRIPTEN__
}
// Carga la configuración de assets desde assets.yaml
@@ -311,118 +364,132 @@ void Director::setFileList() { // NOLINT(readability-convert-member-functions-t
Resource::List::get()->loadFromFile(config_path, PREFIX, system_folder_);
}
// Ejecuta la seccion de juego con el logo
void Director::runLogo() {
auto logo = std::make_unique<Logo>();
logo->run();
// Construeix l'escena segons SceneManager::current i la deixa en active_scene_.
// Substitueix els vells runLogo(), runTitle(), runGame(), etc.
void Director::switchToActiveScene() {
// Si la escena anterior va demanar RESTART_CURRENT, restaurem la que estava activa
if (SceneManager::current == SceneManager::Scene::RESTART_CURRENT) {
SceneManager::current = SceneManager::scene_before_restart;
}
// Destrueix l'escena anterior (pot parar música, etc. al seu destructor)
active_scene_.reset();
switch (SceneManager::current) {
case SceneManager::Scene::BOOT_LOADER:
active_scene_ = std::make_unique<BootLoader>();
break;
case SceneManager::Scene::LOGO:
active_scene_ = std::make_unique<Logo>();
break;
case SceneManager::Scene::LOADING_SCREEN:
active_scene_ = std::make_unique<LoadingScreen>();
break;
case SceneManager::Scene::TITLE:
active_scene_ = std::make_unique<Title>();
break;
case SceneManager::Scene::CREDITS:
active_scene_ = std::make_unique<Credits>();
break;
case SceneManager::Scene::DEMO:
active_scene_ = std::make_unique<Game>(Game::Mode::DEMO);
break;
case SceneManager::Scene::GAME:
Audio::get()->stopMusic();
active_scene_ = std::make_unique<Game>(Game::Mode::GAME);
break;
case SceneManager::Scene::GAME_OVER:
active_scene_ = std::make_unique<GameOver>();
break;
case SceneManager::Scene::ENDING:
active_scene_ = std::make_unique<Ending>();
break;
case SceneManager::Scene::ENDING2:
active_scene_ = std::make_unique<Ending2>();
break;
default:
break;
}
current_scene_ = SceneManager::current;
}
// Ejecuta la seccion de juego de la pantalla de carga
void Director::runLoadingScreen() {
auto loading_screen = std::make_unique<LoadingScreen>();
loading_screen->run();
}
// SDL_AppIterate: executa un frame de l'escena activa
auto Director::iterate() -> SDL_AppResult {
if (SceneManager::current == SceneManager::Scene::QUIT) {
return SDL_APP_SUCCESS;
}
// Ejecuta la seccion de juego con el titulo y los menus
void Director::runTitle() {
auto title = std::make_unique<Title>();
title->run();
}
// Ejecuta la seccion de los creditos del juego
void Director::runCredits() {
auto credits = std::make_unique<Credits>();
credits->run();
}
// Ejecuta la seccion de la demo, donde se ven pantallas del juego
void Director::runDemo() {
auto game = std::make_unique<Game>(Game::Mode::DEMO);
game->run();
}
// Ejecuta la seccion del final del juego
void Director::runEnding() {
auto ending = std::make_unique<Ending>();
ending->run();
}
// Ejecuta la seccion del final del juego
void Director::runEnding2() {
auto ending2 = std::make_unique<Ending2>();
ending2->run();
}
// Ejecuta la seccion del final de la partida
void Director::runGameOver() {
auto game_over = std::make_unique<GameOver>();
game_over->run();
}
// Ejecuta la seccion de juego donde se juega
void Director::runGame() {
Audio::get()->stopMusic();
auto game = std::make_unique<Game>(Game::Mode::GAME);
game->run();
}
auto Director::run() -> int {
// Bucle principal
while (SceneManager::current != SceneManager::Scene::QUIT) {
const SceneManager::Scene ACTIVE = SceneManager::current;
switch (SceneManager::current) {
case SceneManager::Scene::LOGO:
runLogo();
break;
case SceneManager::Scene::LOADING_SCREEN:
runLoadingScreen();
break;
case SceneManager::Scene::TITLE:
runTitle();
break;
case SceneManager::Scene::CREDITS:
runCredits();
break;
case SceneManager::Scene::DEMO:
runDemo();
break;
case SceneManager::Scene::GAME:
runGame();
break;
case SceneManager::Scene::GAME_OVER:
runGameOver();
break;
case SceneManager::Scene::ENDING:
runEnding();
break;
case SceneManager::Scene::ENDING2:
runEnding2();
break;
case SceneManager::Scene::RESTART_CURRENT:
// La escena salió por RESTART_CURRENT → relanzar la escena guardada
SceneManager::current = SceneManager::scene_before_restart;
break;
default:
break;
}
// Si la escena que acaba de correr dejó RESTART_CURRENT pendiente,
// restaurar la escena que estaba activa para relanzarla en la próxima iteración
if (SceneManager::current == SceneManager::Scene::RESTART_CURRENT) {
SceneManager::current = ACTIVE;
// Fase de boot: anem cridant loadStep() fins que el cache estiga ple.
// Durant aquesta fase l'escena activa és BootLoader (una barra de progrés).
if (boot_loading_) {
try {
// Budget de 50ms: durant el boot el joc va a ~15-20 FPS, suficient
// per veure la barra avançar suau i processar events del WM/ESC,
// i evita el 50% d'ineficiència que provocaria un budget < vsync.
if (Resource::Cache::get()->loadStep(50 /*ms*/)) {
if (Options::loading.show && Options::loading.wait_for_input) {
boot_waiting_for_input_ = true; // Esperar tecla antes de continuar
} else {
finishBoot();
}
boot_loading_ = false;
// finishBoot() ja ha fixat SceneManager::current a LOGO (o la que
// digui Debug). El canvi d'escena es fa just a sota.
}
} catch (const std::exception& e) {
std::cerr << "Fatal error during resource load: " << e.what() << '\n';
SceneManager::current = SceneManager::Scene::QUIT;
return SDL_APP_FAILURE;
}
}
return 0;
// Si l'escena ha canviat (o s'ha demanat RESTART_CURRENT), canviar-la abans del frame
if (SceneManager::current != current_scene_ || SceneManager::current == SceneManager::Scene::RESTART_CURRENT) {
switchToActiveScene();
}
if (active_scene_) {
active_scene_->iterate();
}
return SDL_APP_CONTINUE;
}
// SDL_AppEvent: despatxa un event a l'escena activa
auto Director::handleEvent(const SDL_Event& event) -> SDL_AppResult {
#ifndef __EMSCRIPTEN__
// A la versió web no tenim event de quit del navegador
if (event.type == SDL_EVENT_QUIT) {
SceneManager::current = SceneManager::Scene::QUIT;
return SDL_APP_SUCCESS;
}
#endif
// Si estamos esperando input tras la carga: consumir tecla/botón y arrancar
if (boot_waiting_for_input_) {
const bool IS_KEY = event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat;
const bool IS_BUTTON = event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN;
if (IS_KEY || IS_BUTTON) {
boot_waiting_for_input_ = false;
finishBoot();
}
return SDL_APP_CONTINUE;
}
if (active_scene_) {
active_scene_->handleEvent(event);
}
return SDL_APP_CONTINUE;
}

View File

@@ -2,29 +2,34 @@
#include <SDL3/SDL.h>
#include <memory> // Para unique_ptr
#include <string> // Para string
#include "game/scene_manager.hpp" // Para SceneManager::Scene
#include "game/scenes/scene.hpp" // Para Scene base
class Director {
public:
Director(); // Constructor
~Director(); // Destructor
static auto run() -> int; // Bucle principal
Director(); // Constructor: inicialitza sistemes i crea l'escena inicial
~Director(); // Destructor
// SDL3 Callback API: un frame i un event
auto iterate() -> SDL_AppResult;
auto handleEvent(const SDL_Event& event) -> SDL_AppResult;
private:
// --- Variables ---
std::string executable_path_; // Path del ejecutable
std::string system_folder_; // Carpeta del sistema donde guardar datos
std::unique_ptr<Scene> active_scene_; // Escena activa
SceneManager::Scene current_scene_{SceneManager::Scene::BOOT_LOADER}; // Tipus d'escena activa
bool boot_loading_{true}; // True mientras Cache::loadStep() no haya acabado
bool boot_waiting_for_input_{false}; // True si la carga acabó y Options::loading.wait_for_input está activo
// --- Funciones ---
void createSystemFolder(const std::string& folder); // Crea la carpeta del sistema donde guardar datos
void setFileList(); // Carga la configuración de assets desde assets.yaml
static void runLogo(); // Ejecuta la seccion de juego con el logo
static void runLoadingScreen(); // Ejecuta la seccion de juego de la pantalla de carga
static void runTitle(); // Ejecuta la seccion de juego con el titulo y los menus
static void runCredits(); // Ejecuta la seccion de los creditos del juego
static void runDemo(); // Ejecuta la seccion de la demo, donde se ven pantallas del juego
static void runEnding(); // Ejecuta la seccion del final del juego
static void runEnding2(); // Ejecuta la seccion del final del juego
static void runGameOver(); // Ejecuta la seccion del final de la partida
static void runGame(); // Ejecuta la seccion de juego donde se juega
};
void switchToActiveScene(); // Construeix l'escena segons SceneManager::current
void finishBoot(); // Inits que dependen del cache, ejecutado tras loadStep==done
};

View File

@@ -1,23 +1,65 @@
#include "core/system/global_events.hpp"
#include "core/input/input.hpp" // Para Input (gamepad add/remove)
#include "core/input/mouse.hpp"
#include "core/locale/locale.hpp" // Para Locale
#include "game/options.hpp" // Para Options, options, OptionsGame, OptionsAudio
#include "game/scene_manager.hpp" // Para SceneManager
#include "game/scene_manager.hpp" // Para SceneManager::current (filtrar BACK a GAME)
#include "game/ui/console.hpp" // Para Console
#include "game/ui/notifier.hpp" // Para Notifier
namespace GlobalEvents {
// Comprueba los eventos que se pueden producir en cualquier sección del juego
void handle(const SDL_Event& event) {
// Evento de salida de la aplicación
if (event.type == SDL_EVENT_QUIT) {
SceneManager::current = SceneManager::Scene::QUIT;
return;
}
namespace {
// Flag per saber si en aquest frame s'ha rebut un button down del gamepad.
// El consumeix GlobalInputs perquè un botó del comandament salti escenes.
bool gamepad_button_pressed_ = false;
} // namespace
// Comprueba los eventos que se pueden producir en cualquier sección del juego.
// Nota: SDL_EVENT_QUIT el gestiona Director::handleEvent() directament.
void handle(const SDL_Event& event) {
if (event.type == SDL_EVENT_RENDER_DEVICE_RESET || event.type == SDL_EVENT_RENDER_TARGETS_RESET) {
// reLoadTextures();
}
// Connexió/desconnexió de gamepads: cal enrutar-los a Input perquè
// afegisca el dispositiu a gamepads_. Sense això, en wasm els gamepads
// mai es detecten (la Gamepad API del navegador només els exposa
// després que l'usuari els active, més tard que el discoverGamepads
// inicial). En desktop també arregla la connexió en calent.
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED) {
if (Input::get() != nullptr) {
std::string name = Input::get()->handleEvent(event);
if (!name.empty() && Notifier::get() != nullptr && Locale::get() != nullptr) {
const std::string KEY = (event.type == SDL_EVENT_GAMEPAD_ADDED)
? "ui.gamepad_connected"
: "ui.gamepad_disconnected";
Notifier::get()->show({name + " " + Locale::get()->get(KEY)});
}
}
}
// Marcar polsació de qualsevol botó del comandament (els consumirà GlobalInputs
// per saltar escenes d'attract mode). Queden exclosos els botons reservats a
// accions globals perquè el "any button → ACCEPT" no se'ls mengi abans que
// checkAction() els pugui enrutar:
// - BACK → EXIT (a emscripten només a l'escena GAME, ja que no pot tancar).
// - LEFT_SHOULDER / RIGHT_SHOULDER → NEXT_PALETTE / NEXT_PALETTE_SORT.
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
const auto BUTTON = event.gbutton.button;
const bool IS_BACK = (BUTTON == SDL_GAMEPAD_BUTTON_BACK);
const bool IS_SHOULDER = (BUTTON == SDL_GAMEPAD_BUTTON_LEFT_SHOULDER || BUTTON == SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER);
#ifdef __EMSCRIPTEN__
const bool RESERVE_BACK = IS_BACK && SceneManager::current == SceneManager::Scene::GAME;
#else
const bool RESERVE_BACK = IS_BACK;
#endif
if (!RESERVE_BACK && !IS_SHOULDER) {
gamepad_button_pressed_ = true;
}
}
// Enrutar eventos de texto a la consola cuando está activa
if (Console::get() != nullptr && Console::get()->isActive()) {
if (event.type == SDL_EVENT_TEXT_INPUT || event.type == SDL_EVENT_KEY_DOWN) {
@@ -28,4 +70,10 @@ namespace GlobalEvents {
Mouse::handleEvent(event);
}
auto consumeGamepadButtonPressed() -> bool {
const bool RESULT = gamepad_button_pressed_;
gamepad_button_pressed_ = false;
return RESULT;
}
} // namespace GlobalEvents

View File

@@ -5,4 +5,9 @@
namespace GlobalEvents {
// Comprueba los eventos que se pueden producir en cualquier sección del juego
void handle(const SDL_Event& event);
// True si en aquest frame s'ha rebut un SDL_EVENT_GAMEPAD_BUTTON_DOWN.
// Es consumeix (i es reseteja) per GlobalInputs::getPressedAction perquè
// qualsevol botó del comandament actuï com a "ACCEPT" (saltar escena).
auto consumeGamepadButtonPressed() -> bool;
} // namespace GlobalEvents

View File

@@ -1,4 +1,4 @@
// Ogg Vorbis audio decoder - v1.20 - public domain
// Ogg Vorbis audio decoder - v1.22 - public domain
// http://nothings.org/stb_vorbis/
//
// Original version written by Sean Barrett in 2007.
@@ -29,12 +29,15 @@
// Bernhard Wodo Evan Balster github:alxprd
// Tom Beaumont Ingo Leitgeb Nicolas Guillemot
// Phillip Bennefall Rohit Thiago Goulart
// github:manxorist saga musix github:infatum
// github:manxorist Saga Musix github:infatum
// Timur Gagiev Maxwell Koo Peter Waller
// github:audinowho Dougall Johnson David Reid
// github:Clownacy Pedro J. Estebanez Remi Verschelde
// AnthoFoxo github:morlat Gabriel Ravier
//
// Partial history:
// 1.22 - 2021-07-11 - various small fixes
// 1.21 - 2021-07-02 - fix bug for files with no comments
// 1.20 - 2020-07-11 - several small fixes
// 1.19 - 2020-02-05 - warnings
// 1.18 - 2020-02-02 - fix seek bugs; parse header comments; misc warnings etc.
@@ -220,6 +223,12 @@ extern int stb_vorbis_decode_frame_pushdata(
// channel. In other words, (*output)[0][0] contains the first sample from
// the first channel, and (*output)[1][0] contains the first sample from
// the second channel.
//
// *output points into stb_vorbis's internal output buffer storage; these
// buffers are owned by stb_vorbis and application code should not free
// them or modify their contents. They are transient and will be overwritten
// once you ask for more data to get decoded, so be sure to grab any data
// you need before then.
extern void stb_vorbis_flush_pushdata(stb_vorbis *f);
// inform stb_vorbis that your next datablock will not be contiguous with
@@ -579,7 +588,7 @@ enum STBVorbisError
#if defined(_MSC_VER) || defined(__MINGW32__)
#include <malloc.h>
#endif
#if defined(__linux__) || defined(__linux) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__)
#if defined(__linux__) || defined(__linux) || defined(__sun__) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__)
#include <alloca.h>
#endif
#else // STB_VORBIS_NO_CRT
@@ -646,6 +655,12 @@ typedef signed int int32;
typedef float codetype;
#ifdef _MSC_VER
#define STBV_NOTUSED(v) (void)(v)
#else
#define STBV_NOTUSED(v) (void)sizeof(v)
#endif
// @NOTE
//
// Some arrays below are tagged "//varies", which means it's actually
@@ -1046,7 +1061,7 @@ static float float32_unpack(uint32 x)
uint32 sign = x & 0x80000000;
uint32 exp = (x & 0x7fe00000) >> 21;
double res = sign ? -(double)mantissa : (double)mantissa;
return (float) ldexp((float)res, exp-788);
return (float) ldexp((float)res, (int)exp-788);
}
@@ -1077,6 +1092,7 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
// find the first entry
for (k=0; k < n; ++k) if (len[k] < NO_CODE) break;
if (k == n) { assert(c->sorted_entries == 0); return TRUE; }
assert(len[k] < 32); // no error return required, code reading lens checks this
// add to the list
add_entry(c, 0, k, m++, len[k], values);
// add all available leaves
@@ -1090,6 +1106,7 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
uint32 res;
int z = len[i], y;
if (z == NO_CODE) continue;
assert(z < 32); // no error return required, code reading lens checks this
// find lowest available leaf (should always be earliest,
// which is what the specification calls for)
// note that this property, and the fact we can never have
@@ -1099,12 +1116,10 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
while (z > 0 && !available[z]) --z;
if (z == 0) { return FALSE; }
res = available[z];
assert(z >= 0 && z < 32);
available[z] = 0;
add_entry(c, bit_reverse(res), i, m++, len[i], values);
// propagate availability up the tree
if (z != len[i]) {
assert(len[i] >= 0 && len[i] < 32);
for (y=len[i]; y > z; --y) {
assert(available[y] == 0);
available[y] = res + (1 << (32-y));
@@ -2577,34 +2592,33 @@ static void imdct_step3_inner_s_loop_ld654(int n, float *e, int i_off, float *A,
while (z > base) {
float k00,k11;
float l00,l11;
k00 = z[-0] - z[-8];
k11 = z[-1] - z[-9];
z[-0] = z[-0] + z[-8];
z[-1] = z[-1] + z[-9];
z[-8] = k00;
z[-9] = k11 ;
k00 = z[-0] - z[ -8];
k11 = z[-1] - z[ -9];
l00 = z[-2] - z[-10];
l11 = z[-3] - z[-11];
z[ -0] = z[-0] + z[ -8];
z[ -1] = z[-1] + z[ -9];
z[ -2] = z[-2] + z[-10];
z[ -3] = z[-3] + z[-11];
z[ -8] = k00;
z[ -9] = k11;
z[-10] = (l00+l11) * A2;
z[-11] = (l11-l00) * A2;
k00 = z[ -2] - z[-10];
k11 = z[ -3] - z[-11];
z[ -2] = z[ -2] + z[-10];
z[ -3] = z[ -3] + z[-11];
z[-10] = (k00+k11) * A2;
z[-11] = (k11-k00) * A2;
k00 = z[-12] - z[ -4]; // reverse to avoid a unary negation
k00 = z[ -4] - z[-12];
k11 = z[ -5] - z[-13];
l00 = z[ -6] - z[-14];
l11 = z[ -7] - z[-15];
z[ -4] = z[ -4] + z[-12];
z[ -5] = z[ -5] + z[-13];
z[-12] = k11;
z[-13] = k00;
k00 = z[-14] - z[ -6]; // reverse to avoid a unary negation
k11 = z[ -7] - z[-15];
z[ -6] = z[ -6] + z[-14];
z[ -7] = z[ -7] + z[-15];
z[-14] = (k00+k11) * A2;
z[-15] = (k00-k11) * A2;
z[-12] = k11;
z[-13] = -k00;
z[-14] = (l11-l00) * A2;
z[-15] = (l00+l11) * -A2;
iter_54(z);
iter_54(z-8);
@@ -3069,6 +3083,7 @@ static int do_floor(vorb *f, Mapping *map, int i, int n, float *target, YTYPE *f
for (q=1; q < g->values; ++q) {
j = g->sorted_order[q];
#ifndef STB_VORBIS_NO_DEFER_FLOOR
STBV_NOTUSED(step2_flag);
if (finalY[j] >= 0)
#else
if (step2_flag[j])
@@ -3171,6 +3186,7 @@ static int vorbis_decode_packet_rest(vorb *f, int *len, Mode *m, int left_start,
// WINDOWING
STBV_NOTUSED(left_end);
n = f->blocksize[m->blockflag];
map = &f->mapping[m->mapping];
@@ -3368,7 +3384,7 @@ static int vorbis_decode_packet_rest(vorb *f, int *len, Mode *m, int left_start,
// this isn't to spec, but spec would require us to read ahead
// and decode the size of all current frames--could be done,
// but presumably it's not a commonly used feature
f->current_loc = -n2; // start of first frame is positioned for discard
f->current_loc = 0u - n2; // start of first frame is positioned for discard (NB this is an intentional unsigned overflow/wrap-around)
// we might have to discard samples "from" the next frame too,
// if we're lapping a large block then a small at the start?
f->discard_samples_deferred = n - right_end;
@@ -3642,9 +3658,11 @@ static int start_decoder(vorb *f)
f->vendor[len] = (char)'\0';
//user comments
f->comment_list_length = get32_packet(f);
if (f->comment_list_length > 0) {
f->comment_list = (char**)setup_malloc(f, sizeof(char*) * (f->comment_list_length));
if (f->comment_list == NULL) return error(f, VORBIS_outofmem);
f->comment_list = NULL;
if (f->comment_list_length > 0)
{
f->comment_list = (char**) setup_malloc(f, sizeof(char*) * (f->comment_list_length));
if (f->comment_list == NULL) return error(f, VORBIS_outofmem);
}
for(i=0; i < f->comment_list_length; ++i) {
@@ -3867,8 +3885,7 @@ static int start_decoder(vorb *f)
unsigned int div=1;
for (k=0; k < c->dimensions; ++k) {
int off = (z / div) % c->lookup_values;
float val = mults[off];
val = mults[off]*c->delta_value + c->minimum_value + last;
float val = mults[off]*c->delta_value + c->minimum_value + last;
c->multiplicands[j*c->dimensions + k] = val;
if (c->sequence_p)
last = val;
@@ -3951,7 +3968,7 @@ static int start_decoder(vorb *f)
if (g->class_masterbooks[j] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
}
for (k=0; k < 1 << g->class_subclasses[j]; ++k) {
g->subclass_books[j][k] = get_bits(f,8)-1;
g->subclass_books[j][k] = (int16)get_bits(f,8)-1;
if (g->subclass_books[j][k] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
}
}
@@ -4509,6 +4526,7 @@ stb_vorbis *stb_vorbis_open_pushdata(
*error = VORBIS_need_more_data;
else
*error = p.error;
vorbis_deinit(&p);
return NULL;
}
f = vorbis_alloc(&p);
@@ -4566,7 +4584,7 @@ static uint32 vorbis_find_page(stb_vorbis *f, uint32 *end, uint32 *last)
header[i] = get8(f);
if (f->eof) return 0;
if (header[4] != 0) goto invalid;
goal = header[22] + (header[23] << 8) + (header[24]<<16) + (header[25]<<24);
goal = header[22] + (header[23] << 8) + (header[24]<<16) + ((uint32)header[25]<<24);
for (i=22; i < 26; ++i)
header[i] = 0;
crc = 0;
@@ -4970,7 +4988,7 @@ unsigned int stb_vorbis_stream_length_in_samples(stb_vorbis *f)
// set. whoops!
break;
}
previous_safe = last_page_loc+1;
//previous_safe = last_page_loc+1; // NOTE: not used after this point, but note for debugging
last_page_loc = stb_vorbis_get_file_offset(f);
}
@@ -5081,7 +5099,10 @@ stb_vorbis * stb_vorbis_open_filename(const char *filename, int *error, const st
stb_vorbis * stb_vorbis_open_memory(const unsigned char *data, int len, int *error, const stb_vorbis_alloc *alloc)
{
stb_vorbis *f, p;
if (data == NULL) return NULL;
if (!data) {
if (error) *error = VORBIS_unexpected_eof;
return NULL;
}
vorbis_init(&p, alloc);
p.stream = (uint8 *) data;
p.stream_end = (uint8 *) data + len;
@@ -5156,11 +5177,11 @@ static void copy_samples(short *dest, float *src, int len)
static void compute_samples(int mask, short *output, int num_c, float **data, int d_offset, int len)
{
#define BUFFER_SIZE 32
float buffer[BUFFER_SIZE];
int i,j,o,n = BUFFER_SIZE;
#define STB_BUFFER_SIZE 32
float buffer[STB_BUFFER_SIZE];
int i,j,o,n = STB_BUFFER_SIZE;
check_endianness();
for (o = 0; o < len; o += BUFFER_SIZE) {
for (o = 0; o < len; o += STB_BUFFER_SIZE) {
memset(buffer, 0, sizeof(buffer));
if (o + n > len) n = len - o;
for (j=0; j < num_c; ++j) {
@@ -5177,16 +5198,17 @@ static void compute_samples(int mask, short *output, int num_c, float **data, in
output[o+i] = v;
}
}
#undef STB_BUFFER_SIZE
}
static void compute_stereo_samples(short *output, int num_c, float **data, int d_offset, int len)
{
#define BUFFER_SIZE 32
float buffer[BUFFER_SIZE];
int i,j,o,n = BUFFER_SIZE >> 1;
#define STB_BUFFER_SIZE 32
float buffer[STB_BUFFER_SIZE];
int i,j,o,n = STB_BUFFER_SIZE >> 1;
// o is the offset in the source data
check_endianness();
for (o = 0; o < len; o += BUFFER_SIZE >> 1) {
for (o = 0; o < len; o += STB_BUFFER_SIZE >> 1) {
// o2 is the offset in the output data
int o2 = o << 1;
memset(buffer, 0, sizeof(buffer));
@@ -5216,6 +5238,7 @@ static void compute_stereo_samples(short *output, int num_c, float **data, int d
output[o2+i] = v;
}
}
#undef STB_BUFFER_SIZE
}
static void convert_samples_short(int buf_c, short **buffer, int b_offset, int data_c, float **data, int d_offset, int samples)
@@ -5288,8 +5311,6 @@ int stb_vorbis_get_samples_short_interleaved(stb_vorbis *f, int channels, short
float **outputs;
int len = num_shorts / channels;
int n=0;
int z = f->channels;
if (z > channels) z = channels;
while (n < len) {
int k = f->channel_buffer_end - f->channel_buffer_start;
if (n+k >= len) k = len - n;
@@ -5308,8 +5329,6 @@ int stb_vorbis_get_samples_short(stb_vorbis *f, int channels, short **buffer, in
{
float **outputs;
int n=0;
int z = f->channels;
if (z > channels) z = channels;
while (n < len) {
int k = f->channel_buffer_end - f->channel_buffer_start;
if (n+k >= len) k = len - n;

View File

@@ -89,6 +89,12 @@ namespace Defaults::Localization {
constexpr const char* LANGUAGE = "ca"; // Idioma por defecto (en = inglés, ca = catalán)
} // namespace Defaults::Localization
namespace Defaults::Loading {
constexpr bool SHOW = false; // No mostrar la pantalla de carga por defecto
constexpr bool SHOW_RESOURCE_NAME = true; // Mostrar el nombre del recurso por defecto
constexpr bool WAIT_FOR_INPUT = false; // No esperar tecla al terminar por defecto
} // namespace Defaults::Loading
namespace Defaults::Game::Items {
constexpr const float PERCENT_TO_OPEN_THE_JAIL = 0.9F; // Porcentaje de items necesarios para abrir la jail
} // namespace Defaults::Game::Items

View File

@@ -39,9 +39,9 @@ void MiniMap::buildTileColorTable(const std::string& tileset_name) {
auto tileset = Resource::Cache::get()->getSurface(tileset_name);
if (!tileset) { return; }
tileset_width_ = static_cast<int>(tileset->getWidth()) / Tile::SIZE;
tileset_width_ = tileset->getWidth() / Tile::SIZE;
tileset_transparent_ = tileset->getTransparentColor();
int tileset_height = static_cast<int>(tileset->getHeight()) / Tile::SIZE;
int tileset_height = tileset->getHeight() / Tile::SIZE;
int total_tiles = tileset_width_ * tileset_height;
tile_colors_.resize(total_tiles, 0);

View File

@@ -26,8 +26,8 @@ void TilePicker::open(const std::string& tileset_name, int current_tile, int bg_
// Calcular dimensiones del tileset en tiles (teniendo en cuenta spacing de entrada)
int src_cell = Tile::SIZE + spacing_in_;
tileset_width_ = static_cast<int>(tileset_->getWidth()) / src_cell;
tileset_height_ = static_cast<int>(tileset_->getHeight()) / src_cell;
tileset_width_ = tileset_->getWidth() / src_cell;
tileset_height_ = tileset_->getHeight() / src_cell;
// Corregir si el último tile cabe sin spacing
if (tileset_width_ == 0 && tileset_->getWidth() >= Tile::SIZE) { tileset_width_ = 1; }
if (tileset_height_ == 0 && tileset_->getHeight() >= Tile::SIZE) { tileset_height_ = 1; }

View File

@@ -623,6 +623,27 @@ namespace Options {
}
}
// Carga opciones de la pantalla de carga desde YAML
void loadLoadingFromYaml(const fkyaml::node& yaml) {
if (!yaml.contains("loading")) { return; }
const auto& ld = yaml["loading"];
if (ld.contains("show")) {
try {
loading.show = ld["show"].get_value<bool>();
} catch (...) { loading.show = Defaults::Loading::SHOW; }
}
if (ld.contains("show_resource_name")) {
try {
loading.show_resource_name = ld["show_resource_name"].get_value<bool>();
} catch (...) { loading.show_resource_name = Defaults::Loading::SHOW_RESOURCE_NAME; }
}
if (ld.contains("wait_for_input")) {
try {
loading.wait_for_input = ld["wait_for_input"].get_value<bool>();
} catch (...) { loading.wait_for_input = Defaults::Loading::WAIT_FOR_INPUT; }
}
}
// Establece la ruta del fichero de configuración
void setConfigFile(const std::string& path) {
config_file_path = path;
@@ -673,6 +694,7 @@ namespace Options {
loadPlayerConfigFromYaml(yaml);
loadKioskConfigFromYaml(yaml);
loadLocalizationFromYaml(yaml);
loadLoadingFromYaml(yaml);
std::cout << "Config file loaded successfully\n\n";
@@ -810,6 +832,14 @@ namespace Options {
file << "localization:\n";
file << " language: \"" << language << "\"\n";
// LOADING SCREEN
file << "\n";
file << "# LOADING SCREEN\n";
file << "loading:\n";
file << " show: " << (loading.show ? "true" : "false") << "\n";
file << " show_resource_name: " << (loading.show_resource_name ? "true" : "false") << "\n";
file << " wait_for_input: " << (loading.wait_for_input ? "true" : "false") << " # solo si show=true\n";
file.close();
std::cout << "Config file saved successfully\n\n";

View File

@@ -134,6 +134,13 @@ namespace Options {
float volume{Defaults::Audio::VOLUME}; // Volumen al que suenan el audio (0-128 internamente)
};
// Estructura para las opciones de la pantalla de carga
struct Loading {
bool show{Defaults::Loading::SHOW}; // Muestra la pantalla de carga (si no, pantalla en negro)
bool show_resource_name{Defaults::Loading::SHOW_RESOURCE_NAME}; // Muestra el nombre del recurso sobre la barra de progreso
bool wait_for_input{Defaults::Loading::WAIT_FOR_INPUT}; // Al terminar la carga, espera tecla antes de continuar (solo si show=true)
};
// Estructura para las opciones de juego
struct Game {
float width{Defaults::Canvas::WIDTH}; // Ancho de la resolucion del juego
@@ -182,6 +189,7 @@ namespace Options {
inline Stats stats{}; // Datos con las estadisticas de juego
inline Window window{}; // Opciones relativas a la ventana
inline Audio audio{}; // Opciones relativas al audio
inline Loading loading{}; // Opciones de la pantalla de carga
inline KeyboardControls keyboard_controls{}; // Teclas usadas para jugar
inline GamepadControls gamepad_controls{}; // Botones del gamepad usados para jugar
inline Kiosk kiosk{}; // Opciones del modo kiosko

View File

@@ -11,6 +11,7 @@ namespace SceneManager {
// --- Escenas del programa ---
enum class Scene {
BOOT_LOADER, // Carga inicial de recursos dirigida por iterate()
LOGO, // Pantalla del logo
LOADING_SCREEN, // Pantalla de carga
TITLE, // Pantalla de título/menú principal
@@ -34,7 +35,7 @@ namespace SceneManager {
};
// --- Variables de estado globales ---
inline Scene current = Scene::LOGO; // Escena actual (en _DEBUG sobrescrito por Director tras cargar debug.yaml)
inline Scene current = Scene::BOOT_LOADER; // Arranca siempre cargando recursos; Director conmuta a LOGO al terminar
inline Options options = Options::LOGO_TO_LOADING_SCREEN; // Opciones de la escena actual
inline Scene scene_before_restart = Scene::LOGO; // escena a relanzar tras RESTART_CURRENT

View File

@@ -0,0 +1,14 @@
#include "game/scenes/boot_loader.hpp"
#include "core/resources/resource_cache.hpp"
#include "game/scene_manager.hpp"
void BootLoader::iterate() {
Resource::Cache::get()->renderProgress();
}
void BootLoader::handleEvent(const SDL_Event& event) {
if (event.type == SDL_EVENT_QUIT) {
SceneManager::current = SceneManager::Scene::QUIT;
}
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include <SDL3/SDL.h>
#include "game/scenes/scene.hpp"
// Escena mínima que Director usa mientras el cache se carga incrementalmente.
// No avanza la carga — lo hace Director::iterate() llamando a Cache::loadStep()
// antes de despachar la escena. Aquí solo se pinta la barra de progreso.
class BootLoader : public Scene {
public:
BootLoader() = default;
~BootLoader() override = default;
void iterate() override;
void handleEvent(const SDL_Event& event) override;
};

View File

@@ -37,12 +37,9 @@ Credits::Credits()
Audio::get()->playMusic("title.ogg"); // Inicia la musica
}
// Comprueba el manejador de eventos
void Credits::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
// Despatx d'un event (SDL3 Callback API)
void Credits::handleEvent(const SDL_Event& event) {
GlobalEvents::handle(event);
}
// Comprueba las entradas
@@ -124,8 +121,7 @@ void Credits::update() {
const float DELTA_TIME = delta_timer_->tick();
total_time_ += DELTA_TIME; // Actualiza el tiempo total
handleEvents(); // Comprueba los eventos
handleInput(); // Comprueba las entradas
handleInput(); // Comprueba las entradas
updateState(DELTA_TIME); // Actualiza la máquina de estados
@@ -238,10 +234,8 @@ void Credits::render() {
Screen::get()->render();
}
// Bucle para el logo del juego
void Credits::run() {
while (SceneManager::current == SceneManager::Scene::CREDITS) {
update();
render();
}
// Un frame de l'escena (SDL3 Callback API)
void Credits::iterate() {
update();
render();
}

View File

@@ -2,22 +2,25 @@
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
class AnimatedSprite; // lines 11-11
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "game/scenes/scene.hpp" // Para Scene
class AnimatedSprite; // lines 11-11
class Surface;
class PixelReveal;
class DeltaTimer;
class Credits {
class Credits : public Scene {
public:
// --- Constructor y Destructor ---
Credits();
~Credits(); // NOLINT(modernize-use-equals-default, performance-trivially-destructible) -- defined in .cpp for unique_ptr with forward declarations
~Credits() override; // NOLINT(modernize-use-equals-default, performance-trivially-destructible) -- defined in .cpp for unique_ptr with forward declarations
// --- Bucle principal ---
void run();
// --- Bucle principal (SDL3 Callback API) ---
void iterate() override;
void handleEvent(const SDL_Event& event) override;
private:
// --- Tipos anidados ---
@@ -55,7 +58,6 @@ class Credits {
// --- Métodos privados ---
void update(); // Actualiza las variables
void render(); // Dibuja en pantalla
static void handleEvents(); // Comprueba el manejador de eventos
static void handleInput(); // Comprueba las entradas
void updateState(float delta_time); // Actualiza la máquina de estados
void transitionToState(State new_state); // Transición entre estados

View File

@@ -19,7 +19,9 @@
#include "utils/utils.hpp" // Para PaletteColor
// Destructor
Ending::~Ending() = default;
Ending::~Ending() {
Audio::get()->stopMusic();
}
// Constructor
Ending::Ending()
@@ -32,6 +34,7 @@ Ending::Ending()
iniScenes(); // Inicializa las escenas
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK)); // Cambia el color del borde
Audio::get()->playMusic("ending1.ogg");
}
// Actualiza el objeto
@@ -39,8 +42,7 @@ void Ending::update() {
const float DELTA_TIME = delta_timer_->tick();
total_time_ += DELTA_TIME; // Actualiza el tiempo total
handleEvents(); // Comprueba los eventos
handleInput(); // Comprueba las entradas
handleInput(); // Comprueba las entradas
updateState(DELTA_TIME); // Actualiza la máquina de estados
updateSpriteCovers(); // Actualiza las cortinillas de los elementos
@@ -86,12 +88,9 @@ void Ending::render() {
Screen::get()->render();
}
// Comprueba el manejador de eventos
void Ending::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
// Despatx d'un event (SDL3 Callback API)
void Ending::handleEvent(const SDL_Event& event) {
GlobalEvents::handle(event);
}
// Comprueba las entradas
@@ -355,16 +354,10 @@ void Ending::iniScenes() { // NOLINT(readability-convert-member-functions-to-st
scenes_.push_back(sc);
}
// Bucle principal
void Ending::run() {
Audio::get()->playMusic("ending1.ogg");
while (SceneManager::current == SceneManager::Scene::ENDING) {
update();
render();
}
Audio::get()->stopMusic();
// Un frame de l'escena (SDL3 Callback API)
void Ending::iterate() {
update();
render();
}
// Actualiza las cortinillas de los elementos

View File

@@ -5,19 +5,22 @@
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
class Sprite; // lines 8-8
class Surface; // lines 9-9
#include "game/scenes/scene.hpp" // Para Scene
class Sprite; // lines 8-8
class Surface; // lines 9-9
class PixelReveal;
class DeltaTimer;
class Ending {
class Ending : public Scene {
public:
// --- Constructor y Destructor ---
Ending();
~Ending(); // NOLINT(modernize-use-equals-default, performance-trivially-destructible) -- defined in .cpp for unique_ptr with forward declarations
~Ending() override; // NOLINT(modernize-use-equals-default, performance-trivially-destructible) -- defined in .cpp for unique_ptr with forward declarations
// --- Bucle principal ---
void run();
// --- Bucle principal (SDL3 Callback API) ---
void iterate() override;
void handleEvent(const SDL_Event& event) override;
private:
// --- Enumeraciones ---
@@ -77,7 +80,6 @@ class Ending {
// --- Métodos ---
void update(); // Actualiza el objeto
void render(); // Dibuja el final en pantalla
static void handleEvents(); // Comprueba el manejador de eventos
static void handleInput(); // Comprueba las entradas
void iniTexts(); // Inicializa los textos
void iniPics(); // Inicializa las imágenes

View File

@@ -41,14 +41,20 @@ Ending2::Ending2()
placeSprites(); // Coloca los sprites en su sito
createSpriteTexts(); // Crea los sprites con las texturas con los textos
createTexts(); // Crea los sprites con las texturas con los textos del final
Audio::get()->playMusic("ending2.ogg");
}
// Destructor
Ending2::~Ending2() {
Audio::get()->stopMusic();
}
// Actualiza el objeto
void Ending2::update() {
const float DELTA_TIME = delta_timer_->tick();
handleEvents(); // Comprueba los eventos
handleInput(); // Comprueba las entradas
handleInput(); // Comprueba las entradas
updateState(DELTA_TIME); // Actualiza el estado
@@ -95,12 +101,9 @@ void Ending2::render() {
Screen::get()->render();
}
// Comprueba el manejador de eventos
void Ending2::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
// Despatx d'un event (SDL3 Callback API)
void Ending2::handleEvent(const SDL_Event& event) {
GlobalEvents::handle(event);
}
// Comprueba las entradas
@@ -109,16 +112,10 @@ void Ending2::handleInput() {
GlobalInputs::handle();
}
// Bucle principal
void Ending2::run() {
Audio::get()->playMusic("ending2.ogg");
while (SceneManager::current == SceneManager::Scene::ENDING2) {
update();
render();
}
Audio::get()->stopMusic();
// Un frame de l'escena (SDL3 Callback API)
void Ending2::iterate() {
update();
render();
}
// Actualiza el estado

View File

@@ -7,19 +7,21 @@
#include <vector> // Para vector
#include "core/rendering/sprite/dissolve_sprite.hpp" // Para SurfaceDissolveSprite
#include "game/scenes/scene.hpp" // Para Scene
#include "utils/defines.hpp" // Para GameCanvas::WIDTH, GameCanvas::FIRST_QUAR...
class MovingSprite;
class DeltaTimer;
class Ending2 {
class Ending2 : public Scene {
public:
// --- Constructor y Destructor ---
Ending2();
~Ending2() = default;
~Ending2() override;
// --- Bucle principal ---
void run();
// --- Bucle principal (SDL3 Callback API) ---
void iterate() override;
void handleEvent(const SDL_Event& event) override;
private:
// --- Enumeraciones ---
@@ -58,7 +60,6 @@ class Ending2 {
// --- Métodos ---
void update(); // Actualiza el objeto
void render(); // Dibuja el final en pantalla
static void handleEvents(); // Comprueba el manejador de eventos
static void handleInput(); // Comprueba las entradas
void updateState(float delta_time); // Actualiza el estado
void transitionToState(EndingState new_state); // Transición entre estados

View File

@@ -163,9 +163,19 @@ Game::Game(Mode mode)
SceneManager::current = (mode_ == Mode::GAME) ? SceneManager::Scene::GAME : SceneManager::Scene::DEMO;
SceneManager::options = SceneManager::Options::NONE;
// Arranca la música del juego (abans a run())
keepMusicPlaying();
if (!scoreboard_data_->music && mode_ == Mode::GAME) {
Audio::get()->pauseMusic();
}
}
Game::~Game() {
if (mode_ == Mode::GAME) {
Audio::get()->stopMusic();
}
ItemTracker::destroy();
GameControl::change_player_skin = nullptr;
@@ -188,38 +198,35 @@ Game::~Game() {
#endif
}
// Comprueba los eventos de la cola
void Game::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
// Despatx d'un event (SDL3 Callback API)
void Game::handleEvent(const SDL_Event& event) {
GlobalEvents::handle(event);
#ifdef _DEBUG
// En modo editor: click del ratón cierra la consola
if (Console::get()->isActive() && MapEditor::get()->isActive() &&
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
Console::get()->toggle();
}
if (!Console::get()->isActive()) {
// Tecla 9: toggle editor (funciona tanto dentro como fuera del editor)
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_9 && static_cast<int>(event.key.repeat) == 0) {
if (MapEditor::get()->isActive()) {
GameControl::exit_editor();
Notifier::get()->show({Locale::get()->get("game.editor_disabled")}); // NOLINT(readability-static-accessed-through-instance)
} else {
GameControl::enter_editor();
Notifier::get()->show({Locale::get()->get("game.editor_enabled")}); // NOLINT(readability-static-accessed-through-instance)
}
} else if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_8 && static_cast<int>(event.key.repeat) == 0 && MapEditor::get()->isActive()) {
MapEditor::get()->showGrid(!MapEditor::get()->isGridEnabled());
} else if (MapEditor::get()->isActive()) {
MapEditor::get()->handleEvent(event);
} else {
handleDebugEvents(event);
}
}
#endif
// En modo editor: click del ratón cierra la consola
if (Console::get()->isActive() && MapEditor::get()->isActive() &&
event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
Console::get()->toggle();
}
if (!Console::get()->isActive()) {
// Tecla 9: toggle editor (funciona tanto dentro como fuera del editor)
if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_9 && static_cast<int>(event.key.repeat) == 0) {
if (MapEditor::get()->isActive()) {
GameControl::exit_editor();
Notifier::get()->show({Locale::get()->get("game.editor_disabled")}); // NOLINT(readability-static-accessed-through-instance)
} else {
GameControl::enter_editor();
Notifier::get()->show({Locale::get()->get("game.editor_enabled")}); // NOLINT(readability-static-accessed-through-instance)
}
} else if (event.type == SDL_EVENT_KEY_DOWN && event.key.key == SDLK_8 && static_cast<int>(event.key.repeat) == 0 && MapEditor::get()->isActive()) {
MapEditor::get()->showGrid(!MapEditor::get()->isGridEnabled());
} else if (MapEditor::get()->isActive()) {
MapEditor::get()->handleEvent(event);
} else {
handleDebugEvents(event);
}
}
#endif
}
// Comprueba el teclado
@@ -262,29 +269,17 @@ void Game::handleInput() {
GlobalInputs::handle();
}
// Bucle para el juego
void Game::run() {
keepMusicPlaying();
if (!scoreboard_data_->music && mode_ == Mode::GAME) {
Audio::get()->pauseMusic();
}
while (SceneManager::current == SceneManager::Scene::GAME || SceneManager::current == SceneManager::Scene::DEMO) {
update();
render();
}
if (mode_ == Mode::GAME) {
Audio::get()->stopMusic();
}
// Un frame de l'escena (SDL3 Callback API)
void Game::iterate() {
update();
render();
}
// Actualiza el juego, las variables, comprueba la entrada, etc.
void Game::update() {
const float DELTA_TIME = delta_timer_.tick();
handleEvents(); // Comprueba los eventos
handleInput(); // Comprueba las entradas
handleInput(); // Comprueba las entradas
#ifdef _DEBUG
Debug::get()->clear();

View File

@@ -8,6 +8,7 @@
#include <vector> // Para vector
#include "game/entities/player.hpp" // Para PlayerSpawn
#include "game/scenes/scene.hpp" // Para Scene
#include "utils/delta_timer.hpp" // Para DeltaTimer
class Room; // lines 12-12
class RoomTracker; // lines 13-13
@@ -15,7 +16,7 @@ class Scoreboard; // lines 14-14
class Stats; // lines 15-15
class Surface;
class Game {
class Game : public Scene {
public:
// --- Estructuras ---
enum class Mode {
@@ -33,10 +34,11 @@ class Game {
// --- Constructor y Destructor ---
explicit Game(Mode mode);
~Game();
~Game() override;
// --- Bucle para el juego ---
void run();
// --- Bucle para el juego (SDL3 Callback API) ---
void iterate() override;
void handleEvent(const SDL_Event& event) override;
private:
// --- Constantes de tiempo ---
@@ -57,7 +59,6 @@ class Game {
// --- Métodos ---
void update(); // Actualiza el juego, las variables, comprueba la entrada, etc.
void render(); // Pinta los objetos en pantalla
void handleEvents(); // Comprueba los eventos de la cola
void renderRoomName(); // Escribe el nombre de la pantalla
void transitionToState(State new_state); // Cambia al estado especificado y resetea los timers
void updatePlaying(float delta_time); // Actualiza el juego en estado PLAYING

View File

@@ -49,8 +49,7 @@ void GameOver::update() {
const float DELTA_TIME = delta_timer_->tick();
elapsed_time_ += DELTA_TIME;
handleEvents(); // Comprueba los eventos
handleInput(); // Comprueba las entradas
handleInput(); // Comprueba las entradas
updateState(); // Actualiza el estado de la escena
updateColor(); // Actualiza el color usado para renderizar los textos e imagenes
@@ -91,12 +90,9 @@ void GameOver::render() {
Screen::get()->render();
}
// Comprueba el manejador de eventos
void GameOver::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
// Despatx d'un event (SDL3 Callback API)
void GameOver::handleEvent(const SDL_Event& event) {
GlobalEvents::handle(event);
}
// Comprueba las entradas
@@ -105,12 +101,10 @@ void GameOver::handleInput() {
GlobalInputs::handle();
}
// Bucle principal
void GameOver::run() {
while (SceneManager::current == SceneManager::Scene::GAME_OVER) {
update();
render();
}
// Un frame de l'escena (SDL3 Callback API)
void GameOver::iterate() {
update();
render();
}
// Actualiza el color usado para renderizar los textos e imagenes

View File

@@ -2,19 +2,22 @@
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <vector> // Para vector
class AnimatedSprite; // lines 7-7
class DeltaTimer; // Forward declaration
#include <memory> // Para shared_ptr
#include <vector> // Para vector
class GameOver {
#include "game/scenes/scene.hpp" // Para Scene
class AnimatedSprite; // lines 7-7
class DeltaTimer; // Forward declaration
class GameOver : public Scene {
public:
// Constructor y Destructor
GameOver();
~GameOver() = default;
~GameOver() override = default;
// Bucle principal
void run();
// Bucle principal (SDL3 Callback API)
void iterate() override;
void handleEvent(const SDL_Event& event) override;
private:
// --- Enumeraciones ---
@@ -45,13 +48,12 @@ class GameOver {
static constexpr int NIGHTMARE_TEXT_Y_OFFSET = 120; // Offset Y del texto nightmare desde TEXT_Y
// --- Métodos ---
void update(); // Actualiza el objeto
void render(); // Dibuja el final en pantalla
static void handleEvents(); // Comprueba el manejador de eventos
static void handleInput(); // Comprueba las entradas
void updateState(); // Actualiza el estado y transiciones
void updateColor(); // Actualiza el color usado para renderizar
void renderSprites(); // Dibuja los sprites
void update(); // Actualiza el objeto
void render(); // Dibuja el final en pantalla
static void handleInput(); // Comprueba las entradas
void updateState(); // Actualiza el estado y transiciones
void updateColor(); // Actualiza el color usado para renderizar
void renderSprites(); // Dibuja los sprites
// --- Variables miembro ---
// Objetos y punteros a recursos

View File

@@ -41,19 +41,23 @@ LoadingScreen::LoadingScreen()
// Cambia el color del borde
Screen::get()->setBorderColor(stringToColor("white"));
transitionToState(State::SILENT1);
// Ajusta el volumen i neteja la pantalla (abans a run())
Audio::get()->setMusicVolume(50);
Screen::get()->start();
Screen::get()->clearRenderer();
Screen::get()->render();
}
// Destructor
LoadingScreen::~LoadingScreen() {
Audio::get()->stopMusic();
Audio::get()->setMusicVolume(100);
}
// Comprueba el manejador de eventos
void LoadingScreen::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
// Despatx d'un event (SDL3 Callback API)
void LoadingScreen::handleEvent(const SDL_Event& event) {
GlobalEvents::handle(event);
}
// Comprueba las entradas
@@ -347,8 +351,7 @@ void LoadingScreen::renderColoredBorder(PaletteColor color) {
void LoadingScreen::update() {
const float DELTA_TIME = delta_timer_->tick();
handleEvents(); // Comprueba los eventos
handleInput(); // Comprueba las entradas
handleInput(); // Comprueba las entradas
updateState(DELTA_TIME); // Actualiza el estado y gestiona transiciones
@@ -400,22 +403,10 @@ void LoadingScreen::render() {
Screen::get()->render();
}
// Bucle para el logo del juego
void LoadingScreen::run() {
// Ajusta el volumen
Audio::get()->setMusicVolume(50);
// Limpia la pantalla
Screen::get()->start();
Screen::get()->clearRenderer();
Screen::get()->render();
while (SceneManager::current == SceneManager::Scene::LOADING_SCREEN) {
update();
render();
}
Audio::get()->setMusicVolume(100);
// Un frame de l'escena (SDL3 Callback API)
void LoadingScreen::iterate() {
update();
render();
}
// Pinta el borde

View File

@@ -5,19 +5,21 @@
#include <array> // Para std::array
#include <memory> // Para shared_ptr
#include "game/scenes/scene.hpp" // Para Scene
#include "utils/delta_timer.hpp" // Para DeltaTimer
#include "utils/utils.hpp" // Para PaletteColor
class Sprite; // Forward declaration
class Surface; // Forward declaration
class LoadingScreen {
class LoadingScreen : public Scene {
public:
// --- Constructor y Destructor ---
LoadingScreen();
~LoadingScreen();
~LoadingScreen() override;
// --- Bucle principal ---
void run();
// --- Bucle principal (SDL3 Callback API) ---
void iterate() override;
void handleEvent(const SDL_Event& event) override;
private:
// --- Enumeraciones ---
@@ -81,7 +83,6 @@ class LoadingScreen {
// --- Métodos ---
void update(); // Actualiza las variables
void render(); // Dibuja en pantalla
static void handleEvents(); // Comprueba el manejador de eventos
static void handleInput(); // Comprueba las entradas
void updateState(float delta_time); // Actualiza el estado actual
void transitionToState(State new_state); // Transiciona a un nuevo estado

View File

@@ -54,12 +54,9 @@ Logo::Logo()
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK));
}
// Comprueba el manejador de eventos
void Logo::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
}
// Despatx d'un event (SDL3 Callback API)
void Logo::handleEvent(const SDL_Event& event) {
GlobalEvents::handle(event);
}
// Comprueba las entradas
@@ -201,8 +198,7 @@ void Logo::updateState(float delta_time) {
void Logo::update() {
const float DELTA_TIME = delta_timer_->tick();
handleEvents(); // Comprueba los eventos
handleInput(); // Comprueba las entradas
handleInput(); // Comprueba las entradas
updateState(DELTA_TIME); // Actualiza el estado y gestiona transiciones
updateJAILGAMES(DELTA_TIME); // Gestiona el logo de JAILGAME
@@ -228,12 +224,10 @@ void Logo::render() { // NOLINT(readability-convert-member-functions-to-static)
Screen::get()->render();
}
// Bucle para el logo del juego
void Logo::run() {
while (SceneManager::current == SceneManager::Scene::LOGO) {
update();
render();
}
// Un frame de l'escena (SDL3 Callback API)
void Logo::iterate() {
update();
render();
}
// Termina la sección

View File

@@ -6,11 +6,12 @@
#include <memory> // Para shared_ptr
#include <vector> // Para vector
#include "game/scenes/scene.hpp" // Para Scene
#include "utils/delta_timer.hpp" // Para DeltaTimer
class Sprite; // Forward declaration
class Surface; // Forward declaration
class Logo {
class Logo : public Scene {
public:
// --- Tipos ---
using EasingFunction = std::function<float(float)>; // Función de easing (permite lambdas)
@@ -28,10 +29,11 @@ class Logo {
// --- Constructor y Destructor ---
Logo();
~Logo() = default;
~Logo() override = default;
// --- Bucle principal ---
void run();
// --- Bucle principal (SDL3 Callback API) ---
void iterate() override;
void handleEvent(const SDL_Event& event) override;
private:
// --- Constantes de tiempo (en segundos) ---
@@ -48,7 +50,6 @@ class Logo {
// --- Métodos ---
void update(); // Actualiza las variables
void render(); // Dibuja en pantalla
static void handleEvents(); // Comprueba el manejador de eventos
static void handleInput(); // Comprueba las entradas
void updateJAILGAMES(float delta_time); // Gestiona el logo de JAILGAME (time-based)
void updateTextureColors(); // Gestiona el color de las texturas

View File

@@ -0,0 +1,13 @@
#pragma once
#include <SDL3/SDL.h>
// Interfície base per a totes les escenes del joc.
// Cada escena concreta implementa iterate() (un frame) i handleEvent() (un event).
// Director crida aquests mètodes des de SDL_AppIterate / SDL_AppEvent.
class Scene {
public:
virtual ~Scene() = default;
virtual void iterate() = 0;
virtual void handleEvent(const SDL_Event& event) = 0;
};

View File

@@ -81,28 +81,40 @@ void Title::initMarquee() {
last_active_letter_ = 0;
}
// Comprueba el manejador de eventos
void Title::handleEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
GlobalEvents::handle(event);
// Despatx d'un event (SDL3 Callback API)
void Title::handleEvent(const SDL_Event& event) {
GlobalEvents::handle(event);
// Manejo especial para captura de botones de gamepad
if (is_remapping_joystick_ && !remap_completed_ &&
(event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN || event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION)) {
handleJoystickRemap(event);
continue; // No procesar más este evento
// Manejo especial para captura de botones de gamepad
if (is_remapping_joystick_ && !remap_completed_ &&
(event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN || event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION)) {
handleJoystickRemap(event);
return; // No procesar más este evento
}
// Qualsevol botó del comandament al menú principal inicia partida directament
// (els bindings ja estan definits, no cal "pulsar 1" amb el teclat). El botó
// BACK queda exclòs perquè es reserva per a EXIT — excepte a emscripten, on
// no es pot sortir del joc i BACK pot actuar com a botó genèric d'inici.
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN &&
state_ == State::MAIN_MENU &&
!is_remapping_keyboard_ && !is_remapping_joystick_
#ifndef __EMSCRIPTEN__
&& event.gbutton.button != SDL_GAMEPAD_BUTTON_BACK
#endif
) {
handleMainMenuKeyPress(SDLK_1); // PLAY
return;
}
if (event.type == SDL_EVENT_KEY_DOWN && !Console::get()->isActive()) {
// Si estamos en modo remap de teclado, capturar tecla
if (is_remapping_keyboard_ && !remap_completed_) {
handleKeyboardRemap(event);
}
if (event.type == SDL_EVENT_KEY_DOWN && !Console::get()->isActive()) {
// Si estamos en modo remap de teclado, capturar tecla
if (is_remapping_keyboard_ && !remap_completed_) {
handleKeyboardRemap(event);
}
// Si estamos en el menú principal normal
else if (state_ == State::MAIN_MENU && !is_remapping_keyboard_ && !is_remapping_joystick_) {
handleMainMenuKeyPress(event.key.key);
}
// Si estamos en el menú principal normal
else if (state_ == State::MAIN_MENU && !is_remapping_keyboard_ && !is_remapping_joystick_) {
handleMainMenuKeyPress(event.key.key);
}
}
}
@@ -242,7 +254,6 @@ void Title::renderMarquee() const {
void Title::update() {
const float DELTA_TIME = delta_timer_->tick();
handleEvents(); // Comprueba los eventos
handleInput(DELTA_TIME); // Comprueba las entradas
updateState(DELTA_TIME); // Actualiza el estado actual
@@ -434,12 +445,10 @@ void Title::render() {
Screen::get()->render();
}
// Bucle para el logo del juego
void Title::run() {
while (SceneManager::current == SceneManager::Scene::TITLE) {
update();
render();
}
// Un frame de l'escena (SDL3 Callback API)
void Title::iterate() {
update();
render();
}
// Crea y rellena la textura para mostrar los logros
@@ -493,7 +502,7 @@ void Title::createCheevosTexture() { // NOLINT(readability-convert-member-funct
// Crea el sprite para el listado de logros (usa la zona del menu)
cheevos_sprite_ = std::make_unique<Sprite>(cheevos_surface_, (GameCanvas::WIDTH - cheevos_surface_->getWidth()) / 2, MENU_ZONE_Y, cheevos_surface_->getWidth(), cheevos_surface_->getHeight());
cheevos_surface_view_ = {.x = 0, .y = 0, .w = cheevos_surface_->getWidth(), .h = CHEEVOS_TEXTURE_VIEW_HEIGHT};
cheevos_surface_view_ = {.x = 0, .y = 0, .w = static_cast<float>(cheevos_surface_->getWidth()), .h = CHEEVOS_TEXTURE_VIEW_HEIGHT};
cheevos_sprite_->setClip(cheevos_surface_view_);
}

View File

@@ -8,19 +8,21 @@
#include <vector> // Para vector
#include "game/scene_manager.hpp" // Para SceneManager::Scene
#include "game/scenes/scene.hpp" // Para Scene
#include "utils/delta_timer.hpp" // Para DeltaTimer
class Sprite; // Forward declaration
class Surface; // Forward declaration
class Text; // Forward declaration
class Title {
class Title : public Scene {
public:
// --- Constructor y Destructor ---
Title();
~Title();
~Title() override;
// --- Bucle principal ---
void run();
// --- Bucle principal (SDL3 Callback API) ---
void iterate() override;
void handleEvent(const SDL_Event& event) override;
private:
// --- Estructuras y enumeraciones ---
@@ -61,7 +63,6 @@ class Title {
// --- Métodos ---
void update(); // Actualiza las variables
void render(); // Dibuja en pantalla
void handleEvents(); // Comprueba el manejador de eventos
void handleMainMenuKeyPress(SDL_Keycode key); // Maneja las teclas del menu principal
void handleInput(float delta_time); // Comprueba las entradas
void updateState(float delta_time); // Actualiza el estado actual

View File

@@ -14,7 +14,7 @@
#include "core/rendering/text.hpp" // Para Text
#include "core/resources/resource_cache.hpp" // Para Resource
#include "game/options.hpp" // Para Options
#include "game/ui/notifier.hpp" // Para Notifier
#include "utils/easing_functions.hpp" // Para Easing::cubicInOut
// ── Helpers de texto ──────────────────────────────────────────────────────────
@@ -194,22 +194,16 @@ void Console::update(float delta_time) { // NOLINT(readability-function-cogniti
// Animación de altura (resize cuando msg_lines_ cambia); solo en ACTIVE
if (status_ == Status::ACTIVE && height_ != target_height_) {
const float PREV_HEIGHT = height_;
if (height_ < target_height_) {
height_ = std::min(height_ + (SLIDE_SPEED * delta_time), target_height_);
} else {
height_ = std::max(height_ - (SLIDE_SPEED * delta_time), target_height_);
if (anim_progress_ == 0.0F) {
// Iniciar animación de resize
anim_start_ = height_;
anim_end_ = target_height_;
}
// Actualizar el Notifier incrementalmente con el delta de altura
if (Notifier::get() != nullptr) {
const int DELTA_PX = static_cast<int>(height_) - static_cast<int>(PREV_HEIGHT);
if (DELTA_PX > 0) {
Notifier::get()->addYOffset(DELTA_PX);
notifier_offset_applied_ += DELTA_PX;
} else if (DELTA_PX < 0) {
Notifier::get()->removeYOffset(-DELTA_PX);
notifier_offset_applied_ += DELTA_PX;
}
anim_progress_ = std::min(anim_progress_ + (delta_time / ANIM_DURATION), 1.0F);
height_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_));
if (anim_progress_ >= 1.0F) {
height_ = target_height_;
anim_progress_ = 0.0F;
}
// Reconstruir la Surface al nuevo tamaño (pequeña: 256×~18-72px)
const float WIDTH = Options::game.width;
@@ -220,28 +214,23 @@ void Console::update(float delta_time) { // NOLINT(readability-function-cogniti
// Redibujar texto cada frame
redrawText();
switch (status_) {
case Status::RISING: {
y_ += SLIDE_SPEED * delta_time;
if (y_ >= 0.0F) {
y_ = 0.0F;
// Animación de apertura/cierre (basada en tiempo con easing)
if (status_ == Status::RISING || status_ == Status::VANISHING) {
anim_progress_ = std::min(anim_progress_ + (delta_time / ANIM_DURATION), 1.0F);
y_ = anim_start_ + ((anim_end_ - anim_start_) * Easing::cubicInOut(anim_progress_));
if (anim_progress_ >= 1.0F) {
y_ = anim_end_;
anim_progress_ = 0.0F;
if (status_ == Status::RISING) {
status_ = Status::ACTIVE;
}
break;
}
case Status::VANISHING: {
y_ -= SLIDE_SPEED * delta_time;
if (y_ <= -height_) {
y_ = -height_;
} else {
status_ = Status::HIDDEN;
// Resetear el mensaje una vez completamente oculta
msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)};
target_height_ = calcTargetHeight(static_cast<int>(msg_lines_.size()));
}
break;
}
default:
break;
}
SDL_FRect rect = {.x = 0, .y = y_, .w = Options::game.width, .h = height_};
@@ -265,6 +254,9 @@ void Console::toggle() {
target_height_ = calcTargetHeight(static_cast<int>(msg_lines_.size()));
height_ = target_height_;
y_ = -height_;
anim_start_ = y_;
anim_end_ = 0.0F;
anim_progress_ = 0.0F;
status_ = Status::RISING;
input_line_.clear();
cursor_timer_ = 0.0F;
@@ -273,24 +265,18 @@ void Console::toggle() {
typewriter_chars_ = static_cast<int>(msg_lines_[0].size());
typewriter_timer_ = 0.0F;
SDL_StartTextInput(SDL_GetKeyboardFocus());
if (Notifier::get() != nullptr) {
const int OFFSET = static_cast<int>(height_);
Notifier::get()->addYOffset(OFFSET);
notifier_offset_applied_ = OFFSET;
}
if (on_toggle) { on_toggle(true); }
break;
case Status::ACTIVE:
// Al cerrar: mantener el texto visible hasta que esté completamente oculta
anim_start_ = y_;
anim_end_ = -height_;
anim_progress_ = 0.0F;
status_ = Status::VANISHING;
target_height_ = height_; // No animar durante VANISHING
target_height_ = height_; // No animar altura durante VANISHING
history_index_ = -1;
saved_input_.clear();
SDL_StopTextInput(SDL_GetKeyboardFocus());
if (Notifier::get() != nullptr) {
Notifier::get()->removeYOffset(notifier_offset_applied_);
notifier_offset_applied_ = 0;
}
if (on_toggle) { on_toggle(false); }
break;
default:

View File

@@ -51,14 +51,14 @@ class Console {
};
// Constantes visuales
static constexpr Uint8 BG_COLOR = 0; // PaletteColor::BLACK
static constexpr Uint8 BORDER_COLOR = 9; // PaletteColor::BRIGHT_GREEN
static constexpr Uint8 MSG_COLOR = 8; // PaletteColor::GREEN
static constexpr float SLIDE_SPEED = 180.0F;
static constexpr Uint8 BG_COLOR = 0; // PaletteColor::BLACK
static constexpr Uint8 BORDER_COLOR = 9; // PaletteColor::BRIGHT_GREEN
static constexpr Uint8 MSG_COLOR = 8; // PaletteColor::GREEN
static constexpr float ANIM_DURATION = 0.3F; // Duración de cualquier animación (segundos)
// Constantes de consola
static constexpr std::string_view CONSOLE_NAME = "JDD Console";
static constexpr std::string_view CONSOLE_VERSION = "v2.2";
static constexpr std::string_view CONSOLE_VERSION = "v2.3";
static constexpr int MAX_LINE_CHARS = 32;
static constexpr int MAX_HISTORY_SIZE = 20;
static constexpr float CURSOR_ON_TIME = 0.5F;
@@ -99,9 +99,13 @@ class Console {
int typewriter_chars_{0}; // Caracteres de msg_lines_ actualmente visibles
float typewriter_timer_{0.0F};
// Animación basada en tiempo (0→1 en ANIM_DURATION)
float anim_progress_{0.0F}; // Progreso normalizado [0, 1]
float anim_start_{0.0F}; // Valor inicial (y_ o height_)
float anim_end_{0.0F}; // Valor final
// Animación de altura dinámica
float target_height_{0.0F}; // Altura objetivo (según número de líneas de mensaje)
int notifier_offset_applied_{0}; // Acumulador del offset enviado al Notifier
float target_height_{0.0F}; // Altura objetivo (según número de líneas de mensaje)
// Historial de comandos (navegable con flechas arriba/abajo)
std::deque<std::string> history_;

View File

@@ -491,6 +491,9 @@ static auto cmdDebug(const std::vector<std::string>& args) -> std::string { //
if (args[2] == "GAME") {
target = SceneManager::Scene::GAME;
name = "game";
} else if (args[2] == "DEMO") {
target = SceneManager::Scene::DEMO;
name = "demo";
} else if (args[2] == "LOGO") {
target = SceneManager::Scene::LOGO;
name = "logo";
@@ -651,10 +654,10 @@ static auto cmdItems(const std::vector<std::string>& args) -> std::string {
return "Items: " + std::to_string(count);
}
// SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART]
// SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|DEMO|ENDING|ENDING2|RESTART]
static auto cmdScene(const std::vector<std::string>& args) -> std::string {
if (Options::kiosk.enabled) { return "Not allowed in kiosk mode"; }
if (args.empty()) { return "usage: scene [logo|loading|title|credits|game|ending|ending2|restart]"; }
if (args.empty()) { return "usage: scene [logo|loading|title|credits|game|demo|ending|ending2|restart]"; }
if (args[0] == "RESTART") {
SceneManager::scene_before_restart = SceneManager::current;
@@ -677,6 +680,7 @@ static auto cmdScene(const std::vector<std::string>& args) -> std::string {
if (args[0] == "TITLE") { return GO_TO(SceneManager::Scene::TITLE, "Title"); }
if (args[0] == "CREDITS") { return GO_TO(SceneManager::Scene::CREDITS, "Credits"); }
if (args[0] == "GAME") { return GO_TO(SceneManager::Scene::GAME, "Game"); }
if (args[0] == "DEMO") { return GO_TO(SceneManager::Scene::DEMO, "Demo"); }
if (args[0] == "ENDING") { return GO_TO(SceneManager::Scene::ENDING, "Ending"); }
if (args[0] == "ENDING2") { return GO_TO(SceneManager::Scene::ENDING2, "Ending 2"); }
return "Unknown scene: " + args[0];
@@ -948,11 +952,16 @@ static auto cmdKiosk(const std::vector<std::string>& args) -> std::string {
// EXIT / QUIT
static auto cmdExit(const std::vector<std::string>& args) -> std::string {
#ifdef __EMSCRIPTEN__
(void)args;
return "Not allowed in web version";
#else
if (Options::kiosk.enabled && (args.empty() || args[0] != "PLEASE")) {
return "Not allowed in kiosk mode";
}
SceneManager::current = SceneManager::Scene::QUIT;
return "Quitting...";
#endif
}
// SIZE

View File

@@ -15,6 +15,7 @@
#include "core/rendering/text.hpp" // Para Text, Text::CENTER_FLAG, Text::COLOR_FLAG
#include "core/resources/resource_cache.hpp" // Para Resource
#include "game/options.hpp" // Para Options, options, NotificationPosition
#include "game/ui/console.hpp" // Para Console
#include "utils/delta_timer.hpp" // Para DeltaTimer
#include "utils/utils.hpp" // Para PaletteColor
@@ -73,8 +74,11 @@ void Notifier::render() {
// Actualiza el estado de las notificaiones
void Notifier::update(float delta_time) {
// Base Y leída cada frame: cada notificación se dibuja en rect.y (relativo a BASE) + BASE
const float BASE = static_cast<float>(getStackBaseY());
for (auto& notification : notifications_) {
// Si la notificación anterior está "saliendo", no hagas nada
// Si la notificación anterior está "entrando", no hagas nada (stall del resto)
if (!notifications_.empty() && &notification != &notifications_.front()) {
const auto& previous_notification = *(std::prev(&notification));
if (previous_notification.state == Status::RISING) {
@@ -84,17 +88,17 @@ void Notifier::update(float delta_time) {
switch (notification.state) {
case Status::RISING: {
const float DISPLACEMENT = SLIDE_SPEED * delta_time;
notification.rect.y += DISPLACEMENT;
if (notification.rect.y >= notification.y) {
notification.rect.y = notification.y;
const float TARGET = static_cast<float>(notification.y);
notification.rect.y += SLIDE_SPEED * delta_time;
if (notification.rect.y >= TARGET) {
notification.rect.y = TARGET;
notification.state = Status::STAY;
notification.elapsed_time = 0.0F;
}
break;
}
case Status::STAY: {
notification.rect.y = static_cast<float>(notification.y);
notification.elapsed_time += delta_time;
if (notification.elapsed_time >= notification.display_duration) {
notification.state = Status::VANISHING;
@@ -103,10 +107,8 @@ void Notifier::update(float delta_time) {
}
case Status::VANISHING: {
const float DISPLACEMENT = SLIDE_SPEED * delta_time;
notification.rect.y -= DISPLACEMENT;
const float TARGET_Y = notification.y - notification.travel_dist;
const float TARGET_Y = static_cast<float>(notification.y - notification.travel_dist);
notification.rect.y -= SLIDE_SPEED * delta_time;
if (notification.rect.y <= TARGET_Y) {
notification.rect.y = TARGET_Y;
notification.state = Status::FINISHED;
@@ -120,8 +122,13 @@ void Notifier::update(float delta_time) {
default:
break;
}
}
notification.sprite->setPosition(notification.rect);
// Refrescar posiciones de sprite cada frame (convierte rect.y relativo a absoluto)
for (auto& notification : notifications_) {
SDL_FRect sprite_rect = notification.rect;
sprite_rect.y += BASE;
notification.sprite->setPosition(sprite_rect);
}
clearFinishedNotifications();
@@ -170,15 +177,13 @@ void Notifier::show(std::vector<std::string> texts, const Style& style, int icon
// Posición horizontal
float desp_h = ((Options::game.width / 2) - (WIDTH / 2));
;
// Posición vertical
const int DESP_V = y_offset_;
// Offset
// Offset vertical (relativo a la base de la pila, que se consulta cada frame)
const auto TRAVEL_DIST = HEIGHT + PADDING_OUT;
const int TRAVEL_MOD = 1;
const int OFFSET = !notifications_.empty() ? notifications_.back().y + (TRAVEL_MOD * notifications_.back().travel_dist) : DESP_V;
const int OFFSET = !notifications_.empty()
? notifications_.back().y + (TRAVEL_MOD * notifications_.back().travel_dist)
: 0;
// Crea la notificacion
Notification n;
@@ -191,8 +196,9 @@ void Notifier::show(std::vector<std::string> texts, const Style& style, int icon
n.texts = texts;
n.shape = SHAPE;
n.display_duration = style.duration;
const float Y_POS = OFFSET + -TRAVEL_DIST;
n.rect = {.x = desp_h, .y = Y_POS, .w = WIDTH, .h = HEIGHT};
// Posición inicial relativa a la base: arranca "travel_dist" por encima del target (=OFFSET)
const float Y_POS_REL = static_cast<float>(OFFSET) - TRAVEL_DIST;
n.rect = {.x = desp_h, .y = Y_POS_REL, .w = WIDTH, .h = HEIGHT};
// Crea la textura
n.surface = std::make_shared<Surface>(WIDTH, HEIGHT);
@@ -219,7 +225,7 @@ void Notifier::show(std::vector<std::string> texts, const Style& style, int icon
else if (SHAPE == Shape::SQUARED) {
n.surface->clear(style.bg_color);
SDL_FRect squared_rect = {.x = 0, .y = 0, .w = n.surface->getWidth(), .h = n.surface->getHeight()};
SDL_FRect squared_rect = {.x = 0, .y = 0, .w = static_cast<float>(n.surface->getWidth()), .h = static_cast<float>(n.surface->getHeight())};
n.surface->drawRectBorder(&squared_rect, style.border_color);
}
@@ -252,8 +258,10 @@ void Notifier::show(std::vector<std::string> texts, const Style& style, int icon
// Deja de dibujar en la textura
Screen::get()->setRendererSurface(previuos_renderer);
// Crea el sprite de la notificación
n.sprite = std::make_shared<Sprite>(n.surface, n.rect);
// Crea el sprite de la notificación (rect absoluto a partir del relativo + BASE)
SDL_FRect initial_sprite_rect = n.rect;
initial_sprite_rect.y += static_cast<float>(getStackBaseY());
n.sprite = std::make_shared<Sprite>(n.surface, initial_sprite_rect);
// Añade la notificación a la lista
notifications_.emplace_back(n);
@@ -278,9 +286,21 @@ void Notifier::clearNotifications() {
clearFinishedNotifications();
}
// Ajusta el offset vertical base
void Notifier::addYOffset(int px) { y_offset_ += px; }
void Notifier::removeYOffset(int px) { y_offset_ -= px; }
// Y absoluta de la base de la pila (justo debajo de Console, o 0 si no hay Console)
auto Notifier::getStackBaseY() const -> int {
return Console::get() != nullptr ? Console::get()->getVisibleHeight() : 0;
}
// Altura animada ocupada por la pila (usa rect.y animado, no el target — para transiciones suaves)
auto Notifier::getVisibleHeight() const -> int {
int bottom = 0;
for (const auto& n : notifications_) {
if (n.state == Status::FINISHED) { continue; }
const int N_BOTTOM = static_cast<int>(n.rect.y + n.rect.h);
if (N_BOTTOM > bottom) { bottom = N_BOTTOM; }
}
return bottom;
}
// Obtiene los códigos de las notificaciones
auto Notifier::getCodes() -> std::vector<std::string> {

View File

@@ -59,9 +59,9 @@ class Notifier {
auto isActive() -> bool; // Indica si hay notificaciones activas
auto getCodes() -> std::vector<std::string>; // Obtiene códigos de notificaciones
// Offset vertical (para evitar solapamiento con Console y renderInfo)
void addYOffset(int px); // Suma píxeles al offset base
void removeYOffset(int px); // Resta píxeles al offset base
// Altura animada ocupada por la pila de notificaciones, en píxeles (relativa a la base).
// Crece/decrece suavemente con las animaciones de entrada/salida.
[[nodiscard]] auto getVisibleHeight() const -> int;
private:
// Tipos anidados
@@ -78,8 +78,8 @@ class Notifier {
std::vector<std::string> texts;
Status state{Status::RISING};
Shape shape{Shape::SQUARED};
SDL_FRect rect{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F};
int y{0};
SDL_FRect rect{.x = 0.0F, .y = 0.0F, .w = 0.0F, .h = 0.0F}; // rect.y es relativo a la base de la pila
int y{0}; // Top objetivo de la notificación relativo a la base de la pila
int travel_dist{0};
std::string code;
bool can_be_removed{true};
@@ -97,8 +97,9 @@ class Notifier {
static Notifier* notifier;
// Métodos privados
void clearFinishedNotifications(); // Elimina las notificaciones finalizadas
void clearNotifications(); // Finaliza y elimina todas las notificaciones activas
void clearFinishedNotifications(); // Elimina las notificaciones finalizadas
void clearNotifications(); // Finaliza y elimina todas las notificaciones activas
[[nodiscard]] auto getStackBaseY() const -> int; // Y absoluta de la base de la pila (leída de Console)
// Constructor y destructor privados [SINGLETON]
Notifier(const std::string& icon_file, const std::string& text);
@@ -111,5 +112,4 @@ class Notifier {
std::vector<Notification> notifications_; // Lista de notificaciones activas
bool stack_{false}; // Indica si las notificaciones se apilan
bool has_icons_{false}; // Indica si el notificador tiene textura para iconos
int y_offset_{0}; // Offset vertical base (ajustado por Console y renderInfo)
};

View File

@@ -5,14 +5,24 @@ Empezado en Castalla el 01/07/2022.
*/
#include <memory>
#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL_main.h>
#include "core/system/director.hpp"
auto main() -> int {
// Crea el objeto Director
auto director = std::make_unique<Director>();
// Bucle principal
return Director::run();
SDL_AppResult SDL_AppInit(void** appstate, int /*argc*/, char* /*argv*/[]) {
*appstate = new Director();
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void* appstate) {
return static_cast<Director*>(appstate)->iterate();
}
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event) {
return static_cast<Director*>(appstate)->handleEvent(*event);
}
void SDL_AppQuit(void* appstate, SDL_AppResult /*result*/) {
delete static_cast<Director*>(appstate);
}

View File

@@ -6,7 +6,7 @@
namespace Texts {
constexpr const char* WINDOW_CAPTION = "© 2022 JailDoctor's Dilemma — JailDesigner";
constexpr const char* COPYRIGHT = "@2022 JailDesigner";
constexpr const char* VERSION = "1.13"; // Versión por defecto
constexpr const char* VERSION = "1.14"; // Versión por defecto
} // namespace Texts
// Tamaño de bloque

View File

@@ -1,53 +0,0 @@
# Makefile for pack_resources tool
# Compiler
CXX := g++
CXXFLAGS := -std=c++20 -Wall -Wextra -O2
# Directories
TOOL_DIR := .
SOURCE_DIR := ../../source/core/resources
# Source files
SOURCES := pack_resources.cpp \
$(SOURCE_DIR)/resource_pack.cpp
# Output
TARGET := pack_resources
# Platform-specific executable extension
ifeq ($(OS),Windows_NT)
TARGET := $(TARGET).exe
endif
# Default target
all: $(TARGET)
# Build the tool
$(TARGET): $(SOURCES)
@echo "Building pack_resources tool..."
$(CXX) $(CXXFLAGS) $(SOURCES) -o $(TARGET)
@echo "Build complete: $(TARGET)"
# Test: create a test pack
test: $(TARGET)
@echo "Creating test pack..."
./$(TARGET) ../../data test_resources.pack
# Create the actual resources.pack
pack: $(TARGET)
@echo "Creating resources.pack..."
./$(TARGET) ../../data ../../resources.pack
# List contents of a pack
list: $(TARGET)
@echo "Listing pack contents..."
./$(TARGET) --list ../../resources.pack
# Clean
clean:
@echo "Cleaning..."
rm -f $(TARGET) test_resources.pack
@echo "Clean complete"
.PHONY: all test pack list clean