Compare commits

..

45 Commits

Author SHA1 Message Date
JailDesigner e68308d5a0 merge fix/neteja-warnings: neteja de warnings 2026-05-30 23:16:52 +02:00
JailDesigner 0a8478c2b1 fix: silencia warnings de stb_vorbis i paràmetre no usat 2026-05-30 23:16:36 +02:00
JailDesigner 73e21e9dc0 merge docs/arquitectura: guia d'arquitectura del projecte 2026-05-29 13:39:03 +02:00
JailDesigner ff3c3fe2ff afig guia d'arquitectura del projecte 2026-05-29 13:38:06 +02:00
JailDesigner e47bc5188a activa -Wextra -Wpedantic i neteja warnings 2026-05-18 21:53:32 +02:00
JailDesigner 78682b7071 estandarditza la sortida de pack_resources 2026-05-18 17:57:24 +02:00
JailDesigner 981000536c merge: migració PostFX a versió analítica sense supersampling 2026-05-17 20:47:52 +02:00
JailDesigner 5e6a469d46 postfx analític: nou shader + estructures chroma_min/max + scan_*
- Substitueix postfx.frag per la versió analítica amb smoothstep
- PostFXUniforms 12→16 floats (64B, 4×vec4): afegeix chroma_min/max,
  scan_dark_ratio, scan_dark_floor, scan_edge_soft
- PostFXParams i PostFXPreset adopten els nous camps amb defaults d'AEE
- MSL extret a source/core/rendering/sdl3gpu/msl/{postfx_vert,postfx_frag,
  crtpi_frag}.msl.h (estil Rendering::Msl::kXxx)
- SPIR-V regenerat (postfx_frag_spv.h: 13648 bytes)
- options.cpp llegeix 'chroma' antic com compat (assigna a min i max);
  escriu els 6 presets per defecte (CRT/NTSC/CURVED/SCANLINES/SUBTLE/CRT LIVE)
  amb els valors d'aee_arcade

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:32:34 +02:00
JailDesigner 743d8c7877 merge audita-nolint: 287→9 NOLINT (10 refactors + neteja obsolets) 2026-05-17 18:56:43 +02:00
JailDesigner f8efe07e42 enemy: justifica NOLINT(EnumCastOutOfRange) — SDL_FlipMode és bitmask 2026-05-17 18:45:39 +02:00
JailDesigner 418df568a1 elimina NOLINTs petits obsolets; justifica el residual de CallAndMessage 2026-05-17 18:30:37 +02:00
JailDesigner 3228647738 elimina NOLINT(identifier-naming) obsolets 2026-05-17 17:47:19 +02:00
JailDesigner 9e3b960939 elimina NOLINT(cognitive-complexity) obsolets en editor/console 2026-05-17 17:32:29 +02:00
JailDesigner 973bfa80bf console_commands: load() delega a parsers i buildHelp (cognitive 69→<25) 2026-05-17 17:17:55 +02:00
JailDesigner 6fbd5988d4 resource_list: loadFromString delega a parsers en namespace anònim 2026-05-17 16:47:37 +02:00
JailDesigner e89a664eed options: loadCrtPiFromFile en helpers (defaults, writer, parser) 2026-05-17 16:03:33 +02:00
JailDesigner 19c7b58cba console: handleEvent dispatch a handleTextInput/HistoryUp/Down/Tab 2026-05-17 15:29:20 +02:00
JailDesigner 8cca35c9a5 console_commands: cmdCheat usa helpers applyCheatToggle i cmdCheatJail 2026-05-17 15:14:54 +02:00
JailDesigner 724ac5f11d console_commands: cmdSs en subcomandes (size/upscale/downscale/on/off) 2026-05-17 14:59:58 +02:00
JailDesigner b36740ad58 global_inputs: getPressedAction usa helpers per a mods i llistes 2026-05-17 14:45:54 +02:00
JailDesigner 1c3ab9e5c1 options: helpers readYamlField/Volume per a loadAudioConfigFromYaml 2026-05-17 14:05:22 +02:00
JailDesigner 01e61767dc console: update() en 4 sub-passos (cursor, typewriter, resize, open/close) 2026-05-17 13:51:12 +02:00
JailDesigner 4e9d7e1450 console: lambda append_csv per a generateConsoleHelp 2026-05-17 13:36:22 +02:00
JailDesigner 3e33f7bac5 refactor SDL3GPU shader: createPipeline i render en sub-passos 2026-05-17 13:21:39 +02:00
JailDesigner 62935bf892 elimina NOLINT obsolets (241 marques que ja no disparaven warning) 2026-05-17 12:18:19 +02:00
JailDesigner 9b6d6747b5 fix: std::fill en lloc de raw loops (cppcheck) 2026-05-16 13:30:50 +02:00
JailDesigner 3717db49c0 merge: neteja tidy JDD 2026-05-15 06:23:45 +02:00
JailDesigner 8f5d897048 fix: resta tidy (60 troballes — empty-catch, widening, branch-clone, etc.) 2026-05-14 23:55:44 +02:00
JailDesigner f047ae1a56 refactor: JA_* a namespace Ja:: (estil aee_arcade) 2026-05-14 23:06:41 +02:00
JailDesigner da317e707d fix: claus en una línia i nullptr explícit 2026-05-14 22:45:54 +02:00
JailDesigner b480a23c88 fix: literal F, headers C++ i trailing return type 2026-05-14 22:36:48 +02:00
JailDesigner ac93cfa7d7 fix: enum class amb base std::uint8_t (33 troballes) 2026-05-14 22:32:57 +02:00
JailDesigner 058f7b118a merge: neteja cppcheck JDD 2026-05-14 21:46:15 +02:00
JailDesigner b4d3776239 fix: bucles cap a ranges algorithms (38 troballes) 2026-05-14 21:36:21 +02:00
JailDesigner 0aa9f8fe0a fix: init lists, scope reduit i rename vorbis_error 2026-05-14 20:56:14 +02:00
JailDesigner f4dea6d39b fix: const a punters i refs (13 troballes) 2026-05-14 20:47:34 +02:00
JailDesigner 1b40c90a00 fix: simplifica cmdCheat infinite lives, elimina getActionName mort 2026-05-14 20:21:45 +02:00
JailDesigner 6d90b79260 fix: substr → resize en 4 llocs 2026-05-14 20:19:35 +02:00
JailDesigner 2c55dd8eb4 fix: throw; al call site, no a l'helper 2026-05-14 20:11:37 +02:00
JailDesigner f3566821a6 afegir git hooks per format, tidy i cppcheck 2026-05-14 17:48:43 +02:00
JailDesigner d9c49c5c42 elimina compile_spirv.sh obsolet i regenera spv headers 2026-05-14 17:40:08 +02:00
JailDesigner a2c6af23b9 binari i recursos a build/, targets en kebab 2026-05-14 17:26:09 +02:00
JailDesigner d95bec8dac estandaritzat .clang-tidy amb el d'AEEA 2026-05-14 16:35:38 +02:00
JailDesigner 8fcc9ef103 detecta Ninja com a generador de CMake si està al PATH 2026-05-14 16:23:41 +02:00
JailDesigner 3e6fcfeb72 correccions en el makefile de macos 2026-05-03 18:03:30 +02:00
111 changed files with 13154 additions and 16166 deletions
+71 -43
View File
@@ -8,72 +8,100 @@ Checks:
- -bugprone-integer-division
- -bugprone-easily-swappable-parameters
- -bugprone-narrowing-conversions
- -modernize-avoid-c-arrays,-warnings-as-errors
- -modernize-avoid-c-arrays
WarningsAsErrors: '*'
# Solo headers del propio código fuente (external/ y spv/ tienen su propio .clang-tidy dummy)
HeaderFilterRegex: 'source/.*'
# Headers nostres (excloem source/external/ que conté dependències de tercers no editables)
HeaderFilterRegex: 'source/(core|game|utils)/'
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
# =====================================================================
# CONSTANTES → UPPER_CASE (compile-time y runtime, en cualquier scope)
# =====================================================================
# Todo lo que sea const o constexpr se identifica visualmente en UPPER_CASE,
# sin importar si es global, local, miembro o static.
# constexpr en cualquier scope (globales y locales)
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE }
# Constantes globales (const no-constexpr)
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
# Constantes locales (const en función)
- { key: readability-identifier-naming.LocalConstantCase, value: UPPER_CASE }
# Static const a nivel de archivo/namespace
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }
# Miembros static const/constexpr de clase (p.ej. static constexpr int MAX = 100;)
- { key: readability-identifier-naming.ClassConstantCase, value: UPPER_CASE }
# Miembros const no-static de clase (p.ej. const int limit;)
- { key: readability-identifier-naming.ConstantMemberCase, value: UPPER_CASE }
# Valores de enums
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
# NOTA: Los parámetros const NO se tratan como constantes aquí.
# Un parámetro sigue siendo un parámetro aunque sea const → hereda ParameterCase.
# =====================================================================
# VARIABLES NO-CONST
# =====================================================================
# Variables locales
- { key: readability-identifier-naming.VariableCase, value: lower_case }
- { key: readability-identifier-naming.LocalVariableCase, value: lower_case }
# Miembros privados en snake_case con sufijo _
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
# Parámetros de función
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
# Miembros protegidos en snake_case con sufijo _
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
# Miembros públicos en snake_case (sin sufijo)
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
# Namespaces en CamelCase
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
# Variables estáticas privadas como miembros privados
# Variables estáticas no-const (static locales, static file-scope,
# y static members no-const de clase como el instance_ de un Singleton).
# Sufijo _ para marcar que tienen storage estático.
- { 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 }
# =====================================================================
# MIEMBROS DE CLASE NO-CONST
# =====================================================================
# Privados: snake_case con sufijo _
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
# Constantes globales en UPPER_CASE
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
# Protegidos: snake_case con sufijo _
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
# Variables constexpr globales en UPPER_CASE
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE }
# Públicos: snake_case sin sufijo
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
# Constantes locales en UPPER_CASE
- { key: readability-identifier-naming.LocalConstantCase, value: UPPER_CASE }
# Constexpr miembros en UPPER_CASE (sin sufijo)
- { key: readability-identifier-naming.ConstexprMemberCase, value: UPPER_CASE }
# Constexpr miembros privados/protegidos con sufijo _
- { key: readability-identifier-naming.ConstexprMethodCase, value: UPPER_CASE }
# Clases, structs y enums en CamelCase
# =====================================================================
# TIPOS
# =====================================================================
- { key: readability-identifier-naming.ClassCase, value: CamelCase }
- { key: readability-identifier-naming.StructCase, value: CamelCase }
- { key: readability-identifier-naming.EnumCase, value: CamelCase }
- { key: readability-identifier-naming.UnionCase, value: CamelCase }
- { key: readability-identifier-naming.TypeAliasCase, value: CamelCase }
- { key: readability-identifier-naming.TypedefCase, value: CamelCase }
- { key: readability-identifier-naming.TemplateParameterCase, value: CamelCase }
# Valores de enums en UPPER_CASE
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
# Namespaces
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
# Métodos en camelBack (sin sufijos)
# =====================================================================
# FUNCIONES Y MÉTODOS (incluyendo constexpr)
# =====================================================================
# Un método/función constexpr es un invocable, no una constante → camelBack.
- { key: readability-identifier-naming.FunctionCase, value: camelBack }
- { key: readability-identifier-naming.ConstexprFunctionCase, value: camelBack }
- { key: readability-identifier-naming.MethodCase, value: camelBack }
- { key: readability-identifier-naming.PrivateMethodCase, value: camelBack }
- { key: readability-identifier-naming.ProtectedMethodCase, value: camelBack }
- { key: readability-identifier-naming.PublicMethodCase, value: camelBack }
# Funciones en camelBack
- { key: readability-identifier-naming.FunctionCase, value: camelBack }
# Parámetros en lower_case
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
- { key: readability-identifier-naming.ConstexprMethodCase, value: camelBack }
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Pre-commit hook: aplica clang-format als fitxers C++ staged abans del commit.
# - Només toca fitxers staged dins source/ (exclou source/external/).
# - Avorta el commit si hi ha canvis NO staged en aquests fitxers (per no incloure'ls sense voler).
set -euo pipefail
if ! command -v clang-format >/dev/null 2>&1; then
echo "pre-commit: clang-format no trobat — saltant format check" >&2
exit 0
fi
mapfile -t STAGED < <(git diff --cached --name-only --diff-filter=ACMR \
| grep -E '^source/.*\.(cpp|hpp|h)$' \
| grep -vE '^source/external/' || true)
if [ ${#STAGED[@]} -eq 0 ]; then
exit 0
fi
UNSTAGED_DIRTY=()
for f in "${STAGED[@]}"; do
if ! git diff --quiet -- "$f"; then
UNSTAGED_DIRTY+=("$f")
fi
done
if [ ${#UNSTAGED_DIRTY[@]} -gt 0 ]; then
echo "pre-commit: aquests fitxers tenen canvis NO staged i estan al commit." >&2
echo " Fes 'git add' o 'git stash' abans de continuar:" >&2
printf ' %s\n' "${UNSTAGED_DIRTY[@]}" >&2
exit 1
fi
clang-format -i "${STAGED[@]}"
git add -- "${STAGED[@]}"
# --- clang-tidy només sobre els fitxers staged ---
if ! command -v clang-tidy >/dev/null 2>&1; then
echo "pre-commit: clang-tidy no trobat — saltant tidy" >&2
exit 0
fi
REPO_ROOT="$(git rev-parse --show-toplevel)"
BUILD_DIR="$REPO_ROOT/build"
if [ ! -f "$BUILD_DIR/compile_commands.json" ]; then
echo "pre-commit: generant compile_commands.json (build dir buit)..." >&2
cmake -S "$REPO_ROOT" -B "$BUILD_DIR" >/dev/null
fi
echo "pre-commit: clang-tidy sobre ${#STAGED[@]} fitxer(s)..." >&2
if ! clang-tidy -p "$BUILD_DIR" --quiet "${STAGED[@]}"; then
echo "pre-commit: clang-tidy ha trobat errors — commit avortat" >&2
exit 1
fi
# --- cppcheck només sobre els .cpp staged ---
if ! command -v cppcheck >/dev/null 2>&1; then
echo "pre-commit: cppcheck no trobat — saltant cppcheck" >&2
exit 0
fi
CPP_STAGED=()
for f in "${STAGED[@]}"; do
[[ "$f" == *.cpp ]] && CPP_STAGED+=("$f")
done
if [ ${#CPP_STAGED[@]} -eq 0 ]; then
exit 0
fi
echo "pre-commit: cppcheck sobre ${#CPP_STAGED[@]} fitxer(s)..." >&2
if ! cppcheck \
--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/*' \
--suppress=normalCheckLevelMaxBranches \
-D_DEBUG \
-DLINUX_BUILD \
--quiet \
--error-exitcode=1 \
-I "$REPO_ROOT/source" \
"${CPP_STAGED[@]}"; then
echo "pre-commit: cppcheck ha trobat errors — commit avortat" >&2
exit 1
fi
+24 -18
View File
@@ -169,28 +169,20 @@ if(NOT APPLE AND NOT EMSCRIPTEN)
set(SHADER_POSTFX_VERT_SRC "${SHADERS_DIR}/postfx.vert")
set(SHADER_POSTFX_FRAG_SRC "${SHADERS_DIR}/postfx.frag")
set(SHADER_UPSCALE_FRAG_SRC "${SHADERS_DIR}/upscale.frag")
set(SHADER_DOWNSCALE_FRAG_SRC "${SHADERS_DIR}/downscale.frag")
set(SHADER_CRTPI_FRAG_SRC "${SHADERS_DIR}/crtpi_frag.glsl")
set(SHADER_POSTFX_VERT_H "${HEADERS_DIR}/postfx_vert_spv.h")
set(SHADER_POSTFX_FRAG_H "${HEADERS_DIR}/postfx_frag_spv.h")
set(SHADER_UPSCALE_FRAG_H "${HEADERS_DIR}/upscale_frag_spv.h")
set(SHADER_DOWNSCALE_FRAG_H "${HEADERS_DIR}/downscale_frag_spv.h")
set(SHADER_CRTPI_FRAG_H "${HEADERS_DIR}/crtpi_frag_spv.h")
set(ALL_SHADER_HEADERS
"${SHADER_POSTFX_VERT_H}"
"${SHADER_POSTFX_FRAG_H}"
"${SHADER_UPSCALE_FRAG_H}"
"${SHADER_DOWNSCALE_FRAG_H}"
"${SHADER_CRTPI_FRAG_H}"
)
set(ALL_SHADER_SOURCES
"${SHADER_POSTFX_VERT_SRC}"
"${SHADER_POSTFX_FRAG_SRC}"
"${SHADER_UPSCALE_FRAG_SRC}"
"${SHADER_DOWNSCALE_FRAG_SRC}"
"${SHADER_CRTPI_FRAG_SRC}"
)
@@ -214,8 +206,7 @@ if(NOT APPLE AND NOT EMSCRIPTEN)
message(FATAL_ERROR
"glslc no encontrado y header SPIR-V no existe: ${HDR}\n"
" Instala glslc: sudo apt install glslang-tools (Linux)\n"
" choco install vulkan-sdk (Windows)\n"
" O genera los headers manualmente: tools/shaders/compile_spirv.sh"
" choco install vulkan-sdk (Windows)"
)
endif()
endforeach()
@@ -253,7 +244,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
# --- 4. CONFIGURACIÓN PLATAFORMAS Y COMPILADOR ---
# Configuración de flags de compilación
target_compile_options(${PROJECT_NAME} PRIVATE -Wall)
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:-Os -ffunction-sections -fdata-sections>)
# Definir _DEBUG en modo Debug y RELEASE_BUILD en modo Release
@@ -300,11 +291,6 @@ elseif(UNIX AND NOT APPLE)
target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD)
endif()
# 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
@@ -404,11 +390,11 @@ if(NOT EMSCRIPTEN)
source/core/resources/resource_pack.cpp
)
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
target_compile_options(pack_resources PRIVATE -Wall)
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
# Regeneració automàtica de resources.pack en cada build si canvia data/.
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
set(RESOURCE_PACK "${CMAKE_SOURCE_DIR}/resources.pack")
set(RESOURCE_PACK "${CMAKE_BINARY_DIR}/resources.pack")
add_custom_command(
OUTPUT ${RESOURCE_PACK}
@@ -423,4 +409,24 @@ if(NOT EMSCRIPTEN)
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
add_dependencies(${PROJECT_NAME} resource_pack)
# --- CÒPIA DE gamecontrollerdb.txt AL COSTAT DEL BINARI ---
# SDL_AddGamepadMappingsFromFile només llegeix del filesystem real (no del
# pack), així que el fitxer ha de viure al directori del binari. Es copia
# només si existeix per no fallar la build d'algú que encara no ha fet
# `make controllerdb`.
if(EXISTS "${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt")
set(CONTROLLER_DB "${CMAKE_BINARY_DIR}/gamecontrollerdb.txt")
add_custom_command(
OUTPUT ${CONTROLLER_DB}
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt"
"${CONTROLLER_DB}"
DEPENDS "${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt"
COMMENT "Copiant gamecontrollerdb.txt → build/"
VERBATIM
)
add_custom_target(controller_db ALL DEPENDS ${CONTROLLER_DB})
add_dependencies(${PROJECT_NAME} controller_db)
endif()
endif()
+142 -62
View File
@@ -3,13 +3,13 @@
# ==============================================================================
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
DIR_SOURCES := $(addsuffix /, $(DIR_ROOT)source)
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
BUILDDIR := build
# ==============================================================================
# TARGET NAMES
# ==============================================================================
TARGET_NAME := jaildoctors_dilemma
TARGET_FILE := $(DIR_BIN)$(TARGET_NAME)
TARGET_FILE := $(BUILDDIR)/$(TARGET_NAME)
APP_NAME := JailDoctor's Dilemma
DIST_DIR := dist
RELEASE_FOLDER := dist/_tmp
@@ -63,7 +63,7 @@ endif
# WINDOWS-SPECIFIC VARIABLES
# ==============================================================================
ifeq ($(OS),Windows_NT)
WIN_TARGET_FILE := $(DIR_BIN)$(APP_NAME)
WIN_TARGET_FILE := $(BUILDDIR)/$(APP_NAME)
WIN_RELEASE_FILE := $(RELEASE_FOLDER)/$(APP_NAME)
WIN_RELEASE_FILE_PS := $(subst ','',$(WIN_RELEASE_FILE))
else
@@ -96,12 +96,31 @@ else
endif
# ==============================================================================
# CMAKE GENERATOR (Windows needs explicit MinGW Makefiles generator)
# CMAKE GENERATOR (usa Ninja si está disponible; si no, MinGW Makefiles en
# Windows / generador por defecto en Linux/macOS). Ninja paraleliza mejor.
# ==============================================================================
ifeq ($(OS),Windows_NT)
CMAKE_GEN := -G "MinGW Makefiles"
# Dins MSYS2/Git Bash/MinGW, $(shell ...) usa sh.exe i "NUL" NO és
# dispositiu — un redirect "2>NUL" crearia un fitxer literal anomenat
# NUL al cwd. Detectem MSYSTEM per usar /dev/null en aquests entorns.
ifneq ($(MSYSTEM),)
NULDEV := /dev/null
else
NULDEV := NUL
endif
HAS_NINJA := $(shell ninja --version 2>$(NULDEV))
ifneq ($(HAS_NINJA),)
CMAKE_GEN := -G "Ninja"
else
CMAKE_GEN := -G "MinGW Makefiles"
endif
else
CMAKE_GEN :=
HAS_NINJA := $(shell ninja --version 2>/dev/null)
ifneq ($(HAS_NINJA),)
CMAKE_GEN := -G "Ninja"
else
CMAKE_GEN :=
endif
endif
# ==============================================================================
@@ -115,24 +134,35 @@ debug:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH)
@cmake --build build
run: all
@./$(TARGET_FILE)
run-debug: debug
@./$(TARGET_FILE)
clean:
@rm -rf $(BUILDDIR)
rebuild: clean all
# ==============================================================================
# RELEASE AUTOMÁTICO (detecta SO)
# ==============================================================================
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
# ==============================================================================
# REGLAS PARA COMPILACIÓN DE SHADERS (multiplataforma via cmake)
# ==============================================================================
compile_shaders:
compile-shaders:
ifdef GLSLC
@cmake -D GLSLC=$(GLSLC) -D SHADERS_DIR=$(SHADERS_DIR) -D HEADERS_DIR=$(HEADERS_DIR) -P $(SHADER_CMAKE)
else
@@ -145,12 +175,12 @@ endif
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
@./build/pack_resources data build/resources.pack
# ==============================================================================
# COMPILACIÓN PARA WINDOWS (RELEASE)
# ==============================================================================
_windows_release:
_windows-release:
@echo off
@echo Creando release para Windows - Version: $(VERSION)
@@ -164,7 +194,7 @@ _windows_release:
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
# Copia ficheros
@powershell -Command "Copy-Item -Path 'resources.pack' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item -Path 'build/resources.pack' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
@@ -183,15 +213,31 @@ _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 -DGIT_HASH=$(GIT_HASH)
@cmake --build build/intel
# Verifica dependencias necesarias (create-dmg). Si falta, intenta instalarla
# con brew; si brew tampoco está, indica el comando exacto al usuario.
@command -v create-dmg >/dev/null 2>&1 || { \
echo ""; \
echo "============================================"; \
echo " Falta la dependencia: create-dmg"; \
echo "============================================"; \
if command -v brew >/dev/null 2>&1; then \
echo " Instalando con: brew install create-dmg"; \
brew install create-dmg || { \
echo ""; \
echo " ERROR: 'brew install create-dmg' ha fallado."; \
echo " Ejecuta el comando manualmente y vuelve a probar."; \
exit 1; \
}; \
else \
echo " Homebrew no está instalado."; \
echo " Instálalo desde https://brew.sh y luego ejecuta:"; \
echo " brew install create-dmg"; \
exit 1; \
fi; \
}
# Elimina datos de compilaciones anteriores
$(RMDIR) "$(RELEASE_FOLDER)"
@@ -205,12 +251,11 @@ _macos_release:
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS"
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
# Copia carpetas y ficheros
cp resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
# Copia carpetas y ficheros del bundle (resources.pack se generará al compilar)
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
cp release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp LICENSE "$(RELEASE_FOLDER)"
cp README.md "$(RELEASE_FOLDER)"
@@ -220,32 +265,53 @@ _macos_release:
sed -i '' '/<key>CFBundleShortVersionString<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"; \
sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
# Copia el ejecutable Intel al bundle
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
# Firma la aplicación
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
# Empaqueta el .dmg de la versión Intel con create-dmg
@echo "Creando DMG Intel con iconos de 96x96..."
create-dmg \
--volname "$(APP_NAME)" \
--window-pos 200 120 \
--window-size 720 300 \
--icon-size 96 \
--text-size 12 \
--icon "$(APP_NAME).app" 278 102 \
--icon "LICENSE" 441 102 \
--icon "README.md" 604 102 \
--app-drop-link 115 102 \
--hide-extension "$(APP_NAME).app" \
"$(MACOS_INTEL_RELEASE)" \
"$(RELEASE_FOLDER)" || true
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"
# Compila y empaqueta la versión Intel (best-effort: si falla, se omite el
# DMG Intel y continúa con la build de Apple Silicon).
@echo ""
@echo "============================================"
@echo " Compilando version Intel (x86_64)"
@echo "============================================"
@if 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; then \
cp build/resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"; \
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"; \
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"; \
echo "Creando DMG Intel con iconos de 96x96..."; \
create-dmg \
--volname "$(APP_NAME)" \
--window-pos 200 120 \
--window-size 720 300 \
--icon-size 96 \
--text-size 12 \
--icon "$(APP_NAME).app" 278 102 \
--icon "LICENSE" 441 102 \
--icon "README.md" 604 102 \
--app-drop-link 115 102 \
--hide-extension "$(APP_NAME).app" \
"$(MACOS_INTEL_RELEASE)" \
"$(RELEASE_FOLDER)" || true; \
echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"; \
else \
echo ""; \
echo "============================================"; \
echo " WARNING: la build Intel ha fallado."; \
echo " Se omite el DMG Intel y se continúa con"; \
echo " la build de Apple Silicon."; \
echo "============================================"; \
echo ""; \
fi
# Compila la versión para procesadores Apple Silicon con cmake
@echo ""
@echo "============================================"
@echo " Compilando version Apple Silicon (arm64)"
@echo "============================================"
@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 build/resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
# Firma la aplicación
@@ -277,7 +343,7 @@ _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)
@@ -289,7 +355,7 @@ _linux_release:
$(MKDIR) "$(RELEASE_FOLDER)"
# Copia ficheros
cp resources.pack "$(RELEASE_FOLDER)"
cp build/resources.pack "$(RELEASE_FOLDER)"
cp LICENSE "$(RELEASE_FOLDER)"
cp README.md "$(RELEASE_FOLDER)"
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
@@ -327,20 +393,20 @@ wasm:
@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:
# 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/"
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/"
# ==============================================================================
# ==============================================================================
@@ -366,6 +432,13 @@ cppcheck:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target cppcheck
# ==============================================================================
# GIT HOOKS
# ==============================================================================
hooks-install:
@git config core.hooksPath .githooks
@echo "Git hooks activats: $(shell pwd)/.githooks"
# DESCARGA DE GAMECONTROLLERDB
# ==============================================================================
controllerdb:
@@ -378,7 +451,7 @@ controllerdb:
# REGLAS ESPECIALES
# ==============================================================================
# Regla para mostrar la versión actual
show_version:
show-version:
@echo "Version actual: $(VERSION)"
# Regla de ayuda
@@ -390,14 +463,18 @@ help:
@echo " make - Compilar con cmake (Release)"
@echo " make debug - Compilar con cmake (Debug)"
@echo ""
@echo " Ejecucion:"
@echo " make run - Compilar (Release) y ejecutar"
@echo " make run-debug - Compilar (Debug) y ejecutar"
@echo ""
@echo " Release:"
@echo " make release - Crear release (detecta SO automaticamente)"
@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 " 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 - Empaquetar recursos a resources.pack"
@echo " make compile-shaders - Compilar shaders SPIR-V"
@echo " make pack - Empaquetar recursos a $(BUILDDIR)/resources.pack"
@echo " make controllerdb - Descargar gamecontrollerdb.txt actualizado"
@echo ""
@echo " Calidad de codigo:"
@@ -408,7 +485,10 @@ help:
@echo " make cppcheck - Analisis estatico con cppcheck"
@echo ""
@echo " Otros:"
@echo " make show_version - Mostrar version actual ($(VERSION))"
@echo " make clean - Borrar carpeta $(BUILDDIR)/"
@echo " make rebuild - clean + all"
@echo " make show-version - Mostrar version actual ($(VERSION))"
@echo " make hooks-install - Activar git hooks del proyecto"
@echo " make help - Mostrar esta ayuda"
.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
.PHONY: all debug run run-debug clean rebuild release _windows-release _macos-release _linux-release wasm wasm-debug compile-shaders pack controllerdb format format-check tidy tidy-fix cppcheck hooks-install show-version help
-9
View File
@@ -21,15 +21,6 @@ categories:
- name: VIDEO
scope: game
commands:
- keyword: SS
handler: cmd_ss
description: Supersampling
usage: "SS [ON|OFF|SIZE|UPSCALE [NEAREST|LINEAR]|DOWNSCALE [BILINEAR|LANCZOS2|LANCZOS3]]"
completions:
SS: [ON, OFF, SIZE, UPSCALE, DOWNSCALE]
SS UPSCALE: [NEAREST, LINEAR]
SS DOWNSCALE: [BILINEAR, LANCZOS2, LANCZOS3]
- keyword: SHADER
handler: cmd_shader
description: "Toggle/select shader (F4)"
-2
View File
@@ -116,8 +116,6 @@ ui:
shader: "SHADER"
postfx: "POSTFX"
crtpi: "CRTPI"
supersampling_enabled: "SUPERMOSTREIG ACTIVAT"
supersampling_disabled: "SUPERMOSTREIG DESACTIVAT"
palette: "PALETA"
palette_sort: "ORDENACIÓ PALETA"
integer_scale_enabled: "ESCALAT SENCER ACTIVAT"
-2
View File
@@ -116,8 +116,6 @@ ui:
shader: "SHADER"
postfx: "POSTFX"
crtpi: "CRTPI"
supersampling_enabled: "SUPERSAMPLING ON"
supersampling_disabled: "SUPERSAMPLING OFF"
palette: "PALETTE"
palette_sort: "PALETTE SORT"
integer_scale_enabled: "INTEGER SCALE ENABLED"
-48
View File
@@ -1,48 +0,0 @@
#version 450
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_color;
layout(set = 2, binding = 0) uniform sampler2D source;
layout(set = 3, binding = 0) uniform DownscaleUniforms {
int algorithm; // 0 = Lanczos2 (ventana 2, ±2 taps), 1 = Lanczos3 (ventana 3, ±3 taps)
float pad0;
float pad1;
float pad2;
} u;
// Kernel Lanczos normalizado: sinc(t) * sinc(t/a) para |t| < a, 0 fuera.
float lanczos(float t, float a) {
t = abs(t);
if (t < 0.0001) { return 1.0; }
if (t >= a) { return 0.0; }
const float PI = 3.14159265358979;
float pt = PI * t;
return (a * sin(pt) * sin(pt / a)) / (pt * pt);
}
void main() {
vec2 src_size = vec2(textureSize(source, 0));
// Posición en coordenadas de texel (centros de texel en N+0.5)
vec2 p = v_uv * src_size;
vec2 p_floor = floor(p);
float a = (u.algorithm == 0) ? 2.0 : 3.0;
int win = int(a);
vec4 color = vec4(0.0);
float weight_sum = 0.0;
for (int j = -win; j <= win; j++) {
for (int i = -win; i <= win; i++) {
// Centro del texel (i,j) relativo a p_floor
vec2 tap_center = p_floor + vec2(float(i), float(j)) + 0.5;
vec2 offset = tap_center - p;
float w = lanczos(offset.x, a) * lanczos(offset.y, a);
color += texture(source, tap_center / src_size) * w;
weight_sum += w;
}
}
out_color = (weight_sum > 0.0) ? (color / weight_sum) : vec4(0.0, 0.0, 0.0, 1.0);
}
+47 -25
View File
@@ -6,7 +6,9 @@
// xxd -i postfx.frag.spv > ../../source/core/rendering/sdl3gpu/postfx_frag_spv.h
//
// PostFXUniforms must match exactly the C++ struct in sdl3gpu_shader.hpp
// (8 floats, 32 bytes, std140/scalar layout).
// (16 floats = 4 × vec4 = 64 bytes, std140/scalar layout).
// IMPORTANT: Qualsevol canvi ací cal replicar-lo a mà a
// source/core/rendering/sdl3gpu/msl/postfx_frag.msl.h (no hi ha generador).
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_color;
@@ -15,7 +17,7 @@ layout(set = 2, binding = 0) uniform sampler2D scene;
layout(set = 3, binding = 0) uniform PostFXUniforms {
float vignette_strength;
float chroma_strength;
float chroma_min; // intensitat mínima de l'aberració cromàtica
float scanline_strength;
float screen_height;
float mask_strength;
@@ -24,10 +26,28 @@ layout(set = 3, binding = 0) uniform PostFXUniforms {
float bleeding;
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
float time; // seconds since SDL init
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — 48 bytes total (3 × 16)
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
float chroma_max; // intensitat màxima; si == chroma_min → chroma estàtic
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
float scan_dark_ratio; // fracció de subfila fosca per fila lògica (1/3 ≈ 0.333)
float scan_dark_floor; // multiplicador de brillantor de la subfila fosca
float scan_edge_soft; // 0 = step dur; 1 = suavitzat d'1 píxel físic (estil crtpi)
float pad3; // padding per tancar a 64 bytes (4 × vec4)
} u;
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
// NEAREST quan l'offset de chroma és subpíxel: sense interpolar, l'offset
// arrodonia entre 1 i 2 píxels i el drift temporal feia un parpelleig discret.
float sampleBilinearX(vec2 uv_target, int channel) {
vec2 tex_size = vec2(textureSize(scene, 0));
float px = uv_target.x * tex_size.x - 0.5;
float p_floor = floor(px);
float f = px - p_floor;
vec4 c0 = texture(scene, vec2((p_floor + 0.5) / tex_size.x, uv_target.y));
vec4 c1 = texture(scene, vec2((p_floor + 1.5) / tex_size.x, uv_target.y));
return mix(c0[channel], c1[channel], f);
}
// YCbCr helpers for NTSC bleeding
vec3 rgb_to_ycc(vec3 rgb) {
return vec3(
@@ -69,11 +89,11 @@ void main() {
vec3 base = texture(scene, uv).rgb;
// Sangrado NTSC — difuminado horizontal de crominancia.
// step = 1 pixel lógico de juego en UV (corrige SS: textureSize.x = game_w * oversample).
// step = 1 pixel lógico de juego en UV.
vec3 colour;
if (u.bleeding > 0.0) {
float tw = float(textureSize(scene, 0).x);
float step = u.oversample / tw; // 1 pixel lógico en UV
float step = 1.0 / tw; // 1 pixel lógico en UV
vec3 ycc = rgb_to_ycc(base);
vec3 ycc_l2 = rgb_to_ycc(texture(scene, uv - vec2(2.0*step, 0.0)).rgb);
vec3 ycc_l1 = rgb_to_ycc(texture(scene, uv - vec2(1.0*step, 0.0)).rgb);
@@ -85,10 +105,14 @@ void main() {
colour = base;
}
// Aberración cromática (drift animado con time para efecto NTSC real)
float ca = u.chroma_strength * 0.005 * (1.0 + 0.15 * sin(u.time * 7.3));
colour.r = texture(scene, uv + vec2(ca, 0.0)).r;
colour.b = texture(scene, uv - vec2(ca, 0.0)).b;
// Aberración cromática — intensitat varia entre chroma_min i chroma_max amb
// una sinusoidal (si min == max, queda estàtica). Mostreig bilinear horitzontal
// per evitar el "tic-tac" del NEAREST sampler quan l'offset és subpíxel.
if (u.chroma_min > 0.0 || u.chroma_max > 0.0) {
float ca = mix(u.chroma_min, u.chroma_max, 0.5 + 0.5 * sin(u.time * 7.3)) * 0.005;
colour.r = sampleBilinearX(uv + vec2(ca, 0.0), 0);
colour.b = sampleBilinearX(uv - vec2(ca, 0.0), 2);
}
// Corrección gamma (linealizar antes de scanlines, codificar después)
if (u.gamma_strength > 0.0) {
@@ -96,22 +120,20 @@ void main() {
colour = mix(colour, lin, u.gamma_strength);
}
// Scanlines — proporción 2/3 brillantes + 1/3 oscuras por fila lógica.
// Casos especiales: 1 subfila → sin efecto; 2 subfilas → 1+1 (50/50).
// Constantes ajustables:
const float SCAN_DARK_RATIO = 0.333; // fracción de subfilas oscuras (ps >= 3)
const float SCAN_DARK_FLOOR = 0.42; // multiplicador de brillo de subfilas oscuras
// Scanlines — tècnica dels 3 subpíxels verticals per píxel lògic (aee/projecte_2026):
// franja fosca ocupant `scan_dark_ratio` al final de cada fila lògica. La transició es
// suavitza amb smoothstep d'ample ≈ 1 píxel físic (estil crtpi: filtratge analític
// continu), controlat per `scan_edge_soft`. A 0 és equivalent al step dur antic.
if (u.scanline_strength > 0.0) {
float ps = max(1.0, round(u.pixel_scale));
float frac_in_row = fract(uv.y * u.screen_height);
float row_pos = floor(frac_in_row * ps);
// bright_rows: cuántas subfilas son brillantes
// ps==1 → ps (todo brillante → is_dark nunca se activa)
// ps==2 → 1 brillante + 1 oscura
// ps>=3 → floor(ps * (1 - DARK_RATIO)) brillantes
float bright_rows = (ps < 2.0) ? ps : ((ps < 3.0) ? 1.0 : floor(ps * (1.0 - SCAN_DARK_RATIO)));
float is_dark = step(bright_rows, row_pos);
float scan = mix(1.0, SCAN_DARK_FLOOR, is_dark);
float ps = max(u.pixel_scale, 1.0);
float sub = fract(uv.y * u.screen_height); // [0,1) dins la fila lògica
float dark_center = 1.0 - u.scan_dark_ratio * 0.5; // centre de la franja fosca
float d = abs(sub - dark_center);
d = min(d, 1.0 - d); // wrap a la fila següent
float half_width = u.scan_dark_ratio * 0.5;
float softness = u.scan_edge_soft * 0.5 / ps; // mig píxel físic a cada costat
float band = 1.0 - smoothstep(half_width - softness, half_width + softness, d);
float scan = mix(1.0, u.scan_dark_floor, band);
colour *= mix(1.0, scan, u.scanline_strength);
}
-15
View File
@@ -1,15 +0,0 @@
#version 450
// Vulkan GLSL fragment shader — Nearest-neighbour upscale pass
// Used as the first render pass when supersampling is active.
// Compile: glslc upscale.frag -o upscale.frag.spv
// xxd -i upscale.frag.spv > ../../source/core/rendering/sdl3gpu/upscale_frag_spv.h
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_color;
layout(set = 2, binding = 0) uniform sampler2D scene;
void main() {
out_color = texture(scene, v_uv);
}
+553
View File
@@ -0,0 +1,553 @@
# Arquitectura de **JailDoctor's Dilemma**
> Guía de orientación para un desarrollador nuevo en el proyecto.
>
> Cada afirmación está anclada a código real: se cita el fichero (y, cuando
> ayuda, la función o el número de línea) que la respalda. Donde el código
> contradice a la documentación previa (`CLAUDE.md`), lo señalo: **manda el
> código**.
>
> **JailDoctor's Dilemma** es un *puzzle-platformer* 2D retro en C++20 + SDL3:
> 60+ habitaciones interconectadas, ítems coleccionables, enemigos y logros.
> Resolución de juego **256×192**. Los comentarios del código están en
> español/valenciano; este documento está en castellano.
---
## Índice
1. [Visión general](#1-visión-general)
2. [Punto de entrada y bucle principal](#2-punto-de-entrada-y-bucle-principal)
3. [Escenas y flujo de la aplicación](#3-escenas-y-flujo-de-la-aplicación)
4. [Renderizado: de la lógica al píxel](#4-renderizado-de-la-lógica-al-píxel)
5. [Entrada](#5-entrada)
6. [Lógica del juego: la escena `Game`](#6-lógica-del-juego-la-escena-game)
7. [Habitaciones y colisión](#7-habitaciones-y-colisión)
8. [Entidades](#8-entidades)
9. [Logros, estadísticas y marcador](#9-logros-estadísticas-y-marcador)
10. [Editor de mapas (Debug)](#10-editor-de-mapas-debug)
11. [Consola y notificaciones](#11-consola-y-notificaciones)
12. [Modo demo](#12-modo-demo)
13. [Recursos](#13-recursos)
14. [Audio, localización y configuración](#14-audio-localización-y-configuración)
15. [Convenciones y patrones recurrentes](#15-convenciones-y-patrones-recurrentes)
16. [Guía de navegación: "si quieres tocar X, mira Y"](#16-guía-de-navegación-si-quieres-tocar-x-mira-y)
---
## 1. Visión general
El árbol `source/` separa **motor** y **juego**:
- **`source/core/`** — motor genérico: `system` (`director`, `debug`,
`global_events`), `rendering` (+ `sprite`, `sdl3gpu`), `input`, `resources`,
`audio`, `locale`.
- **`source/game/`** — el juego concreto: `scenes/`, `gameplay/`, `entities/`,
`editor/`, `ui/`, `options.*`, `scene_manager.hpp`, `defaults.hpp`.
- **`source/utils/`** — `delta_timer`, `easing_functions`, `utils`, `defines`.
- **`source/external/`** — vendorizado: `fkyaml`, `stb_image`, `stb_vorbis`.
Es el proyecto más grande de su familia: **138 ficheros C++, ~54.000 líneas**.
**Ideas-fuerza que conviene interiorizar:**
1. **Render paletizado por CPU**: `Surface` de 8 bits indexados + paleta, igual
filosofía que un motor retro clásico; la GPU solo escala y aplica post-FX
(§4).
2. **Flujo por `SceneManager::current`** (variable global) + un único
`active_scene_` que el `Director` conmuta (§3).
3. **El mundo son habitaciones** de 256×128 px en tiles de 8 px, con colisión
por superficies (suelos, paredes, rampas, cintas) y transición entre salas
contiguas (§7).
4. Trae **editor de mapas** y **consola de comandos** integrados (solo Debug)
(§10, §11), un **sistema de logros** persistente (§9), y un **modo demo**
que es un *tour de habitaciones* (§12).
```mermaid
graph TD
SDL[SDL3 callbacks · main.cpp] --> DIR[Director]
DIR -->|SceneManager::current| SW{switchToActiveScene}
SW --> SCN["BootLoader / Logo / Title / Game / Demo / Ending…"]
SCN --> GAME["Game (Mode GAME/DEMO)"]
GAME --> ROOM[Room + colisión] & RL[room_loader]
GAME --> PLAYER[Player] & EM[enemy_manager] & IM[item_manager]
GAME --> CHV[Cheevos] & STT[Stats] & SCB[Scoreboard]
GAME -->|blit paletizado| SURF["Surface (8-bit indexed)"]
SURF -->|toARGBBuffer / copyToTexture| SCREEN[Screen]
SCREEN --> GPU["ShaderBackend PostFX/CrtPi"] --> WIN[Ventana]
SCREEN -.fallback.-> WIN
RES["Resource::Cache / List"] -.-> GAME & SCN
EDIT["MapEditor (Debug)"] -.-> GAME
CON[Console] -.-> GAME
```
---
## 2. Punto de entrada y bucle principal
### 2.1. SDL conduce el bucle (callbacks)
`source/main.cpp` define `SDL_MAIN_USE_CALLBACKS`: no hay `while` propio.
```cpp
SDL_AppInit new Director();
SDL_AppIterate Director::iterate(); // un frame
SDL_AppEvent Director::handleEvent(event);
SDL_AppQuit delete Director;
```
> ⚠️ **Discrepancia con el `CLAUDE.md`**: este describe un `Director::run()` con
> un bucle `while (SceneManager::current != QUIT)`. **El código actual no es
> así**: usa la API de callbacks de SDL3. El `Director` real
> (`core/system/director.hpp`) expone `iterate()` y `handleEvent()`, no `run()`.
### 2.2. El `Director`
`source/core/system/director.{hpp,cpp}`. Mantiene **un solo
`std::unique_ptr<Scene> active_scene_`** y un enum `current_scene_`. No guarda un
puntero por escena (a diferencia de los proyectos hermanos): construye la escena
bajo demanda en `switchToActiveScene()` (`director.cpp`).
El constructor inicializa los subsistemas en orden: `Resource::List` (registro
de assets desde `config/assets.yaml`), `Options`, `Audio`, `Screen`, `Input`,
`Resource::Cache` (con `beginLoad()`), y arranca en la escena `BOOT_LOADER`.
### 2.3. Arranque NO bloqueante
`Resource::Cache` no se carga de golpe. El constructor deja la escena en
`BOOT_LOADER` (una barra de progreso) y cada frame `Director::iterate()` llama a
`Resource::Cache::get()->loadStep(50 /*ms*/)` (`director.cpp`): carga assets
hasta agotar un presupuesto de 50 ms por frame, manteniendo ventana y eventos
vivos. Cuando termina, `finishBoot()` inicializa lo que depende de los recursos
(`Notifier`, `RenderInfo`, `Console`, `Cheevos`, `Locale`, en Debug `Debug` y
`MapEditor`) y fija la escena destino (`LOGO` en release; en Debug, la que diga
`debug.yaml` vía `Debug::getInitialScene()`).
### 2.4. Gestión del tiempo
**Time-based**: la escena `Game` posee un `DeltaTimer delta_timer_`
(`utils/delta_timer.hpp`) y toda la física/animación consume `delta_time` en
segundos. Las constantes de tiempo se documentan como "N frames a 66.67 fps →
segundos" (p.ej. `BLACK_SCREEN_DURATION = 0.30F`, `game.hpp:46`).
---
## 3. Escenas y flujo de la aplicación
### 3.1. La base `Scene` y el `SceneManager`
`source/game/scenes/scene.hpp` es minimalista:
```cpp
class Scene {
public:
virtual void iterate() = 0; // un frame (update + render)
virtual void handleEvent(const SDL_Event&) = 0; // un evento
};
```
`source/game/scene_manager.hpp` define el flujo con **variables globales
`inline`** en el namespace `SceneManager`:
```cpp
enum class Scene { BOOT_LOADER, LOGO, LOADING_SCREEN, TITLE, CREDITS,
GAME, DEMO, GAME_OVER, ENDING, ENDING2, RESTART_CURRENT, QUIT };
inline Scene current = Scene::BOOT_LOADER;
inline Options options = Options::LOGO_TO_LOADING_SCREEN;
inline Scene scene_before_restart = Scene::LOGO;
```
Cualquier escena solicita una transición asignando `SceneManager::current`.
### 3.2. La conmutación
`Director::switchToActiveScene()` (`director.cpp`):
- `RESTART_CURRENT` es especial: restaura `scene_before_restart` (relanza la
escena que estaba activa).
- `active_scene_.reset()` destruye la anterior (su destructor puede parar la
música, etc.).
- Un `switch` construye la concreta: `BootLoader`, `Logo`, `LoadingScreen`,
`Title`, `Credits`, `Game(Mode::DEMO)`, `Game(Mode::GAME)`, `GameOver`,
`Ending`, `Ending2`.
Nótese que **DEMO y GAME son la misma clase `Game`**, parametrizada por
`Game::Mode` (§12).
```mermaid
graph LR
BOOT[BOOT_LOADER] --> LOGO --> LOADING[LOADING_SCREEN] --> TITLE
TITLE -->|jugar| GAME --> ENDING --> ENDING2 --> CREDITS
TITLE -->|attract| DEMO --> TITLE
GAME --> GAME_OVER --> TITLE
TITLE --> QUIT
```
---
## 4. Renderizado: de la lógica al píxel
El render es **paletizado por CPU**: se dibuja sobre superficies de 8 bits
indexados y solo al final se sube a la GPU.
### 4.1. `Surface`: 8 bits indexados + paleta
`source/core/rendering/surface.hpp`. Una `Surface` guarda los píxeles como
**índices `Uint8`** (`SurfaceData`) más una **`Palette` de 256 colores ARGB** y
una **`SubPalette`** (remapeo de índices, identidad por defecto vía `std::iota`).
Operaciones clave:
- `render(...)` / `renderWithColorReplace(src, dst)` — blit con color
transparente y reemplazo de índice (para recolorear sprites/glifos).
- `renderWithVerticalFade(...)` — disolución por hash 2D (cantos).
- `fadePalette()` / `fadeSubPalette()` — fundidos manipulando la paleta.
- `copyToTexture(...)` y **`toARGBBuffer(buffer)`** — vuelcan la surface a una
`SDL_Texture` o a un buffer ARGB externo.
Sobre `Surface` se construyen los sprites (`core/rendering/sprite/`):
`Sprite``AnimatedSprite` (frames `.yaml`) → `MovingSprite` (posición/velocidad)
y `DissolveSprite` (transición). Texto: `text.*`. Efectos: `pixel_reveal.*`.
Paletas: `palette_manager.*` (el juego permite **cambiar de paleta en caliente**;
ver §5).
### 4.2. `Screen` y la composición
`source/core/rendering/screen.{hpp,cpp}`. Hay dos superficies/texturas:
- **`game_surface_` / `game_texture_`** — el canvas de juego 256×192
(`SDL_TEXTUREACCESS_STREAMING`, ARGB8888; `screen.cpp:125`).
- **`border_surface_` / `border_texture_`** — el borde/overscan alrededor del
canvas.
El path de presentación (`Screen::render`, `screen.cpp:197`):
- **Con backend GPU acelerado**: vuelca las superficies a buffers ARGB
(`toARGBBuffer`) y los sube al `shader_backend_` (`uploadPixels`), que renderiza
con el shader activo.
- **Sin backend** (fallback): `copyToTexture` + `SDL_RenderTexture` de
`game_texture_` y `border_texture_` a la ventana.
El backend vive en `core/rendering/sdl3gpu/` (interfaz `shader_backend.hpp`). Dos
shaders: **PostFX** y **CrtPi** (scanlines, curvatura, máscara, etc.), GLSL en
`data/shaders/` compilados a SPIR-V (`spv/*_spv.h`), o Metal (MSL) en macOS
(`sdl3gpu/msl/`). En Emscripten (`NO_SHADERS`) se fuerza la ruta clásica.
```mermaid
graph TD
OBJ["room, player, enemies, items, HUD…"] -->|blit índices| GS["game_surface_ (256×192, 8-bit)"]
BORDER["borde / overscan"] --> BS[border_surface_]
GS -->|toARGBBuffer / copyToTexture| SCREEN[Screen]
BS --> SCREEN
SCREEN -->|uploadPixels| SHADER["ShaderBackend (PostFX / CrtPi)"]
SHADER --> WIN[Ventana]
SCREEN -.fallback SDL_Renderer.-> WIN
```
---
## 5. Entrada
### 5.1. `Input`
`source/core/input/input.{hpp,cpp}` + `input_types.*` — abstracción de teclado y
mando bajo un enum `InputAction`. Las vinculaciones se aplican desde `Options`
(`Input::applyKeyboardBindingsFromOptions()` /
`applyGamepadBindingsFromOptions()`, `director.cpp`). `mouse.*` gestiona el ratón
(usado sobre todo por el editor).
### 5.2. Hotkeys globales
`source/core/input/global_inputs.{hpp,cpp}` traduce eventos a acciones de sistema
(`global_inputs.cpp`):
- **Ventana/vídeo**: fullscreen, zoom ±, integer scale, vsync, info.
- **Shaders**: toggle; con **Ctrl** → siguiente shader, con **Shift**
siguiente preset.
- **Paletas**: siguiente / anterior (`NEXT_PALETTE`/`PREVIOUS_PALETTE`), y orden
de paleta — una seña de identidad de este juego (paleta intercambiable).
- **Borde** (overscan) toggle, **consola** toggle, **EXIT**.
- En `GAME`, EXIT vuelve a `TITLE`; el `QUIT` global sale del programa.
Cuando la **consola está activa**, `EXIT`/`ACCEPT` se redirigen a ella en vez de
a la escena (`global_inputs.cpp:231`).
---
## 6. Lógica del juego: la escena `Game`
`source/game/scenes/game.{hpp,cpp}` es la escena de gameplay. Hereda de `Scene` y
coordina habitación, jugador, enemigos, ítems, marcador, estadísticas y logros.
### 6.1. FSM de la escena
`Game::State` (`game.hpp:28`): `PLAYING → BLACK_SCREEN → GAME_OVER →
FADE_TO_ENDING → POST_FADE_ENDING`. Cada estado tiene su `updateX`/`renderX`;
`transitionToState()` cambia de estado y resetea los timers. El modo
(`Game::Mode::GAME` o `DEMO`) condiciona el comportamiento.
### 6.2. El frame
`Game::iterate()` calcula el delta con el `DeltaTimer`, llama a `update()`
(input + lógica + colisiones + cambio de sala) y a `render()`. El render del
estado `PLAYING` va directo a las superficies; los fades de fin de juego usan un
`game_backbuffer_surface_`.
### 6.3. Qué gestiona
- **Habitación activa** (`std::shared_ptr<Room> room_`) y cambio de sala al tocar
un borde (`changeRoom`, `checkPlayerIsOnBorder`; §7).
- **Jugador** (`Player`), con muerte (`killPlayer`, `BLACK_SCREEN`), y la "Jail"
que restaura vidas con el tiempo (`checkRestoringJail`, `JAIL_RESTORE_INTERVAL`).
- **Colisiones**: jugador↔enemigos (`checkPlayerAndEnemies`) y jugador↔ítems
(`checkPlayerAndItems`).
- **Progresión**: `RoomTracker` (salas visitadas), `Stats`, `Scoreboard`, fin de
juego (`checkEndGame`) y secuencias de **ending** (fades a `ENDING`/`ENDING2`).
- **Logros**: `checkSomeCheevos`, `checkEndGameCheevos` (§9).
---
## 7. Habitaciones y colisión
`source/game/gameplay/room.{hpp,cpp}` modela cada sala. Geometría: tiles de
**8 px**, mapa de **32×16** tiles (256×128 px). Los tipos de tile
(`Room::Tile`) son `EMPTY, WALL, PASSABLE, SLOPE_L, SLOPE_R, KILL, ANIMATED`
(`room.hpp:32`).
### 7.1. Datos de sala
`Room::Data` (`room.hpp:42`) se carga de YAML (vía `RoomLoader`,
`Room::loadYAML`). Contiene número/nombre, colores (fondo, borde, ítems),
**salas contiguas** (`upper_room`, `lower_room`, `left_room`, `right_room`
navegación tipo *metroidvania*), tileset, el `tile_map` embebido, y las listas
de enemigos e ítems.
### 7.2. Colisión por superficies
La colisión no es AABB simple contra tiles, sino consultas de **superficies**:
`checkRightSurfaces`, `checkLeftSurfaces`, `checkTopSurfaces`,
`checkBottomSurfaces`, `checkAutoSurfaces` (cintas), más rampas
(`checkLeftSlopes`/`checkRightSlopes`, `getSlopeHeight`, `getSlopeAtPoint`) y
cintas transportadoras (`checkConveyorBelts`, `conveyor_belt_direction_`). El
jugador aporta puntos de colisión finos (8 `collider_points_` + `under_left_foot_`
/ `under_right_foot_`; `player.hpp:147`). Los tiles `KILL` matan al jugador.
Subobjetos de `Room`: `CollisionMap` (datos de colisión), `TilemapRenderer`
(dibujo del tilemap), `EnemyManager` e `ItemManager` (ciclo de vida de enemigos
e ítems de la sala). `RoomTracker` (`gameplay/room_tracker.*`) registra las salas
visitadas.
---
## 8. Entidades
`source/game/entities/`:
- **`Player`** (`player.hpp`) — física *time-based* con `JUMP_VELOCITY = -80`,
`GRAVITY_FORCE = 155.6` px/s² (`player.hpp:42`). FSM de estados (IDLE/WALKING/
JUMPING/…), colisión por 8 puntos + "pies", controladores de sonido de salto y
caída (`JumpSoundController`/`FallSoundController`), y un `SpawnData` para
reaparecer (también usado al cambiar de sala conservando velocidad).
- **`Enemy`** (`enemy.hpp`) — enemigos con datos (`Enemy::Data`), colisión AABB,
gestionados por `EnemyManager` (`gameplay/enemy_manager.*`).
- **`Item`** (`item.hpp`) — coleccionables (`Item::Data`), gestionados por
`ItemManager` (`gameplay/item_manager.*`) y rastreados por `ItemTracker`.
No hay una clase base de entidad común con polimorfismo profundo: cada tipo tiene
su `update`/`render`/colisión y su *manager* dedicado dentro de la `Room`.
---
## 9. Logros, estadísticas y marcador
- **`Cheevos`** (`source/game/gameplay/cheevos.{hpp,cpp}`) — singleton del sistema
de logros. `unlock(id)`, `setUnobtainable(id)`, `getTotalUnlockedAchievements()`;
estado **persistido en `cheevos.bin`** (`loadFromFile`/`saveToFile`). La escena
`Game` llama a `checkSomeCheevos`/`checkEndGameCheevos`, y el `Notifier` muestra
el logro en pantalla (§11).
- **`Stats`** (`gameplay/stats.*`) — diccionario de estadísticas de partida
(`initStats`).
- **`Scoreboard`** (`gameplay/scoreboard.*`) — datos y dibujo del marcador
(`Scoreboard::Data` se comparte por `shared_ptr` con la sala y el editor).
- **`ItemTracker`** / **`RoomTracker`** — progreso de ítems recogidos y salas
visitadas.
---
## 10. Editor de mapas (Debug)
`source/game/editor/`**solo se compila en `_DEBUG`** (todo el header de
`MapEditor` está bajo `#ifdef _DEBUG`, `map_editor.hpp:3`). Es un editor de
habitaciones *in-game* completo, integrado con la escena `Game` y con la consola.
### 10.1. `MapEditor` (singleton)
`map_editor.hpp`. Se entra con `enter(room, player, room_path, scoreboard_data)`
sobre la sala viva. Funcionalidades:
- **Pintado de tiles** con *brush* (`brush_tile_`, `ERASER_BRUSH`, `painting_`),
preview bajo el cursor y rejilla opcional (`renderGrid`, `settings_.grid`).
- **Drag & drop** de jugador, enemigos (posición inicial y *bounds* de patrulla)
e ítems (`DragTarget`, `DragState`, `handleMouseDown/Up`, `updateDrag`), con
*snap* a rejilla.
- **Edición de propiedades** de enemigos, ítems y de la sala
(`setEnemyProperty`, `setItemProperty`, `setRoomProperty`, colores, color de
fondo…), invocables tanto por teclas como por **comandos de consola**.
- **Gestión de salas**: crear (`createNewRoom(direction)`), borrar (`deleteRoom`),
con conexión a las salas contiguas.
- **Persistencia**: `autosave()` + `room_saver.*` escribe el YAML de la sala;
`revert()` restaura desde el backup del nodo YAML (`yaml_backup_`).
### 10.2. Subcomponentes del editor
- **`TilePicker`** (`tile_picker.*`) — selector visual de tiles del tileset
(`openTilePicker`).
- **`MiniMap`** (`mini_map.*`) — minimapa de salas con conexiones, colores
configurables (`setMiniMapBg`/`setMiniMapConn`).
- **`EditorStatusBar`** (`editor_statusbar.*`) — barra de estado con info de
edición (`updateStatusBarInfo`).
- **`RoomSaver`** (`room_saver.*`) — serialización de la sala a YAML preservando
campos no editados.
El editor guarda/restaura estado del juego al entrar/salir (invencibilidad,
overlay de info) para no contaminar la partida.
---
## 11. Consola y notificaciones
### 11.1. `Console`
`source/game/ui/console.{hpp,cpp}` — consola de comandos *in-game* (singleton),
con estética de terminal verde sobre `Surface` propia. Características
(`console.hpp`):
- Panel animado (`Status` HIDDEN/RISING/ACTIVE/VANISHING), efecto *typewriter*,
cursor parpadeante.
- **Historial** navegable (flechas), **autocompletado por TAB** (`tab_matches_`),
*word-wrap* por ancho en píxeles.
- **`CommandRegistry`** (`console_commands.{hpp,cpp}`): metadatos (desde YAML) +
*handlers* C++. Los comandos cubren depuración del juego y **pilotan el editor
de mapas** (`setEnemyProperty`, `addItem`, `setRoomProperty`, etc.).
- **Scopes** (`setScope`/`getScope`): filtran qué comandos y autocompletados
están disponibles según el contexto (p.ej. dentro del editor).
- `on_toggle` notifica a la escena cuando se abre/cierra (para pausar input de
juego).
### 11.2. `Notifier`
`source/game/ui/notifier.{hpp,cpp}` — cola de notificaciones en pantalla (logros
desbloqueados, cambios de opción…). Se inicializa en `finishBoot()` y el `Screen`
las pinta como overlay (`renderNotifications`). El overlay de FPS/driver es
`core/rendering/render_info.*` (toggle por hotkey).
---
## 12. Modo demo
> **El modo demo de este juego NO es reproducción de input grabado.** Es un
> **tour automático de habitaciones** (escaparate de niveles).
La escena `Game` construida con `Game::Mode::DEMO` recorre una **lista curada de
salas** y va cambiando cada `DEMO_ROOM_DURATION = 6.0F` segundos
(`game.hpp:48`):
```cpp
// game.cpp — demoInit()
demo_ = DemoData(0.0F, 0, {"04.yaml","54.yaml","20.yaml","09.yaml",
"05.yaml","11.yaml","31.yaml","44.yaml"});
// demoCheckRoomChange(): acumula delta_time y, al llegar a 6s,
// avanza demo_.room_index y changeRoom(...). Al agotar la lista, vuelve.
```
No hay ficheros `.bin` ni `DemoKeys`: la demo simplemente pasea por las
habitaciones para la pantalla de atracción. La salida de la demo devuelve a la
escena de título.
---
## 13. Recursos
- **`Resource::List`** (`core/resources/resource_list.*`) — registro de rutas de
asset cargado de **`config/assets.yaml`**, con consulta `get(filename)` O(1).
- **`Resource::Cache`** (`core/resources/resource_cache.*`) — caché de surfaces,
música, sonidos y datos de animación (`getSurface`, `getMusic`,
`getAnimationData`). Carga **incremental** vía `beginLoad()` + `loadStep(ms)`
(§2.3), con una FSM interna de etapas (`LoadStage`).
- **Pack y fallback**: `resource_pack.*` + `resource_loader.*` + `resource_helper.*`
sirven desde **`resources.pack`** (release) o el filesystem (desarrollo).
- **Formatos**: GIF (gráficos + paletas, `core/rendering/gif.*`); `.yaml` para
animaciones y para las salas (`data/room/`); `.pal` para paletas
(`data/palette/`); OGG/WAV para audio; GLSL para shaders.
---
## 14. Audio, localización y configuración
- **Audio**: `core/audio/audio.*` (singleton, música y SFX) + `audio_adapter.*`
sobre **`jail_audio`** (`jail_audio.hpp`), wrapper SDL3 *first-party* con
`stb_vorbis` para OGG.
- **Localización**: `core/locale/locale.*` carga las cadenas de `data/locale/`.
En release el locale vive dentro del pack (`Locale::initFromContent`);
`Options::language` selecciona el idioma.
- **Configuración**: `source/game/options.{hpp,cpp}` mantiene las opciones
(ventana, vídeo+shaders, audio, idioma, controles, presets PostFX/CrtPi) y las
persiste; `source/game/defaults.hpp` reúne las constantes de gameplay y layout
(canvas 256×192, tamaños de tile, colores de paleta). En Debug, `debug.yaml`
(`core/system/debug.*`) fija la escena inicial.
- **Builds condicionales**: `_DEBUG` (editor, consola, overlay), `RELEASE_BUILD`,
`__EMSCRIPTEN__` (locale/paths especiales, `NO_SHADERS`), y la selección de
shaders por plataforma (SPIR-V vs Metal).
---
## 15. Convenciones y patrones recurrentes
- **Singletons con `init()`/`destroy()`/`get()`**: `Screen`, `Input`, `Audio`,
`Resource::Cache`, `Resource::List`, `Cheevos`, `Console`, `Notifier`,
`RenderInfo`, `MapEditor`, `Debug`. Se crean/destruyen en orden explícito desde
el `Director` (no por destructores estáticos).
- **Render paletizado por CPU** (`Surface` de 8 bits + `Palette`/`SubPalette`),
con recoloreado por reemplazo de índice y paletas intercambiables en caliente.
- **Flujo por variable global** (`SceneManager::current`) + un único
`active_scene_`.
- **Time-based**: todo consume `delta_time` (`DeltaTimer`); las constantes citan
su equivalencia en frames a 66.67 fps.
- **`#ifdef _DEBUG`** envuelve editor, consola de propiedades, overlays y atajos
de depuración — ausentes en release.
- **Comentarios** en español/valenciano; muchos `#include` con comentario
"// Para X" (estilo IWYU).
- **El `CLAUDE.md` puede ir por detrás del código** (caso `Director::run()` vs
callbacks). Ante duda, **manda el código**.
---
## 16. Guía de navegación: "si quieres tocar X, mira Y"
| Quiero… | Empieza por… |
|---|---|
| Entender el arranque | `core/system/director.cpp` (ctor, `iterate`, `finishBoot`) |
| Cambiar el flujo de pantallas | `game/scene_manager.hpp` + `Director::switchToActiveScene` |
| Añadir/editar una pantalla | `game/scenes/` (hereda de `Scene`) + un `case` en `switchToActiveScene` |
| La barra de carga / arranque | `Resource::Cache::beginLoad/loadStep` + `scenes/boot_loader.*` |
| Cómo se dibuja todo | `core/rendering/surface.*` + `Screen::render` (`screen.cpp`) |
| Sprites / animaciones | `core/rendering/sprite/` + `data/**/*.yaml` (animaciones) |
| Paletas / recoloreado | `core/rendering/palette_manager.*` + `Surface` |
| Shaders / CRT | `core/rendering/sdl3gpu/` + `data/shaders/` |
| Controles / hotkeys | `core/input/input.*` + `global_inputs.cpp` |
| **Lógica de partida** | `game/scenes/game.cpp` (`updatePlaying`, FSM `State`) |
| Habitaciones / colisión | `game/gameplay/room.*` (`check*Surfaces`, slopes, cintas) |
| Cargar una sala | `game/gameplay/room_loader.*` + `data/room/*.yaml` |
| El jugador (física) | `game/entities/player.*` (`JUMP_VELOCITY`, `GRAVITY_FORCE`, colisión por puntos) |
| Enemigos / ítems | `game/entities/{enemy,item}.*` + `gameplay/{enemy,item}_manager.*` |
| Logros | `game/gameplay/cheevos.*` (+ `cheevos.bin`) |
| Marcador / estadísticas | `game/gameplay/{scoreboard,stats,item_tracker,room_tracker}.*` |
| **Editor de mapas** | `game/editor/map_editor.*` (+ `tile_picker`, `mini_map`, `room_saver`) |
| **Consola / comandos** | `game/ui/console.*` + `console_commands.*` |
| Notificaciones / FPS | `game/ui/notifier.*` + `core/rendering/render_info.*` |
| Modo demo (tour) | `Game::demoInit/demoCheckRoomChange` (`game.cpp`) |
| Cargar un recurso | `core/resources/resource_cache.*` + `resource_list.*` + `config/assets.yaml` |
| Audio | `core/audio/audio.*` + `jail_audio.hpp` |
| Idiomas | `core/locale/locale.*` + `data/locale/` |
| Opciones / constantes | `game/options.*`, `game/defaults.hpp` |
| Escena inicial en Debug | `core/system/debug.*` + `debug.yaml` |
---
*Documento generado a partir de la lectura directa del código en el commit
actual de la rama `main`. Si algo aquí no cuadra con el código, el código
manda: actualiza este documento.*
+36 -30
View File
@@ -8,7 +8,13 @@
// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp).
// clang-format off
#undef STB_VORBIS_HEADER_ONLY
// stb_vorbis.c (codi de tercers) dispara -Wtautological-compare; el silenciem
// només per a aquesta inclusió sense afectar el nostre codi.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wtautological-compare"
// NOLINTNEXTLINE(bugprone-suspicious-include) — stb_vorbis és single-file: el TU principal inclou el .c per portar la implementació.
#include "external/stb_vorbis.c"
#pragma GCC diagnostic pop
// stb_vorbis.c filtra les macros L, C i R (i PLAYBACK_*) al TU. Les netegem
// perquè xocarien amb noms de paràmetres de plantilla en altres headers.
#undef L
@@ -43,15 +49,15 @@ Audio::Audio() { initSDLAudio(); }
// Destructor
Audio::~Audio() {
JA_Quit();
Ja::quit();
}
// Método principal
void Audio::update() {
JA_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) {
if (instance != nullptr && instance->music_.state == MusicState::PLAYING && Ja::getMusicState() != Ja::MusicState::PLAYING) {
instance->music_.state = MusicState::STOPPED;
}
}
@@ -65,18 +71,18 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
return;
}
if (!music_enabled_) return;
if (!music_enabled_) { return; }
auto* resource = AudioResource::getMusic(name);
if (resource == nullptr) return;
if (resource == nullptr) { return; }
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
JA_CrossfadeMusic(resource, crossfade_ms, loop);
Ja::crossfadeMusic(resource, crossfade_ms, loop);
} else {
if (music_.state == MusicState::PLAYING) {
JA_StopMusic();
Ja::stopMusic();
}
JA_PlayMusic(resource, loop);
Ja::playMusic(resource, loop);
}
music_.name = name;
@@ -85,16 +91,16 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
}
// 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;
void Audio::playMusic(Ja::Music* 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);
Ja::crossfadeMusic(music, crossfade_ms, loop);
} else {
if (music_.state == MusicState::PLAYING) {
JA_StopMusic();
Ja::stopMusic();
}
JA_PlayMusic(music, loop);
Ja::playMusic(music, loop);
}
music_.name.clear(); // nom desconegut quan es passa per punter
@@ -105,7 +111,7 @@ void Audio::playMusic(JA_Music_t* music, const int loop, const int crossfade_ms)
// Pausa la música
void Audio::pauseMusic() {
if (music_enabled_ && music_.state == MusicState::PLAYING) {
JA_PauseMusic();
Ja::pauseMusic();
music_.state = MusicState::PAUSED;
}
}
@@ -113,7 +119,7 @@ void Audio::pauseMusic() {
// Continua la música pausada
void Audio::resumeMusic() {
if (music_enabled_ && music_.state == MusicState::PAUSED) {
JA_ResumeMusic();
Ja::resumeMusic();
music_.state = MusicState::PLAYING;
}
}
@@ -121,7 +127,7 @@ void Audio::resumeMusic() {
// Detiene la música
void Audio::stopMusic() {
if (music_enabled_) {
JA_StopMusic();
Ja::stopMusic();
music_.state = MusicState::STOPPED;
}
}
@@ -129,42 +135,42 @@ void Audio::stopMusic() {
// Reproduce un sonido por nombre
void Audio::playSound(const std::string& name, Group group) const {
if (sound_enabled_) {
JA_PlaySound(AudioResource::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 {
void Audio::playSound(Ja::Sound* sound, Group group) const {
if (sound_enabled_ && sound != nullptr) {
JA_PlaySound(sound, 0, static_cast<int>(group));
Ja::playSound(sound, 0, static_cast<int>(group));
}
}
// Detiene todos los sonidos
void Audio::stopAllSounds() const {
if (sound_enabled_) {
JA_StopChannel(-1);
Ja::stopChannel(-1);
}
}
// Realiza un fundido de salida de la música
void Audio::fadeOutMusic(int milliseconds) const {
if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) {
JA_FadeOutMusic(milliseconds);
Ja::fadeOutMusic(milliseconds);
}
}
// Consulta directamente el estado real de la música en jailaudio
auto Audio::getRealMusicState() -> MusicState {
JA_Music_state ja_state = JA_GetMusicState();
Ja::MusicState ja_state = Ja::getMusicState();
switch (ja_state) {
case JA_MUSIC_PLAYING:
case Ja::MusicState::PLAYING:
return MusicState::PLAYING;
case JA_MUSIC_PAUSED:
case Ja::MusicState::PAUSED:
return MusicState::PAUSED;
case JA_MUSIC_STOPPED:
case JA_MUSIC_INVALID:
case JA_MUSIC_DISABLED:
case Ja::MusicState::STOPPED:
case Ja::MusicState::INVALID:
case Ja::MusicState::DISABLED:
default:
return MusicState::STOPPED;
}
@@ -175,7 +181,7 @@ void Audio::setSoundVolume(float sound_volume, Group group) const {
if (sound_enabled_) {
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
const float CONVERTED_VOLUME = sound_volume * Options::audio.volume;
JA_SetSoundVolume(CONVERTED_VOLUME, static_cast<int>(group));
Ja::setSoundVolume(CONVERTED_VOLUME, static_cast<int>(group));
}
}
@@ -184,7 +190,7 @@ void Audio::setMusicVolume(float music_volume) const {
if (music_enabled_) {
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
const float CONVERTED_VOLUME = music_volume * Options::audio.volume;
JA_SetMusicVolume(CONVERTED_VOLUME);
Ja::setMusicVolume(CONVERTED_VOLUME);
}
}
@@ -206,7 +212,7 @@ void Audio::initSDLAudio() {
if (!SDL_Init(SDL_INIT_AUDIO)) {
std::cout << "SDL_AUDIO could not initialize! SDL Error: " << SDL_GetError() << '\n';
} else {
JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2);
Ja::init(FREQUENCY, SDL_AUDIO_S16LE, 2);
enable(Options::audio.enabled);
}
}
+17 -11
View File
@@ -1,9 +1,15 @@
#pragma once
#include <cmath> // Para std::lround
#include <cstdint> // Para int8_t, uint8_t
#include <string> // Para string
#include <utility> // Para move
namespace Ja {
struct Music;
struct Sound;
} // namespace Ja
// --- 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
@@ -41,17 +47,17 @@ class Audio {
static void update(); // Actualización del sistema de audio
// --- Control de 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
void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional)
void playMusic(Ja::Music* 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
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
void playSound(Ja::Sound* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
void stopAllSounds() const; // Detener todos los sonidos
// --- Control de volumen (API interna: float 0.0..1.0) ---
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
@@ -59,8 +65,8 @@ class Audio {
// --- 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 auto toPercent(float volume) -> int {
return static_cast<int>(std::lround(volume * 100.0F));
}
static constexpr auto fromPercent(int percent) -> float {
return static_cast<float>(percent) / 100.0F;
+2 -2
View File
@@ -3,11 +3,11 @@
#include "core/resources/resource_cache.hpp"
namespace AudioResource {
JA_Music_t* getMusic(const std::string& name) {
auto getMusic(const std::string& name) -> Ja::Music* {
return Resource::Cache::get()->getMusic(name);
}
JA_Sound_t* getSound(const std::string& name) {
auto getSound(const std::string& name) -> Ja::Sound* {
return Resource::Cache::get()->getSound(name);
}
} // namespace AudioResource
+8 -6
View File
@@ -1,17 +1,19 @@
#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
// Aquest fitxer exposa una interfície comuna a Audio per obtenir Ja::Music* /
// Ja::Sound* 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 Ja {
struct Music;
struct Sound;
} // namespace Ja
namespace AudioResource {
JA_Music_t* getMusic(const std::string& name);
JA_Sound_t* getSound(const std::string& name);
auto getMusic(const std::string& name) -> Ja::Music*;
auto getSound(const std::string& name) -> Ja::Sound*;
} // namespace AudioResource
File diff suppressed because it is too large Load Diff
+55 -73
View File
@@ -2,8 +2,10 @@
#include <SDL3/SDL.h>
#include <string> // Para allocator, operator+, char_traits, string
#include <vector> // Para vector
#include <algorithm> // Para ranges::find_if
#include <initializer_list> // Para initializer_list
#include <string> // Para allocator, operator+, char_traits, string
#include <vector> // Para vector
#include "core/input/input.hpp" // Para Input, InputAction, Input::DO_NOT_ALLOW_REPEAT
#include "core/locale/locale.hpp" // Para Locale
@@ -28,7 +30,7 @@ namespace GlobalInputs {
if (stringInVector(Notifier::get()->getCodes(), CODE)) {
SceneManager::current = SceneManager::Scene::TITLE;
} else {
Notifier::get()->show({Locale::get()->get("ui.press_again_menu")}, Notifier::Style::DEFAULT, -1, true, CODE); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get("ui.press_again_menu")}, Notifier::Style::DEFAULT, -1, true, CODE);
}
return;
}
@@ -53,7 +55,7 @@ namespace GlobalInputs {
if (stringInVector(Notifier::get()->getCodes(), CODE)) {
SceneManager::current = SceneManager::Scene::QUIT;
} else {
Notifier::get()->show({Locale::get()->get("ui.press_again_exit")}, Notifier::Style::DEFAULT, -1, true, CODE); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get("ui.press_again_exit")}, Notifier::Style::DEFAULT, -1, true, CODE);
}
#endif // __EMSCRIPTEN__
}
@@ -78,29 +80,29 @@ namespace GlobalInputs {
void handleToggleBorder() {
Screen::get()->toggleBorder();
Notifier::get()->show({Locale::get()->get(Options::video.border.enabled ? "ui.border_enabled" : "ui.border_disabled")}); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get(Options::video.border.enabled ? "ui.border_enabled" : "ui.border_disabled")});
}
void handleToggleVideoMode() {
Screen::get()->toggleVideoMode();
Notifier::get()->show({Locale::get()->get(static_cast<int>(Options::video.fullscreen) == 0 ? "ui.fullscreen_disabled" : "ui.fullscreen_enabled")}); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get(static_cast<int>(Options::video.fullscreen) == 0 ? "ui.fullscreen_disabled" : "ui.fullscreen_enabled")});
}
void handleDecWindowZoom() {
if (Screen::get()->decWindowZoom()) {
Notifier::get()->show({Locale::get()->get("ui.window_zoom") + std::to_string(Options::window.zoom)}); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get("ui.window_zoom") + std::to_string(Options::window.zoom)});
}
}
void handleIncWindowZoom() {
if (Screen::get()->incWindowZoom()) {
Notifier::get()->show({Locale::get()->get("ui.window_zoom") + std::to_string(Options::window.zoom)}); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get("ui.window_zoom") + std::to_string(Options::window.zoom)});
}
}
void handleToggleShaders() {
Screen::get()->toggleShaders();
Notifier::get()->show({Locale::get()->get(Options::video.shader.enabled ? "ui.shaders_enabled" : "ui.shaders_disabled")}); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get(Options::video.shader.enabled ? "ui.shaders_enabled" : "ui.shaders_disabled")});
}
void handleNextShaderPreset() {
@@ -108,111 +110,91 @@ namespace GlobalInputs {
if (!Options::crtpi_presets.empty()) {
Options::video.shader.current_crtpi_preset = (Options::video.shader.current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size());
Screen::get()->reloadCrtPi();
Notifier::get()->show({Locale::get()->get("ui.crtpi") + " " + prettyName(Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)].name)}); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get("ui.crtpi") + " " + prettyName(Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)].name)});
}
} else {
if (!Options::postfx_presets.empty()) {
Options::video.shader.current_postfx_preset = (Options::video.shader.current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
Screen::get()->reloadPostFX();
Notifier::get()->show({Locale::get()->get("ui.postfx") + " " + prettyName(Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)].name)}); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get("ui.postfx") + " " + prettyName(Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)].name)});
}
}
}
void handleNextShader() {
Screen::get()->nextShader();
Notifier::get()->show({Locale::get()->get("ui.shader") + " " + // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get("ui.shader") + " " +
(Options::video.shader.current_shader == Rendering::ShaderType::CRTPI ? "CRTPI" : "POSTFX")});
}
void handleNextPalette() {
Screen::get()->nextPalette();
Notifier::get()->show({Locale::get()->get("ui.palette") + " " + toUpper(Screen::get()->getPalettePrettyName())}); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get("ui.palette") + " " + toUpper(Screen::get()->getPalettePrettyName())});
}
void handlePreviousPalette() {
Screen::get()->previousPalette();
Notifier::get()->show({Locale::get()->get("ui.palette") + " " + toUpper(Screen::get()->getPalettePrettyName())}); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get("ui.palette") + " " + toUpper(Screen::get()->getPalettePrettyName())});
}
void handleNextPaletteSortMode() {
Screen::get()->nextPaletteSortMode();
Notifier::get()->show({Locale::get()->get("ui.palette_sort") + " " + toUpper(Screen::get()->getPaletteSortModeName())}); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get("ui.palette_sort") + " " + toUpper(Screen::get()->getPaletteSortModeName())});
}
void handleToggleIntegerScale() {
Screen::get()->toggleIntegerScale();
Screen::get()->setVideoMode(Options::video.fullscreen);
Notifier::get()->show({Locale::get()->get(Options::video.integer_scale ? "ui.integer_scale_enabled" : "ui.integer_scale_disabled")}); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get(Options::video.integer_scale ? "ui.integer_scale_enabled" : "ui.integer_scale_disabled")});
}
void handleToggleVSync() {
Screen::get()->toggleVSync();
Notifier::get()->show({Locale::get()->get(Options::video.vertical_sync ? "ui.vsync_enabled" : "ui.vsync_disabled")}); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get(Options::video.vertical_sync ? "ui.vsync_enabled" : "ui.vsync_disabled")});
}
// F4 amb modificadors: Ctrl=next shader (POSTFX↔CRTPI), Shift=next preset, sense modificador=toggle shader
auto getShaderAction() -> InputAction {
if (!Screen::get()->isHardwareAccelerated()) { return InputAction::NONE; }
if (!Input::get()->checkAction(InputAction::TOGGLE_SHADER, Input::DO_NOT_ALLOW_REPEAT)) { return InputAction::NONE; }
const SDL_Keymod MOD = SDL_GetModState();
if (Options::video.shader.enabled && ((MOD & SDL_KMOD_CTRL) != 0U)) { return InputAction::NEXT_SHADER; }
if (Options::video.shader.enabled && ((MOD & SDL_KMOD_SHIFT) != 0U)) { return InputAction::NEXT_SHADER_PRESET; }
return InputAction::TOGGLE_SHADER;
}
// F5 amb modificador Ctrl per a paleta anterior
auto getPaletteAction() -> InputAction {
if (!Input::get()->checkAction(InputAction::NEXT_PALETTE, Input::DO_NOT_ALLOW_REPEAT)) { return InputAction::NONE; }
return ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) ? InputAction::PREVIOUS_PALETTE : InputAction::NEXT_PALETTE;
}
// Comprova una llista d'accions 1:1 (sense modificadors); retorna la primera que dispare
auto firstPressedFrom(std::initializer_list<InputAction> actions) -> InputAction {
const auto* const IT = std::ranges::find_if(actions, [](const InputAction act) {
return Input::get()->checkAction(act, Input::DO_NOT_ALLOW_REPEAT);
});
return (IT != actions.end()) ? *IT : InputAction::NONE;
}
// Detecta qué acción global ha sido presionada (si alguna)
auto getPressedAction() -> InputAction { // NOLINT(readability-function-cognitive-complexity)
auto getPressedAction() -> InputAction {
// 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;
}
if (Input::get()->checkAction(InputAction::ACCEPT, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::ACCEPT;
}
if (Input::get()->checkAction(InputAction::TOGGLE_BORDER, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::TOGGLE_BORDER;
}
if (GlobalEvents::consumeGamepadButtonPressed()) { return InputAction::ACCEPT; }
if (const InputAction ACT = firstPressedFrom({InputAction::EXIT, InputAction::ACCEPT, InputAction::TOGGLE_BORDER}); ACT != InputAction::NONE) { return ACT; }
if (!Options::kiosk.enabled) {
if (Input::get()->checkAction(InputAction::TOGGLE_FULLSCREEN, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::TOGGLE_FULLSCREEN;
}
if (Input::get()->checkAction(InputAction::WINDOW_DEC_ZOOM, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::WINDOW_DEC_ZOOM;
}
if (Input::get()->checkAction(InputAction::WINDOW_INC_ZOOM, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::WINDOW_INC_ZOOM;
}
if (const InputAction ACT = firstPressedFrom({InputAction::TOGGLE_FULLSCREEN, InputAction::WINDOW_DEC_ZOOM, InputAction::WINDOW_INC_ZOOM}); ACT != InputAction::NONE) { return ACT; }
}
if (Screen::get()->isHardwareAccelerated()) {
if (Input::get()->checkAction(InputAction::TOGGLE_SHADER, Input::DO_NOT_ALLOW_REPEAT)) {
if ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) {
return InputAction::TOGGLE_SUPERSAMPLING; // Ctrl+F4
}
if (Options::video.shader.enabled && ((SDL_GetModState() & SDL_KMOD_SHIFT) != 0U)) {
return InputAction::NEXT_SHADER_PRESET; // Shift+F4
}
return InputAction::TOGGLE_SHADER; // F4
}
}
if (Input::get()->checkAction(InputAction::NEXT_PALETTE, Input::DO_NOT_ALLOW_REPEAT)) {
if ((SDL_GetModState() & SDL_KMOD_CTRL) != 0U) {
return InputAction::PREVIOUS_PALETTE; // Ctrl+F5
}
return InputAction::NEXT_PALETTE; // F5
}
if (Input::get()->checkAction(InputAction::NEXT_PALETTE_SORT, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::NEXT_PALETTE_SORT; // F6
}
if (Input::get()->checkAction(InputAction::TOGGLE_INTEGER_SCALE, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::TOGGLE_INTEGER_SCALE;
}
if (Input::get()->checkAction(InputAction::TOGGLE_VSYNC, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::TOGGLE_VSYNC;
}
if (Input::get()->checkAction(InputAction::TOGGLE_INFO, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::TOGGLE_INFO;
}
if (Input::get()->checkAction(InputAction::TOGGLE_CONSOLE, Input::DO_NOT_ALLOW_REPEAT)) {
return InputAction::TOGGLE_CONSOLE;
}
return InputAction::NONE;
if (const InputAction ACT = getShaderAction(); ACT != InputAction::NONE) { return ACT; }
if (const InputAction ACT = getPaletteAction(); ACT != InputAction::NONE) { return ACT; }
return firstPressedFrom({InputAction::NEXT_PALETTE_SORT, InputAction::TOGGLE_INTEGER_SCALE, InputAction::TOGGLE_VSYNC, InputAction::TOGGLE_INFO, InputAction::TOGGLE_CONSOLE});
}
} // namespace
@@ -284,7 +266,7 @@ namespace GlobalInputs {
handleNextShaderPreset();
break;
case InputAction::TOGGLE_SUPERSAMPLING:
case InputAction::NEXT_SHADER:
handleNextShader();
break;
+53 -82
View File
@@ -2,6 +2,7 @@
#include <SDL3/SDL.h> // Para SDL_GetGamepadAxis, SDL_GamepadAxis, SDL_GamepadButton, SDL_GetError, SDL_JoystickID, SDL_AddGamepadMappingsFromFile, SDL_Event, SDL_EventType, SDL_GetGamepadButton, SDL_GetKeyboardState, SDL_INIT_GAMEPAD, SDL_InitSubSystem, SDL_LogError, SDL_OpenGamepad, SDL_PollEvent, SDL_WasInit, Sint16, SDL_Gamepad, SDL_LogCategory, SDL_Scancode
#include <algorithm> // Para ranges::any_of
#include <iostream> // Para basic_ostream, operator<<, cout, cerr
#include <memory> // Para shared_ptr, __shared_ptr_access, allocator, operator==, make_shared
#include <ranges> // Para __find_if_fn, find_if
@@ -49,7 +50,7 @@ static void installWebStandardMapping(SDL_JoystickID jid) {
Input* Input::instance = nullptr;
// Inicializa la instancia única del singleton
void Input::init(const std::string& game_controller_db_path) { // NOLINT(readability-convert-member-functions-to-static)
void Input::init(const std::string& game_controller_db_path) {
Input::instance = new Input(game_controller_db_path);
}
@@ -105,7 +106,7 @@ void Input::applyKeyboardBindingsFromOptions() {
}
// Aplica configuración de botones del gamepad desde Options al primer gamepad conectado
void Input::applyGamepadBindingsFromOptions() { // NOLINT(readability-convert-member-functions-to-static)
void Input::applyGamepadBindingsFromOptions() {
// Si no hay gamepads conectados, no hay nada que hacer
if (gamepads_.empty()) {
return;
@@ -126,21 +127,21 @@ void Input::applyGamepadBindingsFromOptions() { // NOLINT(readability-convert-m
}
// Asigna inputs a botones del mando
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button) { // NOLINT(readability-convert-member-functions-to-static)
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action, SDL_GamepadButton button) {
if (gamepad != nullptr) {
gamepad->bindings[action].button = button;
}
}
// Asigna inputs a botones del mando
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action_target, Action action_source) { // NOLINT(readability-convert-member-functions-to-static)
void Input::bindGameControllerButton(const std::shared_ptr<Gamepad>& gamepad, Action action_target, Action action_source) {
if (gamepad != nullptr) {
gamepad->bindings[action_target].button = gamepad->bindings[action_source].button;
}
}
// Comprueba si alguna acción está activa
auto Input::checkAction(Action action, bool repeat, bool check_keyboard, const std::shared_ptr<Gamepad>& gamepad) -> bool { // NOLINT(readability-convert-member-functions-to-static)
auto Input::checkAction(Action action, bool repeat, bool check_keyboard, const std::shared_ptr<Gamepad>& gamepad) -> bool {
bool success_keyboard = false;
bool success_controller = false;
@@ -178,18 +179,13 @@ auto Input::checkAction(Action action, bool repeat, bool check_keyboard, const s
}
// Comprueba si hay almenos una acción activa
auto Input::checkAnyInput(bool check_keyboard, const std::shared_ptr<Gamepad>& gamepad) -> bool { // NOLINT(readability-convert-member-functions-to-static)
auto Input::checkAnyInput(bool check_keyboard, const std::shared_ptr<Gamepad>& gamepad) -> bool {
// Obtenemos el número total de acciones posibles para iterar sobre ellas.
// --- Comprobación del Teclado ---
if (check_keyboard) {
for (const auto& pair : keyboard_.bindings) {
// Simplemente leemos el estado pre-calculado por Input::update().
// Ya no se llama a SDL_GetKeyboardState ni se modifica el estado '.active'.
if (pair.second.just_pressed) {
return true; // Se encontró una acción recién pulsada.
}
}
// Llegim l'estat pre-calculat per Input::update() (sense tornar a cridar SDL_GetKeyboardState).
if (check_keyboard && std::ranges::any_of(keyboard_.bindings, [](const auto& pair) { return pair.second.just_pressed; })) {
return true;
}
// Si gamepad es nullptr pero hay mandos conectados, usar el primero
@@ -199,15 +195,9 @@ auto Input::checkAnyInput(bool check_keyboard, const std::shared_ptr<Gamepad>& g
}
// --- Comprobación del Mando ---
// Comprobamos si hay mandos y si el índice solicitado es válido.
if (active_gamepad != nullptr) {
// Iteramos sobre todas las acciones, no sobre el número de mandos.
for (const auto& pair : active_gamepad->bindings) {
// Leemos el estado pre-calculado para el mando y la acción específicos.
if (pair.second.just_pressed) {
return true; // Se encontró una acción recién pulsada en el mando.
}
}
// Iterem sobre totes les accions del mandos pre-calculades per Input::update().
if (active_gamepad != nullptr && std::ranges::any_of(active_gamepad->bindings, [](const auto& pair) { return pair.second.just_pressed; })) {
return true;
}
// Si llegamos hasta aquí, no se detectó ninguna nueva pulsación.
@@ -215,23 +205,15 @@ auto Input::checkAnyInput(bool check_keyboard, const std::shared_ptr<Gamepad>& g
}
// Comprueba si hay algún botón pulsado
auto Input::checkAnyButton(bool repeat) -> bool { // NOLINT(readability-convert-member-functions-to-static)
// Solo comprueba los botones definidos previamente
for (auto bi : BUTTON_INPUTS) {
// Comprueba el teclado
auto Input::checkAnyButton(bool repeat) -> bool {
return std::ranges::any_of(BUTTON_INPUTS, [&](auto bi) {
if (checkAction(bi, repeat, CHECK_KEYBOARD)) {
return true;
}
// Comprueba los mandos
for (const auto& gamepad : gamepads_) {
if (checkAction(bi, repeat, DO_NOT_CHECK_KEYBOARD, gamepad)) {
return true;
}
}
}
return false;
return std::ranges::any_of(gamepads_, [&](const auto& gamepad) {
return checkAction(bi, repeat, DO_NOT_CHECK_KEYBOARD, gamepad);
});
});
}
// Comprueba si hay algun mando conectado
@@ -245,9 +227,8 @@ auto Input::getControllerName(const std::shared_ptr<Gamepad>& gamepad) -> std::s
// Obtiene la lista de nombres de mandos
auto Input::getControllerNames() const -> std::vector<std::string> {
std::vector<std::string> names;
for (const auto& gamepad : gamepads_) {
names.push_back(gamepad->name);
}
names.reserve(gamepads_.size());
std::ranges::transform(gamepads_, std::back_inserter(names), [](const auto& gamepad) { return gamepad->name; });
return names;
}
@@ -255,31 +236,25 @@ auto Input::getControllerNames() const -> std::vector<std::string> {
auto Input::getNumGamepads() const -> int { return gamepads_.size(); }
// Obtiene el gamepad a partir de un event.id
auto Input::getGamepad(SDL_JoystickID id) const -> std::shared_ptr<Input::Gamepad> { // NOLINT(readability-convert-member-functions-to-static)
for (const auto& gamepad : gamepads_) {
if (gamepad->instance_id == id) {
return gamepad;
}
}
return nullptr;
auto Input::getGamepad(SDL_JoystickID id) const -> std::shared_ptr<Input::Gamepad> {
auto it = std::ranges::find_if(gamepads_,
[id](const auto& gamepad) { return gamepad->instance_id == id; });
return (it != gamepads_.end()) ? *it : nullptr;
}
auto Input::getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad> { // NOLINT(readability-convert-member-functions-to-static)
for (const auto& gamepad : gamepads_) {
if (gamepad && gamepad->name == name) {
return gamepad;
}
}
return nullptr;
auto Input::getGamepadByName(const std::string& name) const -> std::shared_ptr<Input::Gamepad> {
auto it = std::ranges::find_if(gamepads_,
[&name](const auto& gamepad) { return gamepad && gamepad->name == name; });
return (it != gamepads_.end()) ? *it : nullptr;
}
// Obtiene el SDL_GamepadButton asignado a un action
auto Input::getControllerBinding(const std::shared_ptr<Gamepad>& gamepad, Action action) -> SDL_GamepadButton { // NOLINT(readability-convert-member-functions-to-static)
auto Input::getControllerBinding(const std::shared_ptr<Gamepad>& gamepad, Action action) -> SDL_GamepadButton {
return static_cast<SDL_GamepadButton>(gamepad->bindings[action].button);
}
// Comprueba el eje del mando
auto Input::checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool { // NOLINT(readability-convert-member-functions-to-static)
auto Input::checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool {
// Obtener el binding configurado para esta acción
auto& binding = gamepad->bindings[action];
@@ -322,7 +297,7 @@ auto Input::checkAxisInput(Action action, const std::shared_ptr<Gamepad>& gamepa
}
// Comprueba los triggers del mando como botones digitales
auto Input::checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool { // NOLINT(readability-convert-member-functions-to-static)
auto Input::checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gamepad, bool repeat) -> bool {
// Solo manejamos botones específicos que pueden ser triggers
if (gamepad->bindings[action].button != static_cast<int>(SDL_GAMEPAD_BUTTON_INVALID)) {
// Solo procesamos L2 y R2 como triggers
@@ -369,13 +344,13 @@ auto Input::checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gam
return false;
}
void Input::addGamepadMappingsFromFile() { // NOLINT(readability-convert-member-functions-to-static)
void Input::addGamepadMappingsFromFile() {
if (SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str()) < 0) {
std::cout << "Error, could not load " << gamepad_mappings_file_.c_str() << " file: " << SDL_GetError() << '\n';
}
}
void Input::discoverGamepads() { // NOLINT(readability-convert-member-functions-to-static)
void Input::discoverGamepads() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
handleEvent(event); // Comprueba mandos conectados
@@ -411,7 +386,7 @@ void Input::resetInputStates() {
}
}
void Input::update() { // NOLINT(readability-convert-member-functions-to-static)
void Input::update() {
// --- TECLADO ---
const bool* key_states = SDL_GetKeyboardState(nullptr);
@@ -431,10 +406,10 @@ void Input::update() { // NOLINT(readability-convert-member-functions-to-static
// 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);
SDL_GetGamepadButton(gamepad->pad, SDL_GAMEPAD_BUTTON_SOUTH) ||
SDL_GetGamepadButton(gamepad->pad, SDL_GAMEPAD_BUTTON_EAST) ||
SDL_GetGamepadButton(gamepad->pad, SDL_GAMEPAD_BUTTON_NORTH) ||
SDL_GetGamepadButton(gamepad->pad, SDL_GAMEPAD_BUTTON_WEST);
}
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
@@ -444,17 +419,18 @@ void Input::update() { // NOLINT(readability-convert-member-functions-to-static
}
}
auto Input::handleEvent(const SDL_Event& event) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
auto Input::handleEvent(const SDL_Event& event) -> std::string {
switch (event.type) {
case SDL_EVENT_GAMEPAD_ADDED:
return addGamepad(event.gdevice.which);
case SDL_EVENT_GAMEPAD_REMOVED:
return removeGamepad(event.gdevice.which);
default:
return {};
}
return {};
}
auto Input::addGamepad(int device_index) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
auto Input::addGamepad(int device_index) -> std::string {
installWebStandardMapping(device_index);
SDL_Gamepad* pad = SDL_OpenGamepad(device_index);
if (pad == nullptr) {
@@ -475,7 +451,7 @@ auto Input::addGamepad(int device_index) -> std::string { // NOLINT(readability
return name;
}
auto Input::removeGamepad(SDL_JoystickID id) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
auto it = std::ranges::find_if(gamepads_, [id](const std::shared_ptr<Gamepad>& gamepad) -> bool {
return gamepad->instance_id == id;
});
@@ -490,7 +466,7 @@ auto Input::removeGamepad(SDL_JoystickID id) -> std::string { // NOLINT(readabi
return {};
}
void Input::printConnectedGamepads() const { // NOLINT(readability-convert-member-functions-to-static)
void Input::printConnectedGamepads() const {
if (gamepads_.empty()) {
std::cout << "No hay gamepads conectados." << '\n';
return;
@@ -504,26 +480,21 @@ void Input::printConnectedGamepads() const { // NOLINT(readability-convert-memb
}
}
auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Input::Gamepad> { // NOLINT(readability-convert-member-functions-to-static)
auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::shared_ptr<Input::Gamepad> {
// Si no hay gamepads disponibles, devolver gamepad por defecto
if (gamepads_.empty()) {
return nullptr;
}
// Buscar por nombre
for (const auto& gamepad : gamepads_) {
if (gamepad && gamepad->name == gamepad_name) {
return gamepad;
}
auto by_name = std::ranges::find_if(gamepads_,
[&gamepad_name](const auto& gamepad) { return gamepad && gamepad->name == gamepad_name; });
if (by_name != gamepads_.end()) {
return *by_name;
}
// Si no se encuentra por nombre, devolver el primer gamepad válido
for (const auto& gamepad : gamepads_) {
if (gamepad) {
return gamepad;
}
}
// Si llegamos aquí, no hay gamepads válidos
return nullptr;
auto first_valid = std::ranges::find_if(gamepads_,
[](const auto& gamepad) { return static_cast<bool>(gamepad); });
return (first_valid != gamepads_.end()) ? *first_valid : nullptr;
}
+2 -2
View File
@@ -55,8 +55,8 @@ class Input {
// 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); }
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;
}
+3 -2
View File
@@ -2,11 +2,12 @@
#include <SDL3/SDL.h>
#include <cstdint>
#include <string>
#include <unordered_map>
// --- Enums ---
enum class InputAction : int { // Acciones de entrada posibles en el juego
enum class InputAction : std::uint8_t { // Acciones de entrada posibles en el juego
// Inputs de movimiento
LEFT,
RIGHT,
@@ -25,8 +26,8 @@ enum class InputAction : int { // Acciones de entrada posibles en el juego
TOGGLE_VSYNC,
TOGGLE_INTEGER_SCALE,
TOGGLE_SHADER,
NEXT_SHADER,
NEXT_SHADER_PRESET,
TOGGLE_SUPERSAMPLING,
TOGGLE_BORDER,
TOGGLE_IN_GAME_MUSIC,
NEXT_PALETTE,
+6 -6
View File
@@ -11,13 +11,13 @@
Locale* Locale::instance = nullptr;
// [SINGLETON] Crea el objeto con esta función estática
void Locale::init(const std::string& file_path) { // NOLINT(readability-convert-member-functions-to-static)
void Locale::init(const std::string& file_path) {
Locale::instance = new Locale();
Locale::instance->loadFromFile(file_path);
}
// [SINGLETON] Crea el objeto desde contenido en memoria (para release con pack)
void Locale::initFromContent(const std::string& content) { // NOLINT(readability-convert-member-functions-to-static)
void Locale::initFromContent(const std::string& content) {
Locale::instance = new Locale();
Locale::instance->loadFromContent(content);
}
@@ -34,7 +34,7 @@ auto Locale::get() -> Locale* {
}
// Devuelve la traducción de la clave o la clave como fallback
auto Locale::get(const std::string& key) const -> std::string { // NOLINT(readability-convert-member-functions-to-static)
auto Locale::get(const std::string& key) const -> std::string {
auto it = strings_.find(key);
if (it != strings_.end()) {
return it->second;
@@ -45,7 +45,7 @@ auto Locale::get(const std::string& key) const -> std::string { // NOLINT(reada
}
// Aplana un nodo YAML de forma recursiva: {a: {b: "val"}} -> {"a.b" -> "val"}
void Locale::flatten(const void* node_ptr, const std::string& prefix) { // NOLINT(readability-convert-member-functions-to-static)
void Locale::flatten(const void* node_ptr, const std::string& prefix) {
const auto& node = *static_cast<const fkyaml::node*>(node_ptr);
for (auto itr = node.begin(); itr != node.end(); ++itr) {
@@ -63,7 +63,7 @@ void Locale::flatten(const void* node_ptr, const std::string& prefix) { // NOLI
}
// Carga las traducciones desde contenido YAML en memoria
void Locale::loadFromContent(const std::string& content) { // NOLINT(readability-convert-member-functions-to-static)
void Locale::loadFromContent(const std::string& content) {
if (content.empty()) {
std::cerr << "Locale: contenido vacío, sin traducciones cargadas\n";
return;
@@ -81,7 +81,7 @@ void Locale::loadFromContent(const std::string& content) { // NOLINT(readabilit
}
// Carga las traducciones desde el fichero YAML indicado
void Locale::loadFromFile(const std::string& file_path) { // NOLINT(readability-convert-member-functions-to-static)
void Locale::loadFromFile(const std::string& file_path) {
if (file_path.empty()) {
std::cerr << "Locale: ruta de fichero vacía, sin traducciones cargadas\n";
return;
+9 -9
View File
@@ -15,7 +15,7 @@ namespace GIF {
}
// Inicializa el diccionario LZW con los valores iniciales
inline void initializeDictionary(std::vector<DictionaryEntry>& dictionary, int code_length, int& dictionary_ind) { // NOLINT(readability-identifier-naming)
inline void initializeDictionary(std::vector<DictionaryEntry>& dictionary, int code_length, int& dictionary_ind) {
int size = 1 << code_length;
dictionary.resize(1 << (code_length + 1));
for (dictionary_ind = 0; dictionary_ind < size; dictionary_ind++) {
@@ -55,7 +55,7 @@ namespace GIF {
}
// Agrega una nueva entrada al diccionario
inline void addDictionaryEntry(std::vector<DictionaryEntry>& dictionary, int& dictionary_ind, int& code_length, int prev, int code) { // NOLINT(readability-identifier-naming)
inline void addDictionaryEntry(std::vector<DictionaryEntry>& dictionary, int& dictionary_ind, int& code_length, int prev, int code) {
uint8_t first_byte;
if (code == dictionary_ind) {
first_byte = findFirstByte(dictionary, prev);
@@ -90,7 +90,7 @@ namespace GIF {
return match_len;
}
void Gif::decompress(int code_length, const uint8_t* input, int input_length, uint8_t* out) { // NOLINT(readability-convert-member-functions-to-static)
void Gif::decompress(int code_length, const uint8_t* input, int input_length, uint8_t* out) {
// Verifica que el code_length tenga un rango razonable.
if (code_length < 2 || code_length > 12) {
throw std::runtime_error("Invalid LZW code length");
@@ -146,7 +146,7 @@ namespace GIF {
}
}
auto Gif::readSubBlocks(const uint8_t*& buffer) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
auto Gif::readSubBlocks(const uint8_t*& buffer) -> std::vector<uint8_t> {
std::vector<uint8_t> data;
uint8_t block_size = *buffer;
buffer++;
@@ -159,7 +159,7 @@ namespace GIF {
return data;
}
auto Gif::processImageDescriptor(const uint8_t*& buffer, const std::vector<RGB>& gct, int resolution_bits) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
auto Gif::processImageDescriptor(const uint8_t*& buffer, const std::vector<RGB>& /*gct*/, int /*resolution_bits*/) -> std::vector<uint8_t> {
ImageDescriptor image_descriptor;
// Lee 9 bytes para el image descriptor.
readBytes(buffer, &image_descriptor, sizeof(ImageDescriptor));
@@ -175,7 +175,7 @@ namespace GIF {
return uncompressed_data;
}
auto Gif::loadPalette(const uint8_t* buffer) -> std::vector<uint32_t> { // NOLINT(readability-convert-member-functions-to-static)
auto Gif::loadPalette(const uint8_t* buffer) -> std::vector<uint32_t> {
uint8_t header[6];
std::memcpy(header, buffer, 6);
buffer += 6;
@@ -199,7 +199,7 @@ namespace GIF {
return global_color_table;
}
auto Gif::processGifStream(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
auto Gif::processGifStream(const uint8_t* buffer, uint16_t& w, uint16_t& h) -> std::vector<uint8_t> {
// Leer la cabecera de 6 bytes ("GIF87a" o "GIF89a")
uint8_t header[6];
std::memcpy(header, buffer, 6);
@@ -224,8 +224,8 @@ namespace GIF {
if ((screen_descriptor.fields & 0x80) != 0) {
int global_color_table_size = 1 << ((screen_descriptor.fields & 0x07) + 1);
global_color_table.resize(global_color_table_size);
std::memcpy(global_color_table.data(), buffer, 3 * global_color_table_size);
buffer += 3 * global_color_table_size;
std::memcpy(global_color_table.data(), buffer, static_cast<size_t>(3) * global_color_table_size);
buffer += static_cast<ptrdiff_t>(3) * global_color_table_size;
}
// Supongamos que 'buffer' es el puntero actual y TRAILER es 0x3B
+1 -3
View File
@@ -120,9 +120,7 @@ namespace {
}
// Si quedan colores sin asignar, añadirlos al final
for (const auto& c : available) {
result.push_back(c);
}
std::ranges::copy(available, std::back_inserter(result));
Palette out{};
out.fill(0);
+2 -1
View File
@@ -3,6 +3,7 @@
#include <SDL3/SDL.h>
#include <array>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
@@ -14,7 +15,7 @@ using Palette = std::array<Uint32, 256>;
class Surface;
// Modo de ordenación de paletas
enum class PaletteSortMode : int {
enum class PaletteSortMode : std::uint8_t {
ORIGINAL = 0, // Paleta tal cual viene del fichero
LUMINANCE = 1, // Ordenada por luminancia percibida
SPECTRUM = 2, // Reordenada para imitar la paleta ZX Spectrum
+1 -1
View File
@@ -66,7 +66,7 @@ PixelReveal::PixelReveal(int width, int height, float pixels_per_second, float s
}
// Actualiza el estado del revelado
void PixelReveal::update(float time_active) { // NOLINT(readability-make-member-function-const)
void PixelReveal::update(float time_active) {
// En modo normal revela (pone transparente); en modo inverso cubre (pone negro)
const auto PIXEL_COLOR = reverse_ ? static_cast<Uint8>(PaletteColor::BLACK) : static_cast<Uint8>(PaletteColor::TRANSPARENT);
+4 -3
View File
@@ -1,7 +1,8 @@
#pragma once
#include <memory> // Para shared_ptr
#include <vector> // Para vector
#include <cstdint> // Para uint8_t
#include <memory> // Para shared_ptr
#include <vector> // Para vector
class Surface;
@@ -10,7 +11,7 @@ class Surface;
class PixelReveal {
public:
// Modo de revelado: aleatorio por fila o en orden de bisección (dithering ordenado 1D)
enum class RevealMode { RANDOM,
enum class RevealMode : std::uint8_t { RANDOM,
ORDERED };
// Constructor
+2 -3
View File
@@ -89,7 +89,7 @@ void RenderInfo::render() const {
}
line += " | " + zoom_str + "x";
// PostFX: muestra shader + preset y supersampling, o nada si está desactivado
// PostFX: muestra shader + preset, o nada si está desactivado
if (Options::video.shader.enabled) {
const bool IS_CRTPI = (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI);
const std::string SHADER_NAME = IS_CRTPI ? "crtpi" : "postfx";
@@ -103,8 +103,7 @@ void RenderInfo::render() const {
preset_name = prettyName(Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)].name);
}
}
const bool SHOW_SS = Options::video.supersampling.enabled && !IS_CRTPI;
line += " | " + SHADER_NAME + " " + preset_name + (SHOW_SS ? " (ss)" : "");
line += " | " + SHADER_NAME + " " + preset_name;
}
// Todo en lowercase
+3 -1
View File
@@ -1,5 +1,7 @@
#pragma once
#include <cstdint> // Para uint8_t
class RenderInfo {
public:
// Singleton
@@ -20,7 +22,7 @@ class RenderInfo {
static constexpr float SLIDE_SPEED = 120.0F;
private:
enum class Status { HIDDEN,
enum class Status : std::uint8_t { HIDDEN,
RISING,
ACTIVE,
VANISHING };
+25 -45
View File
@@ -447,7 +447,7 @@ void Screen::nextPalette() { palette_manager_->next(); }
void Screen::previousPalette() { palette_manager_->previous(); }
// Copia la surface a la textura
void Screen::surfaceToTexture() { // NOLINT(readability-convert-member-functions-to-static)
void Screen::surfaceToTexture() {
if (Options::video.border.enabled) {
border_surface_->copyToTexture(renderer_, border_texture_);
game_surface_->copyToTexture(renderer_, border_texture_, nullptr, &game_surface_dstrect_);
@@ -475,13 +475,13 @@ void Screen::textureToRenderer() {
// Franjas superior e inferior (ancho completo)
std::fill_n(border_pixel_buffer_.data(), OFF_Y * BORDER_W, border_argb_color_);
std::fill_n(&border_pixel_buffer_[(OFF_Y + GAME_H) * BORDER_W],
std::fill_n(&border_pixel_buffer_[static_cast<size_t>(OFF_Y + GAME_H) * BORDER_W],
(BORDER_H - OFF_Y - GAME_H) * BORDER_W,
border_argb_color_);
// Columnas laterales en las filas del área de juego
for (int y = OFF_Y; y < OFF_Y + GAME_H; ++y) {
std::fill_n(&border_pixel_buffer_[y * BORDER_W], OFF_X, border_argb_color_);
std::fill_n(&border_pixel_buffer_[(y * BORDER_W) + OFF_X + GAME_W],
std::fill_n(&border_pixel_buffer_[static_cast<size_t>(y) * BORDER_W], OFF_X, border_argb_color_);
std::fill_n(&border_pixel_buffer_[(static_cast<size_t>(y) * BORDER_W) + OFF_X + GAME_W],
BORDER_W - OFF_X - GAME_W,
border_argb_color_);
}
@@ -494,7 +494,7 @@ void Screen::textureToRenderer() {
// Overlay del juego sobre el centro del buffer (ambos paths)
game_surface_->toARGBBuffer(game_pixel_buffer_.data());
for (int y = 0; y < GAME_H; ++y) {
const Uint32* src = &game_pixel_buffer_[y * GAME_W];
const Uint32* src = &game_pixel_buffer_[static_cast<size_t>(y) * GAME_W];
Uint32* dst = &border_pixel_buffer_[((OFF_Y + y) * BORDER_W) + OFF_X];
std::memcpy(dst, src, GAME_W * sizeof(Uint32));
}
@@ -588,48 +588,30 @@ auto loadData(const std::string& filepath) -> std::vector<uint8_t> {
return Resource::Helper::loadFile(filepath);
}
void Screen::setLinearUpscale(bool linear) {
Options::video.supersampling.linear_upscale = linear;
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
shader_backend_->setLinearUpscale(linear);
}
}
void Screen::setDownscaleAlgo(int algo) {
Options::video.supersampling.downscale_algo = algo;
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
shader_backend_->setDownscaleAlgo(algo);
}
}
auto Screen::getSsTextureSize() const -> std::pair<int, int> {
if (!shader_backend_) { return {0, 0}; }
return shader_backend_->getSsTextureSize();
}
// Activa/desactiva el supersampling global (Ctrl+F4)
void Screen::toggleSupersampling() {
Options::video.supersampling.enabled = !Options::video.supersampling.enabled;
if (Options::video.shader.enabled && shader_backend_ && shader_backend_->isHardwareAccelerated()) {
applyCurrentPostFXPreset();
}
}
// Aplica los parámetros del preset actual al backend de shaders
void Screen::applyCurrentPostFXPreset() { // NOLINT(readability-convert-member-functions-to-static)
void Screen::applyCurrentPostFXPreset() {
if (shader_backend_ && !Options::postfx_presets.empty()) {
const auto& p = Options::postfx_presets[static_cast<size_t>(Options::video.shader.current_postfx_preset)];
// Supersampling es un toggle global (Options::video.supersampling.enabled), no por preset.
// setOversample primero: puede recrear texturas antes de que setPostFXParams
// decida si hornear scanlines en CPU o aplicarlas en GPU.
shader_backend_->setOversample(Options::video.supersampling.enabled ? 3 : 1);
Rendering::PostFXParams params{.vignette = p.vignette, .scanlines = p.scanlines, .chroma = p.chroma, .mask = p.mask, .gamma = p.gamma, .curvature = p.curvature, .bleeding = p.bleeding, .flicker = p.flicker};
Rendering::PostFXParams params{
.vignette = p.vignette,
.scanlines = p.scanlines,
.chroma_min = p.chroma_min,
.chroma_max = p.chroma_max,
.mask = p.mask,
.gamma = p.gamma,
.curvature = p.curvature,
.bleeding = p.bleeding,
.flicker = p.flicker,
.scan_dark_ratio = p.scan_dark_ratio,
.scan_dark_floor = p.scan_dark_floor,
.scan_edge_soft = p.scan_edge_soft,
};
shader_backend_->setPostFXParams(params);
}
}
// Aplica los parámetros del preset CrtPi actual al backend de shaders
void Screen::applyCurrentCrtPiPreset() { // NOLINT(readability-convert-member-functions-to-static)
void Screen::applyCurrentCrtPiPreset() {
if (shader_backend_ && !Options::crtpi_presets.empty()) {
const auto& p = Options::crtpi_presets[static_cast<size_t>(Options::video.shader.current_crtpi_preset)];
Rendering::CrtPiParams params{
@@ -696,11 +678,9 @@ void Screen::initShaders() {
shader_backend_->init(window_, tex, "", "");
gpu_driver_ = shader_backend_->getDriverName();
// Propagar flags de vsync, integer scale, upscale y downscale al backend GPU
// Propagar flags de vsync e integer scale al backend GPU
shader_backend_->setVSync(Options::video.vertical_sync);
shader_backend_->setScaleMode(Options::video.integer_scale);
shader_backend_->setLinearUpscale(Options::video.supersampling.linear_upscale);
shader_backend_->setDownscaleAlgo(Options::video.supersampling.downscale_algo);
if (Options::video.shader.enabled) {
applyCurrentPostFXPreset();
@@ -718,7 +698,7 @@ void Screen::initShaders() {
}
// Obtiene información sobre la pantalla
void Screen::getDisplayInfo() { // NOLINT(readability-convert-member-functions-to-static)
void Screen::getDisplayInfo() {
std::cout << "\n** VIDEO SYSTEM **\n";
int num_displays = 0;
@@ -827,7 +807,7 @@ auto Screen::initSDLVideo() -> bool {
// 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)
void Screen::registerEmscriptenEventCallbacks() {
#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,
@@ -838,7 +818,7 @@ void Screen::registerEmscriptenEventCallbacks() { // NOLINT(readability-convert
}
// Crea el objeto de texto
void Screen::createText() { // NOLINT(readability-convert-member-functions-to-static)
void Screen::createText() {
// Carga la surface de la fuente directamente del archivo
auto surface = std::make_shared<Surface>(Resource::List::get()->get("aseprite.gif"));
+2 -6
View File
@@ -4,9 +4,9 @@
#include <SDL3/SDL_pixels.h> // Para Uint32
#include <cstddef> // Para size_t
#include <cstdint> // Para uint8_t
#include <memory> // Para shared_ptr, __shared_ptr_access
#include <string> // Para string
#include <utility> // Para std::pair
#include <vector> // Para vector
#include "core/rendering/palette_manager.hpp" // Para PaletteManager
@@ -18,7 +18,7 @@ class Text;
class Screen {
public:
// Tipos de filtro
enum class Filter : Uint32 {
enum class Filter : std::uint8_t {
NEAREST = 0,
LINEAR = 1,
};
@@ -64,11 +64,8 @@ class Screen {
void setPaletteSortMode(PaletteSortMode mode); // Establece modo de ordenación concreto
[[nodiscard]] auto getPaletteSortModeName() const -> std::string; // Nombre del modo de ordenación actual
void toggleShaders(); // Activa/desactiva todos los shaders respetando current_shader
void toggleSupersampling(); // Activa/desactiva el supersampling global
void reloadPostFX(); // Recarga el shader del preset actual sin toggle
void reloadCrtPi(); // Recarga el shader CrtPi del preset actual sin toggle
void setLinearUpscale(bool linear); // Upscale NEAREST (false) o LINEAR (true) en el paso SS
void setDownscaleAlgo(int algo); // 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
void setActiveShader(Rendering::ShaderType type); // Cambia el shader de post-procesado activo
void nextShader(); // Cicla al siguiente shader disponible (para futura UI)
@@ -89,7 +86,6 @@ class Screen {
[[nodiscard]] auto getLastFPS() const -> int { return fps_.last_value; }
[[nodiscard]] auto getZoomFactor() const -> float { return zoom_factor_; }
[[nodiscard]] static auto getMaxZoom() -> int;
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int>;
private:
// Estructuras
@@ -0,0 +1,144 @@
#pragma once
#ifdef __APPLE__
// Fragment shader del shader "crtpi" (algoritme CRT-Pi): scanlines amb
// pesos gaussians, multisample opcional, gamma i màscara de subpíxels.
namespace Rendering::Msl {
inline constexpr const char* kCrtpiFrag = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
struct CrtPiUniforms {
float scanline_weight;
float scanline_gap_brightness;
float bloom_factor;
float input_gamma;
float output_gamma;
float mask_brightness;
float curvature_x;
float curvature_y;
int mask_type;
int enable_scanlines;
int enable_multisample;
int enable_gamma;
int enable_curvature;
int enable_sharper;
float texture_width;
float texture_height;
};
static float2 crtpi_distort(float2 coord, float2 screen_scale, float cx, float cy) {
float2 curvature = float2(cx, cy);
float2 barrel_scale = 1.0f - (0.23f * curvature);
coord *= screen_scale;
coord -= 0.5f;
float rsq = coord.x * coord.x + coord.y * coord.y;
coord += coord * (curvature * rsq);
coord *= barrel_scale;
if (abs(coord.x) >= 0.5f || abs(coord.y) >= 0.5f) { return float2(-1.0f); }
coord += 0.5f;
coord /= screen_scale;
return coord;
}
static float crtpi_scan_weight(float dist, float sw, float gap) {
return max(1.0f - dist * dist * sw, gap);
}
static float crtpi_scan_line(float dy, float filter_w, float sw, float gap, bool ms) {
float w = crtpi_scan_weight(dy, sw, gap);
if (ms) {
w += crtpi_scan_weight(dy - filter_w, sw, gap);
w += crtpi_scan_weight(dy + filter_w, sw, gap);
w *= 0.3333333f;
}
return w;
}
fragment float4 crtpi_fs(PostVOut in [[stage_in]],
texture2d<float> tex [[texture(0)]],
sampler samp [[sampler(0)]],
constant CrtPiUniforms& u [[buffer(0)]]) {
float2 tex_size = float2(u.texture_width, u.texture_height);
// Amplada del filtre de scanline analític. 768 = alçada de referència
// CRT a la qual es va tarar l'algoritme original; 3 = divisió per
// subpíxel (R/G/B) del multisample. El resultat escala amb la textura
// d'entrada, de manera que més alçada → filtre més fi.
const float CRT_REFERENCE_HEIGHT = 768.0f;
const float SUBPIXEL_DIV = 3.0f;
float filter_width = (CRT_REFERENCE_HEIGHT / u.texture_height) / SUBPIXEL_DIV;
float2 texcoord = in.uv;
if (u.enable_curvature != 0) {
texcoord = crtpi_distort(texcoord, float2(1.0f, 1.0f), u.curvature_x, u.curvature_y);
if (texcoord.x < 0.0f) { return float4(0.0f, 0.0f, 0.0f, 1.0f); }
}
float2 coord_in_pixels = texcoord * tex_size;
float2 tc;
float scan_weight;
if (u.enable_sharper != 0) {
float2 temp = floor(coord_in_pixels) + 0.5f;
tc = temp / tex_size;
float2 deltas = coord_in_pixels - temp;
scan_weight = crtpi_scan_line(deltas.y, filter_width, u.scanline_weight, u.scanline_gap_brightness, u.enable_multisample != 0);
float2 signs = sign(deltas);
deltas.x *= 2.0f;
deltas = deltas * deltas;
deltas.y = deltas.y * deltas.y;
deltas.x *= 0.5f;
deltas.y *= 8.0f;
deltas /= tex_size;
deltas *= signs;
tc = tc + deltas;
} else {
float temp_y = floor(coord_in_pixels.y) + 0.5f;
float y_coord = temp_y / tex_size.y;
float dy = coord_in_pixels.y - temp_y;
scan_weight = crtpi_scan_line(dy, filter_width, u.scanline_weight, u.scanline_gap_brightness, u.enable_multisample != 0);
float sign_y = sign(dy);
dy = dy * dy;
dy = dy * dy;
dy *= 8.0f;
dy /= tex_size.y;
dy *= sign_y;
tc = float2(texcoord.x, y_coord + dy);
}
float3 colour = tex.sample(samp, tc).rgb;
if (u.enable_scanlines != 0) {
if (u.enable_gamma != 0) { colour = pow(colour, float3(u.input_gamma)); }
colour *= scan_weight * u.bloom_factor;
if (u.enable_gamma != 0) { colour = pow(colour, float3(1.0f / u.output_gamma)); }
}
if (u.mask_type == 1) {
float wm = fract(in.pos.x * 0.5f);
float3 mask = (wm < 0.5f) ? float3(u.mask_brightness, 1.0f, u.mask_brightness)
: float3(1.0f, u.mask_brightness, 1.0f);
colour *= mask;
} else if (u.mask_type == 2) {
float wm = fract(in.pos.x * 0.3333333f);
float3 mask = float3(u.mask_brightness);
if (wm < 0.3333333f) mask.x = 1.0f;
else if (wm < 0.6666666f) mask.y = 1.0f;
else mask.z = 1.0f;
colour *= mask;
}
return float4(colour, 1.0f);
}
)";
} // namespace Rendering::Msl
#endif // __APPLE__
@@ -0,0 +1,168 @@
#pragma once
#ifdef __APPLE__
// Fragment shader del shader "postfx": vignette, chroma, scanlines, mask,
// gamma, curvature, bleeding i flicker. Els paràmetres venen via uniforms.
//
// IMPORTANT: mantenir sincronitzat a mà amb data/shaders/postfx.frag. SDL3 GPU
// compila aquest string MSL en runtime; no hi ha generador automàtic. Qualsevol
// canvi a la struct d'uniforms o a la lògica del GLSL cal replicar-lo ací al
// mateix commit. Mida total = 64 bytes (4 × vec4).
namespace Rendering::Msl {
inline constexpr const char* kPostfxFrag = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
struct PostFXUniforms {
float vignette_strength;
float chroma_min;
float scanline_strength;
float screen_height;
float mask_strength;
float gamma_strength;
float curvature;
float bleeding;
float pixel_scale;
float time;
float flicker;
float chroma_max;
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
float scan_dark_ratio;
float scan_dark_floor;
float scan_edge_soft;
float pad3;
};
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
// NEAREST quan l'offset de chroma és subpíxel.
static float sampleBilinearX(float2 uv_target, int channel, texture2d<float> scene, sampler samp) {
float2 tex_size = float2(scene.get_width(), scene.get_height());
float px = uv_target.x * tex_size.x - 0.5f;
float p_floor = floor(px);
float f = px - p_floor;
float4 c0 = scene.sample(samp, float2((p_floor + 0.5f) / tex_size.x, uv_target.y));
float4 c1 = scene.sample(samp, float2((p_floor + 1.5f) / tex_size.x, uv_target.y));
return mix(c0[channel], c1[channel], f);
}
static float3 rgb_to_ycc(float3 rgb) {
return float3(
0.299f*rgb.r + 0.587f*rgb.g + 0.114f*rgb.b,
-0.169f*rgb.r - 0.331f*rgb.g + 0.500f*rgb.b + 0.5f,
0.500f*rgb.r - 0.419f*rgb.g - 0.081f*rgb.b + 0.5f
);
}
static float3 ycc_to_rgb(float3 ycc) {
float y = ycc.x;
float cb = ycc.y - 0.5f;
float cr = ycc.z - 0.5f;
return clamp(float3(
y + 1.402f*cr,
y - 0.344f*cb - 0.714f*cr,
y + 1.772f*cb
), 0.0f, 1.0f);
}
fragment float4 postfx_fs(PostVOut in [[stage_in]],
texture2d<float> scene [[texture(0)]],
sampler samp [[sampler(0)]],
constant PostFXUniforms& u [[buffer(0)]]) {
float2 uv = in.uv;
if (u.curvature > 0.0f) {
float2 c = uv - 0.5f;
float rsq = dot(c, c);
float2 dist = float2(0.05f, 0.1f) * u.curvature;
float2 barrelScale = 1.0f - 0.23f * dist;
c += c * (dist * rsq);
c *= barrelScale;
if (abs(c.x) >= 0.5f || abs(c.y) >= 0.5f) {
return float4(0.0f, 0.0f, 0.0f, 1.0f);
}
uv = c + 0.5f;
}
float3 base = scene.sample(samp, uv).rgb;
float3 colour;
if (u.bleeding > 0.0f) {
float tw = float(scene.get_width());
float step = 1.0f / tw;
float3 ycc = rgb_to_ycc(base);
float3 ycc_l2 = rgb_to_ycc(scene.sample(samp, uv - float2(2.0f*step, 0.0f)).rgb);
float3 ycc_l1 = rgb_to_ycc(scene.sample(samp, uv - float2(1.0f*step, 0.0f)).rgb);
float3 ycc_r1 = rgb_to_ycc(scene.sample(samp, uv + float2(1.0f*step, 0.0f)).rgb);
float3 ycc_r2 = rgb_to_ycc(scene.sample(samp, uv + float2(2.0f*step, 0.0f)).rgb);
ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0f + ycc.yz*2.0f + ycc_r1.yz*2.0f + ycc_r2.yz) / 8.0f;
colour = mix(base, ycc_to_rgb(ycc), u.bleeding);
} else {
colour = base;
}
// Chroma — varia entre chroma_min i chroma_max via sinusoidal; si min == max
// queda estàtic. Mostreig bilinear horitzontal per evitar el "tic-tac" del
// NEAREST sampler amb offsets subpíxel.
if (u.chroma_min > 0.0f || u.chroma_max > 0.0f) {
float ca = mix(u.chroma_min, u.chroma_max, 0.5f + 0.5f * sin(u.time * 7.3f)) * 0.005f;
colour.r = sampleBilinearX(uv + float2(ca, 0.0f), 0, scene, samp);
colour.b = sampleBilinearX(uv - float2(ca, 0.0f), 2, scene, samp);
}
if (u.gamma_strength > 0.0f) {
float3 lin = pow(colour, float3(2.4f));
colour = mix(colour, lin, u.gamma_strength);
}
// Scanlines — 3 subpíxels per fila lògica (2 brillants + 1 fosca). Transició
// suavitzada amb smoothstep d'ample ≈ 1 píxel físic (estil crtpi: filtratge
// analític continu). scan_edge_soft = 0 recupera el step dur de l'original.
if (u.scanline_strength > 0.0f) {
float ps = max(u.pixel_scale, 1.0f);
float sub = fract(uv.y * u.screen_height);
float dark_center = 1.0f - u.scan_dark_ratio * 0.5f;
float d = abs(sub - dark_center);
d = min(d, 1.0f - d);
float half_width = u.scan_dark_ratio * 0.5f;
float softness = u.scan_edge_soft * 0.5f / ps;
float band = 1.0f - smoothstep(half_width - softness, half_width + softness, d);
float scan = mix(1.0f, u.scan_dark_floor, band);
colour *= mix(1.0f, scan, u.scanline_strength);
}
if (u.gamma_strength > 0.0f) {
float3 enc = pow(colour, float3(1.0f/2.2f));
colour = mix(colour, enc, u.gamma_strength);
}
float2 d = uv - 0.5f;
float vignette = 1.0f - dot(d, d) * u.vignette_strength;
colour *= clamp(vignette, 0.0f, 1.0f);
if (u.mask_strength > 0.0f) {
float whichMask = fract(in.pos.x * 0.3333333f);
float3 mask = float3(0.80f);
if (whichMask < 0.3333333f) mask.x = 1.0f;
else if (whichMask < 0.6666667f) mask.y = 1.0f;
else mask.z = 1.0f;
colour = mix(colour, colour * mask, u.mask_strength);
}
if (u.flicker > 0.0f) {
float flicker_wave = sin(u.time * 100.0f) * 0.5f + 0.5f;
colour *= 1.0f - u.flicker * 0.04f * flicker_wave;
}
return float4(colour, 1.0f);
}
)";
} // namespace Rendering::Msl
#endif // __APPLE__
@@ -0,0 +1,30 @@
#pragma once
#ifdef __APPLE__
// Vertex shader compartit per tots els pipelines de post-procés:
// fullscreen-triangle que cobreix tota l'àrea del swapchain amb UVs a [0,1].
namespace Rendering::Msl {
inline constexpr const char* kPostfxVert = R"(
#include <metal_stdlib>
using namespace metal;
struct PostVOut {
float4 pos [[position]];
float2 uv;
};
vertex PostVOut postfx_vs(uint vid [[vertex_id]]) {
const float2 positions[3] = { {-1.0, -1.0}, {3.0, -1.0}, {-1.0, 3.0} };
const float2 uvs[3] = { { 0.0, 1.0}, {2.0, 1.0}, { 0.0,-1.0} };
PostVOut out;
out.pos = float4(positions[vid], 0.0, 1.0);
out.uv = uvs[vid];
return out;
}
)";
} // namespace Rendering::Msl
#endif // __APPLE__
File diff suppressed because it is too large Load Diff
@@ -7,20 +7,28 @@
// PostFX uniforms pushed to fragment stage each frame.
// Must match the MSL struct and GLSL uniform block layout.
// 12 floats = 48 bytes — meets Metal/Vulkan 16-byte alignment requirement.
// 16 floats = 64 bytes (4 × vec4) — meets Metal/Vulkan 16-byte alignment.
struct PostFXUniforms {
// vec4 #0
float vignette_strength; // 0 = none, ~0.8 = subtle
float chroma_strength; // 0 = off, ~0.2 = subtle chromatic aberration
float chroma_min; // aberració cromàtica mínima (sempre present)
float scanline_strength; // 0 = off, 1 = full
float screen_height; // logical height in pixels (used by bleeding effect)
float mask_strength; // 0 = off, 1 = full phosphor dot mask
float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction
float curvature; // 0 = flat, 1 = max barrel distortion
float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f)
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — keep struct at 48 bytes (3 × 16)
// vec4 #1
float mask_strength; // 0 = off, 1 = full phosphor dot mask
float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction
float curvature; // 0 = flat, 1 = max barrel distortion
float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding
// vec4 #2
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f)
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
float chroma_max; // si == chroma_min queda estàtic; si != pulsa sinusoidalment
// vec4 #3 — paràmetres de forma de les scanlines (exposats per preset)
float scan_dark_ratio; // fracció de subfila fosca (1/3 = 0.333 per defecte)
float scan_dark_floor; // brillantor de la subfila fosca (0.42 per defecte)
float scan_edge_soft; // suavitzat de la transició (0 = step dur, 1 = 1px físic)
float pad3;
};
// CrtPi uniforms pushed to fragment stage each frame.
@@ -49,15 +57,6 @@ struct CrtPiUniforms {
float texture_height; // Alto del canvas en píxeles (inyectado en render)
};
// Downscale uniforms pushed to the Lanczos downscale fragment stage.
// 1 int + 3 floats = 16 bytes — meets Metal/Vulkan alignment.
struct DownscaleUniforms {
int algorithm; // 0 = Lanczos2 (ventana 2), 1 = Lanczos3 (ventana 3)
float pad0;
float pad1;
float pad2;
};
namespace Rendering {
/**
@@ -78,7 +77,7 @@ namespace Rendering {
const std::string& fragment_source) -> bool override;
void render() override;
void setTextureSize(float width, float height) override {}
void setTextureSize(float /*width*/, float /*height*/) override {}
void cleanup() final; // Libera pipeline/texturas pero mantiene el device vivo
void destroy(); // Limpieza completa (device + swapchain); llamar solo al cerrar
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
@@ -99,18 +98,6 @@ namespace Rendering {
// Activa/desactiva escalado entero (integer scale)
void setScaleMode(bool integer_scale) override;
// Establece factor de supersampling (1 = off, 3 = 3×SS)
void setOversample(int factor) override;
// Activa/desactiva interpolación LINEAR en el upscale (false = NEAREST)
void setLinearUpscale(bool linear) override;
// Selecciona algoritmo de downscale: 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
void setDownscaleAlgo(int algo) override;
// Devuelve las dimensiones de la textura de supersampling (0,0 si SS desactivado)
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int> override;
// Selecciona el shader de post-procesado activo (POSTFX o CRTPI)
void setActiveShader(ShaderType type) override;
@@ -137,42 +124,76 @@ namespace Rendering {
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
auto createPipeline() -> bool;
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
auto recreateScaledTexture(int factor) -> bool; // Recrea scaled_texture_ para factor dado
static auto calcSsFactor(float zoom) -> int; // Primer múltiplo de 3 >= zoom (mín 3)
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
auto createPostfxVertexShader() -> SDL_GPUShader*; // Vertex shader fullscreen-triangle compartido (MSL/SPIRV)
// Empaqueta el patrón vert(postfx) + frag dado + target format en un pipeline gráfico.
// Toma ownership de `frag`: lo libera tras crear el pipeline (o si vert falla).
auto createPostfxLikePipeline(SDL_GPUShader* frag, SDL_GPUTextureFormat format, const char* debug_name) -> SDL_GPUGraphicsPipeline*;
// Sub-passos de render() (extrets per reduir complexitat ciclomàtica)
struct Viewport {
float x, y, w, h;
};
void uploadSceneTexture(SDL_GPUCommandBuffer* cmd);
[[nodiscard]] auto computeViewport(Uint32 sw, Uint32 sh) const -> Viewport;
void updateDynamicUniforms(float viewport_h);
void runCrtPiPass(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchain, const Viewport& vp);
void runDirectPostfxPass(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchain, const Viewport& vp);
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
SDL_Window* window_ = nullptr;
SDL_GPUDevice* device_ = nullptr;
SDL_GPUGraphicsPipeline* pipeline_ = nullptr; // PostFX pass (→ swapchain o → postfx_texture_)
SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr; // CrtPi pass (→ swapchain directo, sin SS)
SDL_GPUGraphicsPipeline* postfx_offscreen_pipeline_ = nullptr; // PostFX → postfx_texture_ (B8G8R8A8, solo con Lanczos)
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS)
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr; // Lanczos downscale (solo con SS + algo > 0)
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del juego (game_width_ × game_height_)
SDL_GPUTexture* scaled_texture_ = nullptr; // Upscale target (game×factor), solo con SS
SDL_GPUTexture* postfx_texture_ = nullptr; // PostFX output a resolución escalada, solo con Lanczos
SDL_GPUGraphicsPipeline* pipeline_ = nullptr; // PostFX pass → swapchain
SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr; // CrtPi pass → swapchain
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del juego (game_width_ × game_height_)
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
SDL_GPUSampler* sampler_ = nullptr; // NEAREST
SDL_GPUSampler* linear_sampler_ = nullptr; // LINEAR
SDL_GPUSampler* sampler_ = nullptr; // NEAREST
PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_strength = 0.15F, .scanline_strength = 0.7F, .screen_height = 192.0F, .pixel_scale = 1.0F, .oversample = 1.0F};
CrtPiUniforms crtpi_uniforms_{.scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = 1, .enable_multisample = 1, .enable_gamma = 1};
PostFXUniforms uniforms_{
.vignette_strength = 0.6F,
.chroma_min = 0.15F,
.scanline_strength = 0.7F,
.screen_height = 192.0F,
.mask_strength = 0.0F,
.gamma_strength = 0.0F,
.curvature = 0.0F,
.bleeding = 0.0F,
.pixel_scale = 1.0F,
.time = 0.0F,
.flicker = 0.0F,
.chroma_max = 0.15F,
.scan_dark_ratio = 0.333F,
.scan_dark_floor = 0.42F,
.scan_edge_soft = 1.0F,
.pad3 = 0.0F};
CrtPiUniforms crtpi_uniforms_{
.scanline_weight = 6.0F,
.scanline_gap_brightness = 0.12F,
.bloom_factor = 3.5F,
.input_gamma = 2.4F,
.output_gamma = 2.2F,
.mask_brightness = 0.80F,
.curvature_x = 0.05F,
.curvature_y = 0.10F,
.mask_type = 2,
.enable_scanlines = 1,
.enable_multisample = 1,
.enable_gamma = 1,
.enable_curvature = 0,
.enable_sharper = 0,
.texture_width = 0.0F,
.texture_height = 0.0F};
ShaderType active_shader_ = ShaderType::POSTFX; // Shader de post-procesado activo
int game_width_ = 0; // Dimensiones originales del canvas
int game_height_ = 0;
int ss_factor_ = 0; // Factor SS activo (3, 6, 9...) o 0 si SS desactivado
int oversample_ = 1; // SS on/off (1 = off, >1 = on)
int downscale_algo_ = 1; // 0 = bilinear legacy, 1 = Lanczos2, 2 = Lanczos3
std::string driver_name_;
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
bool is_initialized_ = false;
bool vsync_ = true;
bool integer_scale_ = false;
bool linear_upscale_ = false; // Upscale NEAREST (false) o LINEAR (true)
};
} // namespace Rendering
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1445,5 +1445,6 @@ static const uint8_t kpostfx_vert_spv[] = {
0x38,
0x00,
0x01,
0x00};
0x00,
};
static const size_t kpostfx_vert_spv_size = 1444;
@@ -1,633 +0,0 @@
#pragma once
#include <cstddef>
#include <cstdint>
static const uint8_t kupscale_frag_spv[] = {
0x03,
0x02,
0x23,
0x07,
0x00,
0x00,
0x01,
0x00,
0x0b,
0x00,
0x0d,
0x00,
0x14,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x11,
0x00,
0x02,
0x00,
0x01,
0x00,
0x00,
0x00,
0x0b,
0x00,
0x06,
0x00,
0x01,
0x00,
0x00,
0x00,
0x47,
0x4c,
0x53,
0x4c,
0x2e,
0x73,
0x74,
0x64,
0x2e,
0x34,
0x35,
0x30,
0x00,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x03,
0x00,
0x00,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x0f,
0x00,
0x07,
0x00,
0x04,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x6d,
0x61,
0x69,
0x6e,
0x00,
0x00,
0x00,
0x00,
0x09,
0x00,
0x00,
0x00,
0x11,
0x00,
0x00,
0x00,
0x10,
0x00,
0x03,
0x00,
0x04,
0x00,
0x00,
0x00,
0x07,
0x00,
0x00,
0x00,
0x03,
0x00,
0x03,
0x00,
0x02,
0x00,
0x00,
0x00,
0xc2,
0x01,
0x00,
0x00,
0x04,
0x00,
0x0a,
0x00,
0x47,
0x4c,
0x5f,
0x47,
0x4f,
0x4f,
0x47,
0x4c,
0x45,
0x5f,
0x63,
0x70,
0x70,
0x5f,
0x73,
0x74,
0x79,
0x6c,
0x65,
0x5f,
0x6c,
0x69,
0x6e,
0x65,
0x5f,
0x64,
0x69,
0x72,
0x65,
0x63,
0x74,
0x69,
0x76,
0x65,
0x00,
0x00,
0x04,
0x00,
0x08,
0x00,
0x47,
0x4c,
0x5f,
0x47,
0x4f,
0x4f,
0x47,
0x4c,
0x45,
0x5f,
0x69,
0x6e,
0x63,
0x6c,
0x75,
0x64,
0x65,
0x5f,
0x64,
0x69,
0x72,
0x65,
0x63,
0x74,
0x69,
0x76,
0x65,
0x00,
0x05,
0x00,
0x04,
0x00,
0x04,
0x00,
0x00,
0x00,
0x6d,
0x61,
0x69,
0x6e,
0x00,
0x00,
0x00,
0x00,
0x05,
0x00,
0x05,
0x00,
0x09,
0x00,
0x00,
0x00,
0x6f,
0x75,
0x74,
0x5f,
0x63,
0x6f,
0x6c,
0x6f,
0x72,
0x00,
0x00,
0x00,
0x05,
0x00,
0x04,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x73,
0x63,
0x65,
0x6e,
0x65,
0x00,
0x00,
0x00,
0x05,
0x00,
0x04,
0x00,
0x11,
0x00,
0x00,
0x00,
0x76,
0x5f,
0x75,
0x76,
0x00,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x09,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x21,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x22,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x47,
0x00,
0x04,
0x00,
0x11,
0x00,
0x00,
0x00,
0x1e,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x13,
0x00,
0x02,
0x00,
0x02,
0x00,
0x00,
0x00,
0x21,
0x00,
0x03,
0x00,
0x03,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x16,
0x00,
0x03,
0x00,
0x06,
0x00,
0x00,
0x00,
0x20,
0x00,
0x00,
0x00,
0x17,
0x00,
0x04,
0x00,
0x07,
0x00,
0x00,
0x00,
0x06,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x08,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0x07,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x08,
0x00,
0x00,
0x00,
0x09,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0x19,
0x00,
0x09,
0x00,
0x0a,
0x00,
0x00,
0x00,
0x06,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x1b,
0x00,
0x03,
0x00,
0x0b,
0x00,
0x00,
0x00,
0x0a,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x0c,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x0b,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x0c,
0x00,
0x00,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x17,
0x00,
0x04,
0x00,
0x0f,
0x00,
0x00,
0x00,
0x06,
0x00,
0x00,
0x00,
0x02,
0x00,
0x00,
0x00,
0x20,
0x00,
0x04,
0x00,
0x10,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x0f,
0x00,
0x00,
0x00,
0x3b,
0x00,
0x04,
0x00,
0x10,
0x00,
0x00,
0x00,
0x11,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x36,
0x00,
0x05,
0x00,
0x02,
0x00,
0x00,
0x00,
0x04,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x03,
0x00,
0x00,
0x00,
0xf8,
0x00,
0x02,
0x00,
0x05,
0x00,
0x00,
0x00,
0x3d,
0x00,
0x04,
0x00,
0x0b,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x00,
0x00,
0x0d,
0x00,
0x00,
0x00,
0x3d,
0x00,
0x04,
0x00,
0x0f,
0x00,
0x00,
0x00,
0x12,
0x00,
0x00,
0x00,
0x11,
0x00,
0x00,
0x00,
0x57,
0x00,
0x05,
0x00,
0x07,
0x00,
0x00,
0x00,
0x13,
0x00,
0x00,
0x00,
0x0e,
0x00,
0x00,
0x00,
0x12,
0x00,
0x00,
0x00,
0x3e,
0x00,
0x03,
0x00,
0x09,
0x00,
0x00,
0x00,
0x13,
0x00,
0x00,
0x00,
0xfd,
0x00,
0x01,
0x00,
0x38,
0x00,
0x01,
0x00};
static const size_t kupscale_frag_spv_size = 628;
+10 -31
View File
@@ -2,13 +2,13 @@
#include <SDL3/SDL.h>
#include <cstdint>
#include <string>
#include <utility>
namespace Rendering {
/** @brief Identificador del shader de post-procesado activo */
enum class ShaderType { POSTFX,
enum class ShaderType : std::uint8_t { POSTFX,
CRTPI };
/**
@@ -18,12 +18,19 @@ namespace Rendering {
struct PostFXParams {
float vignette = 0.0F; // Intensidad de la viñeta
float scanlines = 0.0F; // Intensidad de las scanlines
float chroma = 0.0F; // Aberración cromática
// Aberració cromàtica — varia entre min i max via sinusoidal; si coincideixen
// queda estàtica. min > 0 garanteix que la imatge mai sigui lliure de chroma.
float chroma_min = 0.0F;
float chroma_max = 0.0F;
float mask = 0.0F; // Máscara de fósforo RGB
float gamma = 0.0F; // Corrección gamma (blend 0=off, 1=full)
float curvature = 0.0F; // Curvatura barrel CRT
float bleeding = 0.0F; // Sangrado de color NTSC
float flicker = 0.0F; // Parpadeo de fósforo CRT ~50 Hz
// Forma de les scanlines — 3 subpíxels per fila lògica per defecte.
float scan_dark_ratio = 0.333F; // fracció obscura (1/3)
float scan_dark_floor = 0.42F; // brillantor subfila fosca
float scan_edge_soft = 1.0F; // 0 = step dur; 1 = suavitzat 1 px físic
};
/**
@@ -109,34 +116,6 @@ namespace Rendering {
*/
virtual void setScaleMode(bool /*integer_scale*/) {}
/**
* @brief Establece el factor de supersampling (1 = off, 3 = 3× SS)
* Con factor > 1, la textura GPU se crea a game×factor resolución y
* las scanlines se hornean en CPU (uploadPixels). El sampler usa LINEAR.
*/
virtual void setOversample(int /*factor*/) {}
/**
* @brief Activa/desactiva interpolación LINEAR en el paso de upscale (SS).
* Por defecto NEAREST (false). Solo tiene efecto con supersampling activo.
*/
virtual void setLinearUpscale(bool /*linear*/) {}
[[nodiscard]] virtual auto isLinearUpscale() const -> bool { return false; }
/**
* @brief Selecciona el algoritmo de downscale tras el PostFX (SS activo).
* 0 = bilinear legacy (comportamiento actual, sin textura intermedia),
* 1 = Lanczos2 (ventana 2, ~25 muestras), 2 = Lanczos3 (ventana 3, ~49 muestras).
*/
virtual void setDownscaleAlgo(int /*algo*/) {}
[[nodiscard]] virtual auto getDownscaleAlgo() const -> int { return 0; }
/**
* @brief Devuelve las dimensiones de la textura de supersampling.
* @return Par (ancho, alto) en píxeles; (0, 0) si SS está desactivado.
*/
[[nodiscard]] virtual auto getSsTextureSize() const -> std::pair<int, int> { return {0, 0}; }
/**
* @brief Verifica si el backend está usando aceleración por hardware
* @return true si usa aceleración (OpenGL/Metal/Vulkan)
@@ -31,7 +31,7 @@ auto convertYAMLFramesToRects(const fkyaml::node& frames_node, float frame_width
}
// Carga las animaciones desde un fichero YAML
auto AnimatedSprite::loadAnimationsFromYAML(const std::string& file_path, std::shared_ptr<Surface>& surface, float& frame_width, float& frame_height) -> std::vector<AnimationData> { // NOLINT(readability-convert-member-functions-to-static)
auto AnimatedSprite::loadAnimationsFromYAML(const std::string& file_path, std::shared_ptr<Surface>& surface, float& frame_width, float& frame_height) -> std::vector<AnimationData> {
std::vector<AnimationData> animations;
// Extract filename for logging
@@ -224,7 +224,7 @@ AnimatedSprite::AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
}
// Obtiene el indice de la animación a partir del nombre
auto AnimatedSprite::getIndex(const std::string& name) -> int { // NOLINT(readability-convert-member-functions-to-static)
auto AnimatedSprite::getIndex(const std::string& name) -> int {
auto index = -1;
for (const auto& a : animations_) {
@@ -238,7 +238,7 @@ auto AnimatedSprite::getIndex(const std::string& name) -> int { // NOLINT(reada
}
// Calcula el frame correspondiente a la animación (time-based)
void AnimatedSprite::animate(float delta_time) { // NOLINT(readability-convert-member-functions-to-static)
void AnimatedSprite::animate(float delta_time) {
if (animations_.empty()) { return; }
if (animations_[current_animation_].speed <= 0.0F) {
return;
@@ -2,14 +2,15 @@
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <cstdint> // Para uint8_t
#include <memory> // Para shared_ptr
#include "core/rendering/sprite/animated_sprite.hpp" // Para SurfaceAnimatedSprite
class Surface;
// Direcció de la dissolució
enum class DissolveDirection { NONE,
enum class DissolveDirection : std::uint8_t { NONE,
DOWN,
UP };
@@ -41,7 +42,7 @@ class DissolveSprite : public AnimatedSprite {
void setColorReplace(Uint8 source, Uint8 target);
private:
enum class TransitionMode { NONE,
enum class TransitionMode : std::uint8_t { NONE,
DISSOLVING,
GENERATING };
+3 -3
View File
@@ -10,10 +10,10 @@ class Surface; // lines 5-5
class Sprite {
public:
// Constructores
Sprite(std::shared_ptr<Surface>, float x, float y, float w, float h);
Sprite(std::shared_ptr<Surface>, SDL_FRect rect);
Sprite(std::shared_ptr<Surface> surface, float x, float y, float w, float h);
Sprite(std::shared_ptr<Surface> surface, SDL_FRect rect);
Sprite();
explicit Sprite(std::shared_ptr<Surface>);
explicit Sprite(std::shared_ptr<Surface> surface);
// Destructor
virtual ~Sprite() = default;
+20 -20
View File
@@ -148,14 +148,14 @@ 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)
void Surface::clear(Uint8 color) {
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);
}
// Pone un pixel en la SurfaceData
void Surface::putPixel(int x, int y, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
void Surface::putPixel(int x, int y, Uint8 color) {
if (x < 0 || y < 0 || x >= surface_data_->width || y >= surface_data_->height) {
return; // Coordenadas fuera de rango
}
@@ -168,7 +168,7 @@ void Surface::putPixel(int x, int y, Uint8 color) { // NOLINT(readability-conve
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)
void Surface::fillRect(const SDL_FRect* rect, Uint8 color) {
// 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);
@@ -180,12 +180,12 @@ void Surface::fillRect(const SDL_FRect* rect, Uint8 color) { // NOLINT(readabil
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);
std::memset(data_ptr + (static_cast<ptrdiff_t>(y) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
}
}
// Dibuja el borde de un rectangulo
void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) {
// 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);
@@ -196,8 +196,8 @@ void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(re
Uint8* data_ptr = surface_data_->data.get();
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);
std::memset(data_ptr + (static_cast<ptrdiff_t>(y_start) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
std::memset(data_ptr + ((static_cast<ptrdiff_t>(y_end) - 1) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
// Dibujar bordes verticales
for (int y = y_start; y < y_end; ++y) {
@@ -212,7 +212,7 @@ void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(re
}
// 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)
void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) {
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));
@@ -247,7 +247,7 @@ void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) { /
}
}
void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) { // NOLINT(readability-make-member-function-const)
void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) {
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
// Determina la región de origen (clip) a renderizar
@@ -415,7 +415,7 @@ static auto computeFadeDensity(int screen_y, int fade_h, int canvas_height) -> f
}
// Render amb dissolució als cantons superior/inferior (hash 2D, sense parpelleig)
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, SDL_FRect* src_rect) const {
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, const 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) : surface_data_->width;
@@ -452,7 +452,7 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
}
// Idem però reemplaçant un color índex
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect) const {
void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, const 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) : surface_data_->width;
@@ -513,7 +513,7 @@ void Surface::toARGBBuffer(Uint32* buffer) const {
}
// Vuelca la superficie a una textura
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) { // NOLINT(readability-convert-member-functions-to-static)
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) {
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {
throw std::runtime_error("Renderer or texture is null.");
}
@@ -539,8 +539,8 @@ void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) { //
const int WIDTH = surface_data_->width;
const int HEIGHT = surface_data_->height;
for (int y = 0; y < HEIGHT; ++y) {
const Uint8* src_row = src + (y * WIDTH);
Uint32* dst_row = pixels + (y * row_stride);
const Uint8* src_row = src + (static_cast<ptrdiff_t>(y) * WIDTH);
Uint32* dst_row = pixels + (static_cast<ptrdiff_t>(y) * row_stride);
for (int x = 0; x < WIDTH; ++x) {
dst_row[x] = pal[src_row[x]];
}
@@ -555,7 +555,7 @@ void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture) { //
}
// Vuelca la superficie a una textura
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FRect* src_rect, SDL_FRect* dest_rect) { // NOLINT(readability-convert-member-functions-to-static)
void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FRect* src_rect, SDL_FRect* dest_rect) {
if ((renderer == nullptr) || (texture == nullptr) || !surface_data_) {
throw std::runtime_error("Renderer or texture is null.");
}
@@ -588,8 +588,8 @@ void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FR
const int WIDTH = surface_data_->width;
const int HEIGHT = surface_data_->height;
for (int y = 0; y < HEIGHT; ++y) {
const Uint8* src_row = src + (y * WIDTH);
Uint32* dst_row = pixels + (y * row_stride);
const Uint8* src_row = src + (static_cast<ptrdiff_t>(y) * WIDTH);
Uint32* dst_row = pixels + (static_cast<ptrdiff_t>(y) * row_stride);
for (int x = 0; x < WIDTH; ++x) {
dst_row[x] = pal[src_row[x]];
}
@@ -604,7 +604,7 @@ 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)
auto Surface::fadePalette() -> bool {
static constexpr int PALETTE_SIZE = 19;
static_assert(std::tuple_size_v<Palette> >= PALETTE_SIZE, "Palette size is insufficient for fadePalette operation.");
@@ -621,7 +621,7 @@ auto Surface::fadePalette() -> bool { // NOLINT(readability-convert-member-func
}
// Realiza un efecto de fundido en la paleta secundaria
auto Surface::fadeSubPalette(Uint32 delay) -> bool { // NOLINT(readability-convert-member-functions-to-static)
auto Surface::fadeSubPalette(Uint32 delay) -> bool {
// Variable estática para almacenar el último tick
static Uint32 last_tick_ = 0;
@@ -652,4 +652,4 @@ auto Surface::fadeSubPalette(Uint32 delay) -> bool { // NOLINT(readability-conv
}
// Restaura la sub paleta a su estado original
void Surface::resetSubPalette() { initializeSubPalette(sub_palette_); } // NOLINT(readability-convert-member-functions-to-static)
void Surface::resetSubPalette() { initializeSubPalette(sub_palette_); }
+2 -2
View File
@@ -87,10 +87,10 @@ class Surface {
void renderWithColorReplace(int x, int y, Uint8 source_color = 0, Uint8 target_color = 0, SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE) const;
// Render amb dissolució als cantons superior/inferior (hash 2D, sense parpelleig)
void renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, SDL_FRect* src_rect = nullptr) const;
void renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, const SDL_FRect* src_rect = nullptr) const;
// Idem però reemplaçant un color índex (per a sprites sobre fons del mateix color)
void renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, SDL_FRect* src_rect = nullptr) const;
void renderWithVerticalFade(int x, int y, int fade_h, int canvas_height, Uint8 source_color, Uint8 target_color, const SDL_FRect* src_rect = nullptr) const;
// Establece un color en la paleta
void setColor(int index, Uint32 color);
+16 -16
View File
@@ -14,7 +14,7 @@
#include "utils/utils.hpp" // Para getFileName, stringToColor, printWithDots
// Extrae el siguiente codepoint UTF-8 de la cadena, avanzando 'pos' al byte siguiente
auto Text::nextCodepoint(const std::string& s, size_t& pos) -> uint32_t { // NOLINT(readability-convert-member-functions-to-static)
auto Text::nextCodepoint(const std::string& s, size_t& pos) -> uint32_t {
auto c = static_cast<unsigned char>(s[pos]);
uint32_t cp = 0;
size_t extra = 0;
@@ -22,11 +22,11 @@ auto Text::nextCodepoint(const std::string& s, size_t& pos) -> uint32_t { // NO
if (c < 0x80) {
cp = c;
extra = 0;
} else if (c < 0xC0) {
pos++;
return 0xFFFD;
} // byte de continuación suelto
else if (c < 0xE0) {
} else if (c < 0xE0) {
if (c < 0xC0) {
pos++;
return 0xFFFD; // byte de continuación suelto
}
cp = c & 0x1F;
extra = 1;
} else if (c < 0xF0) {
@@ -50,7 +50,7 @@ auto Text::nextCodepoint(const std::string& s, size_t& pos) -> uint32_t { // NO
}
// Convierte un codepoint Unicode a una cadena UTF-8
auto Text::codepointToUtf8(uint32_t cp) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
auto Text::codepointToUtf8(uint32_t cp) -> std::string {
std::string result;
if (cp < 0x80) {
result += static_cast<char>(cp);
@@ -72,7 +72,7 @@ auto Text::codepointToUtf8(uint32_t cp) -> std::string { // NOLINT(readability-
// Carga un fichero de definición de fuente .fnt
// Formato: líneas "clave valor", comentarios con #, gliphos como "codepoint ancho"
auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> { // NOLINT(readability-convert-member-functions-to-static)
auto Text::loadTextFile(const std::string& file_path) -> std::shared_ptr<File> {
auto tf = std::make_shared<File>();
auto file_data = Resource::Helper::loadFile(file_path);
@@ -148,7 +148,7 @@ Text::Text(const std::shared_ptr<Surface>& surface, const std::shared_ptr<File>&
}
// Escribe texto en pantalla
void Text::write(int x, int y, const std::string& text, int kerning, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
void Text::write(int x, int y, const std::string& text, int kerning, int lenght) {
int shift = 0;
int glyphs_done = 0;
size_t pos = 0;
@@ -170,7 +170,7 @@ void Text::write(int x, int y, const std::string& text, int kerning, int lenght)
}
// Escribe el texto en una surface
auto Text::writeToSurface(const std::string& text, int zoom, int kerning) -> std::shared_ptr<Surface> { // NOLINT(readability-make-member-function-const)
auto Text::writeToSurface(const std::string& text, int zoom, int kerning) -> std::shared_ptr<Surface> {
auto width = length(text, kerning) * zoom;
auto height = box_height_ * zoom;
auto surface = std::make_shared<Surface>(width, height);
@@ -184,7 +184,7 @@ auto Text::writeToSurface(const std::string& text, int zoom, int kerning) -> std
}
// Escribe el texto con extras en una surface
auto Text::writeDXToSurface(Uint8 flags, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int lenght) -> std::shared_ptr<Surface> { // NOLINT(readability-make-member-function-const)
auto Text::writeDXToSurface(Uint8 flags, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int lenght) -> std::shared_ptr<Surface> {
auto width = Text::length(text, kerning) + shadow_distance;
auto height = box_height_ + shadow_distance;
auto surface = std::make_shared<Surface>(width, height);
@@ -198,7 +198,7 @@ auto Text::writeDXToSurface(Uint8 flags, const std::string& text, int kerning, U
}
// Escribe el texto con colores
void Text::writeColored(int x, int y, const std::string& text, Uint8 color, int kerning, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
void Text::writeColored(int x, int y, const std::string& text, Uint8 color, int kerning, int lenght) {
int shift = 0;
int glyphs_done = 0;
size_t pos = 0;
@@ -232,7 +232,7 @@ void Text::writeCentered(int x, int y, const std::string& text, int kerning, int
}
// Escribe texto con extras
void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int lenght) { // NOLINT(readability-convert-member-functions-to-static)
void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerning, Uint8 text_color, Uint8 shadow_distance, Uint8 shadow_color, int lenght) {
const auto CENTERED = ((flags & CENTER_FLAG) == CENTER_FLAG);
const auto SHADOWED = ((flags & SHADOW_FLAG) == SHADOW_FLAG);
const auto COLORED = ((flags & COLOR_FLAG) == COLOR_FLAG);
@@ -260,12 +260,12 @@ void Text::writeDX(Uint8 flags, int x, int y, const std::string& text, int kerni
if (COLORED) {
writeColored(x, y, text, text_color, kerning, lenght);
} else {
writeColored(x, y, text, text_color, kerning, lenght);
write(x, y, text, kerning, lenght);
}
}
// Obtiene la longitud en pixels de una cadena UTF-8
auto Text::length(const std::string& text, int kerning) const -> int { // NOLINT(readability-convert-member-functions-to-static)
auto Text::length(const std::string& text, int kerning) const -> int {
int shift = 0;
size_t pos = 0;
@@ -282,7 +282,7 @@ auto Text::length(const std::string& text, int kerning) const -> int { // NOLIN
}
// Devuelve el ancho en pixels de un glifo dado su codepoint Unicode
auto Text::glyphWidth(uint32_t codepoint, int kerning) const -> int { // NOLINT(readability-convert-member-functions-to-static)
auto Text::glyphWidth(uint32_t codepoint, int kerning) const -> int {
auto it = offset_.find(codepoint);
if (it == offset_.end()) { it = offset_.find('?'); }
if (it != offset_.end()) { return it->second.w + kerning; }
+105 -156
View File
@@ -9,7 +9,7 @@
#include <stdexcept> // Para runtime_error
#include <utility>
#include "core/audio/jail_audio.hpp" // Para JA_DeleteMusic, JA_DeleteSound, JA_Loa...
#include "core/audio/jail_audio.hpp" // Para Ja::deleteMusic, Ja::deleteSound, JA_Loa...
#include "core/rendering/screen.hpp" // Para Screen
#include "core/rendering/text.hpp" // Para Text, loadTextFile
#include "core/resources/resource_helper.hpp" // Para Helper
@@ -21,8 +21,10 @@
#include "utils/defines.hpp" // Para WINDOW_CAPTION
#include "utils/utils.hpp" // Para getFileName, printWithDots, PaletteColor
#include "version.h" // Para Version::GIT_HASH
struct JA_Music_t; // lines 17-17
struct JA_Sound_t; // lines 18-18
namespace Ja {
struct Music;
struct Sound;
} // namespace Ja
namespace Resource {
@@ -85,148 +87,88 @@ namespace Resource {
return stage_ == LoadStage::DONE;
}
// Helper per a una etapa que itera una llista de recursos.
// Imprimeix la capçalera i neteja el vector al primer cop; després carrega
// un element per crida fins exhaurir la llista, moment en què passa a `next`.
void Cache::stepEachInList(List::Type type, const char* header, const std::function<void()>& clear_fn, LoadStage next, const std::function<void(size_t)>& load_fn) {
auto list = List::get()->getListByType(type);
if (stage_index_ == 0) {
std::cout << "\n>> " << header << '\n';
clear_fn();
}
if (stage_index_ >= list.size()) {
stage_ = next;
stage_index_ = 0;
return;
}
load_fn(stage_index_++);
}
// 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;
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); };
const Uint64 START_NS = SDL_GetTicksNS();
const Uint64 BUDGET_NS = static_cast<Uint64>(budget_ms) * 1'000'000ULL;
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_++);
case LoadStage::SOUNDS:
stepEachInList(List::Type::SOUND, "SOUND FILES", [this] { sounds_.clear(); }, LoadStage::MUSICS, [this](size_t i) { loadOneSound(i); });
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_++);
case LoadStage::MUSICS:
stepEachInList(List::Type::MUSIC, "MUSIC FILES", [this] { musics_.clear(); }, LoadStage::SURFACES, [this](size_t i) { loadOneMusic(i); });
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_++);
case LoadStage::SURFACES:
stepEachInList(List::Type::BITMAP, "SURFACES", [this] { surfaces_.clear(); }, LoadStage::SURFACES_POST, [this](size_t i) { loadOneSurface(i); });
break;
}
case LoadStage::SURFACES_POST: {
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_++);
case LoadStage::PALETTES:
stepEachInList(List::Type::PALETTE, "PALETTES", [this] { palettes_.clear(); }, LoadStage::TEXT_FILES, [this](size_t i) { loadOnePalette(i); });
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_++);
case LoadStage::TEXT_FILES:
stepEachInList(List::Type::FONT, "TEXT FILES", [this] { text_files_.clear(); }, LoadStage::ANIMATIONS, [this](size_t i) { loadOneTextFile(i); });
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_++);
case LoadStage::ANIMATIONS:
stepEachInList(List::Type::ANIMATION, "ANIMATIONS", [this] { animations_.clear(); }, LoadStage::ROOMS, [this](size_t i) { loadOneAnimation(i); });
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_++);
case LoadStage::ROOMS:
stepEachInList(List::Type::ROOM, "ROOMS", [this] { rooms_.clear(); }, LoadStage::TEXTS, [this](size_t i) { loadOneRoom(i); });
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_++);
case LoadStage::TEXTS:
stepTexts();
break;
}
case LoadStage::DONE:
break;
}
if ((SDL_GetTicksNS() - start_ns) >= budget_ns) break;
if ((SDL_GetTicksNS() - START_NS) >= BUDGET_NS) { break; }
}
return stage_ == LoadStage::DONE;
}
void Cache::stepTexts() {
// 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';
return;
}
createOneText(stage_index_++);
}
// Recarga todos los recursos (síncrono, solo para hot-reload de debug)
void Cache::reload() {
clear();
@@ -234,7 +176,7 @@ namespace Resource {
}
// Obtiene el sonido a partir de un nombre
auto Cache::getSound(const std::string& name) -> JA_Sound_t* { // NOLINT(readability-convert-member-functions-to-static)
auto Cache::getSound(const std::string& name) -> Ja::Sound* {
auto it = std::ranges::find_if(sounds_, [&name](const auto& s) -> bool { return s.name == name; });
if (it != sounds_.end()) {
@@ -246,7 +188,7 @@ namespace Resource {
}
// Obtiene la música a partir de un nombre
auto Cache::getMusic(const std::string& name) -> JA_Music_t* { // NOLINT(readability-convert-member-functions-to-static)
auto Cache::getMusic(const std::string& name) -> Ja::Music* {
auto it = std::ranges::find_if(musics_, [&name](const auto& m) -> bool { return m.name == name; });
if (it != musics_.end()) {
@@ -258,7 +200,7 @@ namespace Resource {
}
// Obtiene la surface a partir de un nombre
auto Cache::getSurface(const std::string& name) -> std::shared_ptr<Surface> { // NOLINT(readability-convert-member-functions-to-static)
auto Cache::getSurface(const std::string& name) -> std::shared_ptr<Surface> {
auto it = std::ranges::find_if(surfaces_, [&name](const auto& t) -> bool { return t.name == name; });
if (it != surfaces_.end()) {
@@ -270,7 +212,7 @@ namespace Resource {
}
// Obtiene la paleta a partir de un nombre
auto Cache::getPalette(const std::string& name) -> Palette { // NOLINT(readability-convert-member-functions-to-static)
auto Cache::getPalette(const std::string& name) -> Palette {
auto it = std::ranges::find_if(palettes_, [&name](const auto& t) -> bool { return t.name == name; });
if (it != palettes_.end()) {
@@ -282,7 +224,7 @@ namespace Resource {
}
// Obtiene el fichero de texto a partir de un nombre
auto Cache::getTextFile(const std::string& name) -> std::shared_ptr<Text::File> { // NOLINT(readability-convert-member-functions-to-static)
auto Cache::getTextFile(const std::string& name) -> std::shared_ptr<Text::File> {
auto it = std::ranges::find_if(text_files_, [&name](const auto& t) -> bool { return t.name == name; });
if (it != text_files_.end()) {
@@ -294,7 +236,7 @@ namespace Resource {
}
// Obtiene el objeto de texto a partir de un nombre
auto Cache::getText(const std::string& name) -> std::shared_ptr<Text> { // NOLINT(readability-convert-member-functions-to-static)
auto Cache::getText(const std::string& name) -> std::shared_ptr<Text> {
auto it = std::ranges::find_if(texts_, [&name](const auto& t) -> bool { return t.name == name; });
if (it != texts_.end()) {
@@ -306,7 +248,7 @@ namespace Resource {
}
// Obtiene los datos de animación parseados a partir de un nombre
auto Cache::getAnimationData(const std::string& name) -> const AnimationResource& { // NOLINT(readability-convert-member-functions-to-static)
auto Cache::getAnimationData(const std::string& name) -> const AnimationResource& {
auto it = std::ranges::find_if(animations_, [&name](const auto& a) -> bool { return a.name == name; });
if (it != animations_.end()) {
@@ -318,7 +260,7 @@ namespace Resource {
}
// Obtiene la habitación a partir de un nombre
auto Cache::getRoom(const std::string& name) -> std::shared_ptr<Room::Data> { // NOLINT(readability-convert-member-functions-to-static)
auto Cache::getRoom(const std::string& name) -> std::shared_ptr<Room::Data> {
auto it = std::ranges::find_if(rooms_, [&name](const auto& r) -> bool { return r.name == name; });
if (it != rooms_.end()) {
@@ -362,13 +304,13 @@ namespace Resource {
return rooms_;
}
// Helper para lanzar errores de carga con formato consistente
[[noreturn]] void Cache::throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e) { // NOLINT(readability-convert-member-functions-to-static)
// Helper para registrar errores de carga con formato consistente.
// El rethrow es responsabilitat del catch que crida la funció.
void Cache::logLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e) {
std::cerr << "\n[ ERROR ] Failed to load " << asset_type << ": " << getFileName(file_path) << '\n';
std::cerr << "[ ERROR ] Path: " << file_path << '\n';
std::cerr << "[ ERROR ] Reason: " << e.what() << '\n';
std::cerr << "[ ERROR ] Check config/assets.yaml configuration\n";
throw;
}
// Lista fija de text objects. Compartida entre createText() y createOneText(i).
@@ -379,14 +321,14 @@ namespace Resource {
std::string text_file; // Nombre del archivo de texto
};
const std::vector<TextObjectInfo>& getTextObjectInfos() {
static const std::vector<TextObjectInfo> info = {
auto getTextObjectInfos() -> const std::vector<TextObjectInfo>& {
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;
return INFO;
}
} // namespace
@@ -398,14 +340,14 @@ namespace Resource {
try {
auto name = getFileName(l);
setCurrentLoading(name);
JA_Sound_t* sound = nullptr;
Ja::Sound* sound = nullptr;
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
sound = JA_LoadSound(audio_data.data(), static_cast<Uint32>(audio_data.size()));
sound = Ja::loadSound(audio_data.data(), static_cast<Uint32>(audio_data.size()));
}
if (sound == nullptr) {
sound = JA_LoadSound(l.c_str());
sound = Ja::loadSound(l.c_str());
}
if (sound == nullptr) {
throw std::runtime_error("Failed to decode audio file");
@@ -415,7 +357,8 @@ namespace Resource {
printWithDots("Sound : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("SOUND", l, e);
logLoadError("SOUND", l, e);
throw;
}
}
@@ -425,14 +368,14 @@ namespace Resource {
try {
auto name = getFileName(l);
setCurrentLoading(name);
JA_Music_t* music = nullptr;
Ja::Music* music = nullptr;
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
music = JA_LoadMusic(audio_data.data(), static_cast<Uint32>(audio_data.size()));
music = Ja::loadMusic(audio_data.data(), static_cast<Uint32>(audio_data.size()));
}
if (music == nullptr) {
music = JA_LoadMusic(l.c_str());
music = Ja::loadMusic(l.c_str());
}
if (music == nullptr) {
throw std::runtime_error("Failed to decode music file");
@@ -442,7 +385,8 @@ namespace Resource {
printWithDots("Music : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("MUSIC", l, e);
logLoadError("MUSIC", l, e);
throw;
}
}
@@ -456,7 +400,8 @@ namespace Resource {
surfaces_.back().surface->setTransparentColor(0);
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("BITMAP", l, e);
logLoadError("BITMAP", l, e);
throw;
}
}
@@ -480,7 +425,8 @@ namespace Resource {
palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("PALETTE", l, e);
logLoadError("PALETTE", l, e);
throw;
}
}
@@ -493,7 +439,8 @@ namespace Resource {
text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("FONT", l, e);
logLoadError("FONT", l, e);
throw;
}
}
@@ -513,7 +460,8 @@ namespace Resource {
printWithDots("Animation : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("ANIMATION", l, e);
logLoadError("ANIMATION", l, e);
throw;
}
}
@@ -527,7 +475,8 @@ namespace Resource {
printWithDots("Room : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("ROOM", l, e);
logLoadError("ROOM", l, e);
throw;
}
}
@@ -546,21 +495,21 @@ namespace Resource {
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);
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);
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);
for (size_t i = 0; i < list.size(); ++i) { loadOneSurface(i); }
finalizeSurfaces();
}
@@ -568,43 +517,43 @@ namespace Resource {
std::cout << "\n>> PALETTES" << '\n';
auto list = List::get()->getListByType(List::Type::PALETTE);
palettes_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOnePalette(i);
for (size_t i = 0; i < list.size(); ++i) { loadOnePalette(i); }
}
void Cache::loadTextFiles() {
std::cout << "\n>> TEXT FILES" << '\n';
auto list = List::get()->getListByType(List::Type::FONT);
text_files_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOneTextFile(i);
for (size_t i = 0; i < list.size(); ++i) { loadOneTextFile(i); }
}
void Cache::loadAnimations() {
std::cout << "\n>> ANIMATIONS" << '\n';
auto list = List::get()->getListByType(List::Type::ANIMATION);
animations_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOneAnimation(i);
for (size_t i = 0; i < list.size(); ++i) { loadOneAnimation(i); }
}
void Cache::loadRooms() {
std::cout << "\n>> ROOMS" << '\n';
auto list = List::get()->getListByType(List::Type::ROOM);
rooms_.clear();
for (size_t i = 0; i < list.size(); ++i) loadOneRoom(i);
for (size_t i = 0; i < list.size(); ++i) { loadOneRoom(i); }
}
void Cache::createText() {
std::cout << "\n>> CREATING TEXT_OBJECTS" << '\n';
texts_.clear();
const auto& infos = getTextObjectInfos();
for (size_t i = 0; i < infos.size(); ++i) createOneText(i);
for (size_t i = 0; i < infos.size(); ++i) { createOneText(i); }
}
// Vacía el vector de sonidos
void Cache::clearSounds() {
// Itera sobre el vector y libera los recursos asociados a cada JA_Sound_t
// Itera sobre el vector y libera los recursos asociados a cada Ja::Sound
for (auto& sound : sounds_) {
if (sound.sound != nullptr) {
JA_DeleteSound(sound.sound);
Ja::deleteSound(sound.sound);
sound.sound = nullptr;
}
}
@@ -613,10 +562,10 @@ namespace Resource {
// Vacía el vector de musicas
void Cache::clearMusics() {
// Itera sobre el vector y libera los recursos asociados a cada JA_Music_t
// Itera sobre el vector y libera los recursos asociados a cada Ja::Music
for (auto& music : musics_) {
if (music.music != nullptr) {
JA_DeleteMusic(music.music);
Ja::deleteMusic(music.music);
music.music = nullptr;
}
}
+14 -6
View File
@@ -1,10 +1,13 @@
#pragma once
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <cstdint> // Para uint8_t
#include <functional> // Para std::function
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <utility>
#include <vector> // Para vector
#include "core/resources/resource_list.hpp" // Para List::Type
#include "core/resources/resource_types.hpp" // Para structs de recursos
namespace Resource {
@@ -15,8 +18,8 @@ namespace Resource {
static void destroy(); // Destrucción singleton
static auto get() -> Cache*; // Acceso al singleton
auto getSound(const std::string& name) -> JA_Sound_t*; // Getters de recursos
auto getMusic(const std::string& name) -> JA_Music_t*;
auto getSound(const std::string& name) -> Ja::Sound*; // Getters de recursos
auto getMusic(const std::string& name) -> Ja::Music*;
auto getSurface(const std::string& name) -> std::shared_ptr<Surface>;
auto getPalette(const std::string& name) -> Palette;
auto getTextFile(const std::string& name) -> std::shared_ptr<Text::File>;
@@ -54,7 +57,7 @@ namespace Resource {
};
// Etapas del loader incremental
enum class LoadStage {
enum class LoadStage : std::uint8_t {
SOUNDS,
MUSICS,
SURFACES,
@@ -100,7 +103,12 @@ namespace Resource {
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);
static void logLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e);
// Helper d'iteració per a una etapa que recorre una llista de recursos.
// Crida `load_fn(i)` per a cada element i, en exhaurir-se, transiciona a `next`.
void stepEachInList(List::Type type, const char* header, const std::function<void()>& clear_fn, LoadStage next, const std::function<void(size_t)>& load_fn);
void stepTexts(); // Etapa especial: no usa List, itera sobre TEXT_COUNT fonts fixes.
// Constructor y destructor
Cache();
+83 -108
View File
@@ -16,14 +16,79 @@
namespace Resource {
namespace {
// Un item del format modern: pot ser string (path) o mapping ({path, required?, absolute?})
void parseAssetItem(List& list, const fkyaml::node& item, List::Type type, const std::string& prefix, const std::string& system_folder, const std::string& category, const std::string& type_str) {
try {
if (item.is_string()) {
auto path = List::replaceVariables(item.get_value<std::string>(), prefix, system_folder);
list.add(path, type, true, false);
return;
}
if (item.is_mapping() && item.contains("path")) {
auto path = List::replaceVariables(item["path"].get_value<std::string>(), prefix, system_folder);
const bool REQUIRED = !item.contains("required") || item["required"].get_value<bool>();
const bool ABSOLUTE = item.contains("absolute") && item["absolute"].get_value<bool>();
list.add(path, type, REQUIRED, ABSOLUTE);
return;
}
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: Invalid item in type '%s', category '%s', skipping", type_str.c_str(), category.c_str());
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error parsing asset in category '%s', type '%s': %s", category.c_str(), type_str.c_str(), e.what());
}
}
// (TIPO: [items...]) del format modern. Itera els items i delega a parseAssetItem.
void parseModernType(List& list, const fkyaml::node& items_node, List::Type type, const std::string& type_str, const std::string& category, const std::string& prefix, const std::string& system_folder) {
if (!items_node.is_sequence()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: Type '%s' in category '%s' is not a sequence, skipping", type_str.c_str(), category.c_str());
return;
}
for (const auto& item : items_node) {
parseAssetItem(list, item, type, prefix, system_folder, category, type_str);
}
}
// {type, path, required?, absolute?} del format antic
void parseLegacyAsset(List& list, const fkyaml::node& asset, const std::string& category, const std::string& prefix, const std::string& system_folder) {
try {
if (!asset.contains("type") || !asset.contains("path")) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: Asset in category '%s' missing 'type' or 'path', skipping", category.c_str());
return;
}
auto type_str = asset["type"].get_value<std::string>();
auto path = asset["path"].get_value<std::string>();
const bool REQUIRED = !asset.contains("required") || asset["required"].get_value<bool>();
const bool ABSOLUTE = asset.contains("absolute") && asset["absolute"].get_value<bool>();
path = List::replaceVariables(path, prefix, system_folder);
list.add(path, List::parseAssetType(type_str), REQUIRED, ABSOLUTE);
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error parsing asset in category '%s': %s", category.c_str(), e.what());
}
}
// Categoria amb format modern (TIPO → [items]). Itera els tipus.
void parseModernCategory(List& list, const fkyaml::node& category_assets, const std::string& category, const std::string& prefix, const std::string& system_folder) {
for (auto type_it = category_assets.begin(); type_it != category_assets.end(); ++type_it) {
try {
auto type_str = type_it.key().get_value<std::string>();
const List::Type TYPE = List::parseAssetType(type_str);
parseModernType(list, type_it.value(), TYPE, type_str, category, prefix, system_folder);
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error parsing type in category '%s': %s", category.c_str(), e.what());
}
}
}
} // namespace
// Singleton
List* List::instance = nullptr;
void List::init(const std::string& executable_path) { // NOLINT(readability-convert-member-functions-to-static)
void List::init(const std::string& executable_path) {
List::instance = new List(executable_path);
}
void List::destroy() { // NOLINT(readability-convert-member-functions-to-static)
void List::destroy() {
delete List::instance;
}
@@ -32,7 +97,7 @@ namespace Resource {
}
// Añade un elemento al mapa (función auxiliar)
void List::addToMap(const std::string& file_path, Type type, bool required, bool absolute) { // NOLINT(readability-convert-member-functions-to-static)
void List::addToMap(const std::string& file_path, Type type, bool required, bool absolute) {
std::string full_path = absolute ? file_path : executable_path_ + file_path;
std::string filename = getFileName(full_path);
@@ -75,11 +140,11 @@ namespace Resource {
}
// Buscar la última entrada con el mismo prefijo de ruta e insertar después
std::string entry = " - " + var_path + "\n";
auto last_pos = content.rfind(var_path.substr(0, var_path.rfind('/')));
if (last_pos != std::string::npos) {
auto end_of_line = content.find('\n', last_pos);
if (end_of_line != std::string::npos) {
std::string entry = " - " + var_path + "\n";
content.insert(end_of_line + 1, entry);
}
}
@@ -138,7 +203,7 @@ namespace Resource {
}
// Carga recursos desde un archivo de configuración con soporte para variables
void List::loadFromFile(const std::string& config_file_path, const std::string& prefix, const std::string& system_folder) { // NOLINT(readability-convert-member-functions-to-static)
void List::loadFromFile(const std::string& config_file_path, const std::string& prefix, const std::string& system_folder) {
config_file_path_ = config_file_path;
prefix_ = prefix;
@@ -160,17 +225,13 @@ namespace Resource {
}
// Carga recursos desde un string de configuración (para release con pack)
void List::loadFromString(const std::string& config_content, const std::string& prefix, const std::string& system_folder) { // NOLINT(readability-convert-member-functions-to-static,readability-function-cognitive-complexity)
void List::loadFromString(const std::string& config_content, const std::string& prefix, const std::string& system_folder) {
try {
// Parsear YAML
auto yaml = fkyaml::node::deserialize(config_content);
// Verificar estructura básica
if (!yaml.contains("assets")) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error: Invalid assets.yaml format - missing 'assets' key");
return;
}
const auto& assets = yaml["assets"];
// Iterar sobre cada categoría (fonts, palettes, etc.)
@@ -179,110 +240,24 @@ namespace Resource {
const auto& category_assets = it.value();
if (category_assets.is_mapping()) {
// Nuevo formato: categoría → { TIPO: [paths...], TIPO2: [paths...] }
for (auto type_it = category_assets.begin(); type_it != category_assets.end(); ++type_it) {
try {
auto type_str = type_it.key().get_value<std::string>();
Type type = parseAssetType(type_str);
const auto& items = type_it.value();
if (!items.is_sequence()) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Warning: Type '%s' in category '%s' is not a sequence, skipping",
type_str.c_str(),
category.c_str());
continue;
}
for (const auto& item : items) {
try {
if (item.is_string()) {
// Formato simple: solo el path
auto path = replaceVariables(item.get_value<std::string>(), prefix, system_folder);
addToMap(path, type, true, false);
} else if (item.is_mapping() && item.contains("path")) {
// Formato expandido: { path, required?, absolute? }
auto path = replaceVariables(item["path"].get_value<std::string>(), prefix, system_folder);
bool required = !item.contains("required") || item["required"].get_value<bool>();
bool absolute = item.contains("absolute") && item["absolute"].get_value<bool>();
addToMap(path, type, required, absolute);
} else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Warning: Invalid item in type '%s', category '%s', skipping",
type_str.c_str(),
category.c_str());
}
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error parsing asset in category '%s', type '%s': %s",
category.c_str(),
type_str.c_str(),
e.what());
}
}
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error parsing type in category '%s': %s",
category.c_str(),
e.what());
}
}
parseModernCategory(*this, category_assets, category, prefix, system_folder);
} else if (category_assets.is_sequence()) {
// Formato antiguo (retrocompatibilidad): categoría → [{type, path}, ...]
for (const auto& asset : category_assets) {
try {
if (!asset.contains("type") || !asset.contains("path")) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Warning: Asset in category '%s' missing 'type' or 'path', skipping",
category.c_str());
continue;
}
auto type_str = asset["type"].get_value<std::string>();
auto path = asset["path"].get_value<std::string>();
bool required = true;
bool absolute = false;
if (asset.contains("required")) {
required = asset["required"].get_value<bool>();
}
if (asset.contains("absolute")) {
absolute = asset["absolute"].get_value<bool>();
}
path = replaceVariables(path, prefix, system_folder);
Type type = parseAssetType(type_str);
addToMap(path, type, required, absolute);
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error parsing asset in category '%s': %s",
category.c_str(),
e.what());
}
}
for (const auto& asset : category_assets) { parseLegacyAsset(*this, asset, category, prefix, system_folder); }
} else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Warning: Category '%s' has invalid format, skipping",
category.c_str());
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Warning: Category '%s' has invalid format, skipping", category.c_str());
}
}
std::cout << "Loaded " << file_list_.size() << " assets from YAML config" << '\n';
} catch (const fkyaml::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"YAML parsing error: %s",
e.what());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "YAML parsing error: %s", e.what());
} catch (const std::exception& e) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
"Error loading assets: %s",
e.what());
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Error loading assets: %s", e.what());
}
}
// Devuelve la ruta completa a un fichero (búsqueda O(1))
auto List::get(const std::string& filename) const -> std::string { // NOLINT(readability-convert-member-functions-to-static)
auto List::get(const std::string& filename) const -> std::string {
auto it = file_list_.find(filename);
if (it != file_list_.end()) {
return it->second.file;
@@ -293,7 +268,7 @@ namespace Resource {
}
// Carga datos del archivo
auto List::loadData(const std::string& filename) const -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
auto List::loadData(const std::string& filename) const -> std::vector<uint8_t> {
auto it = file_list_.find(filename);
if (it != file_list_.end()) {
std::ifstream file(it->second.file, std::ios::binary);
@@ -327,7 +302,7 @@ namespace Resource {
}
// Parsea string a Type
auto List::parseAssetType(const std::string& type_str) -> Type { // NOLINT(readability-convert-member-functions-to-static)
auto List::parseAssetType(const std::string& type_str) -> Type {
if (type_str == "DATA") {
return Type::DATA;
}
@@ -361,7 +336,7 @@ namespace Resource {
}
// Devuelve el nombre del tipo de recurso
auto List::getTypeName(Type type) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
auto List::getTypeName(Type type) -> std::string {
switch (type) {
case Type::DATA:
return "DATA";
@@ -385,7 +360,7 @@ namespace Resource {
}
// Devuelve la lista de recursos de un tipo
auto List::getListByType(Type type) const -> std::vector<std::string> { // NOLINT(readability-convert-member-functions-to-static)
auto List::getListByType(Type type) const -> std::vector<std::string> {
std::vector<std::string> list;
for (const auto& [filename, item] : file_list_) {
@@ -401,7 +376,7 @@ namespace Resource {
}
// Reemplaza variables en las rutas
auto List::replaceVariables(const std::string& path, const std::string& prefix, const std::string& system_folder) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
auto List::replaceVariables(const std::string& path, const std::string& prefix, const std::string& system_folder) -> std::string {
std::string result = path;
// Reemplazar ${PREFIX}
@@ -422,7 +397,7 @@ namespace Resource {
}
// Parsea las opciones de una línea de configuración
auto List::parseOptions(const std::string& options, bool& required, bool& absolute) -> void { // NOLINT(readability-convert-member-functions-to-static)
auto List::parseOptions(const std::string& options, bool& required, bool& absolute) -> void {
if (options.empty()) {
return;
}
+8 -6
View File
@@ -12,7 +12,7 @@ namespace Resource {
class List {
public:
// --- Enums ---
enum class Type : int {
enum class Type : std::uint8_t {
DATA, // Datos
BITMAP, // Imágenes
ANIMATION, // Animaciones
@@ -42,6 +42,10 @@ namespace Resource {
[[nodiscard]] auto getListByType(Type type) const -> std::vector<std::string>;
[[nodiscard]] auto exists(const std::string& filename) const -> bool; // Verifica si un asset existe
// --- Helpers static (públics perquè els fan servir parsers externs del .cpp) ---
[[nodiscard]] static auto parseAssetType(const std::string& type_str) -> Type; // Convierte string a tipo
[[nodiscard]] static auto replaceVariables(const std::string& path, const std::string& prefix, const std::string& system_folder) -> std::string; // Reemplaza variables en la ruta
private:
// --- Estructuras privadas ---
struct Item {
@@ -62,11 +66,9 @@ namespace Resource {
std::string prefix_; // Prefijo para rutas (${PREFIX})
// --- Métodos internos ---
[[nodiscard]] static auto getTypeName(Type type) -> std::string; // Obtiene el nombre del tipo
[[nodiscard]] static auto parseAssetType(const std::string& type_str) -> Type; // Convierte string a tipo
void addToMap(const std::string& file_path, Type type, bool required, bool absolute); // Añade archivo al mapa
[[nodiscard]] static auto replaceVariables(const std::string& path, const std::string& prefix, const std::string& system_folder) -> std::string; // Reemplaza variables en la ruta
static auto parseOptions(const std::string& options, bool& required, bool& absolute) -> void; // Parsea opciones
[[nodiscard]] static auto getTypeName(Type type) -> std::string; // Obtiene el nombre del tipo
void addToMap(const std::string& file_path, Type type, bool required, bool absolute); // Añade archivo al mapa
static auto parseOptions(const std::string& options, bool& required, bool& absolute) -> void; // Parsea opciones
// --- Constructores y destructor privados (singleton) ---
explicit List(std::string executable_path) // Constructor privado
+7 -7
View File
@@ -53,7 +53,7 @@ namespace Resource {
}
// Load a resource
auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> { // NOLINT(readability-make-member-function-const)
auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
if (!initialized_) {
std::cerr << "Loader: Not initialized\n";
return {};
@@ -81,7 +81,7 @@ namespace Resource {
}
// Check if a resource exists
auto Loader::resourceExists(const std::string& filename) -> bool { // NOLINT(readability-make-member-function-const)
auto Loader::resourceExists(const std::string& filename) -> bool {
if (!initialized_) {
return false;
}
@@ -107,7 +107,7 @@ namespace Resource {
}
// Get pack statistics
auto Loader::getPackResourceCount() const -> size_t { // NOLINT(readability-convert-member-functions-to-static)
auto Loader::getPackResourceCount() const -> size_t {
if (resource_pack_ && resource_pack_->isLoaded()) {
return resource_pack_->getResourceCount();
}
@@ -122,7 +122,7 @@ namespace Resource {
}
// Load from filesystem
auto Loader::loadFromFilesystem(const std::string& filepath) // NOLINT(readability-convert-member-functions-to-static)
auto Loader::loadFromFilesystem(const std::string& filepath)
-> std::vector<uint8_t> {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
@@ -147,7 +147,7 @@ namespace Resource {
}
// Validate pack integrity
auto Loader::validatePack() const -> bool { // NOLINT(readability-convert-member-functions-to-static)
auto Loader::validatePack() const -> bool {
if (!initialized_ || !resource_pack_ || !resource_pack_->isLoaded()) {
std::cerr << "Loader: Cannot validate - pack not loaded\n";
return false;
@@ -158,7 +158,7 @@ namespace Resource {
if (checksum == 0) {
std::cerr << "Loader: Pack checksum is zero (invalid)\n";
return false; // NOLINT(readability-simplify-boolean-expr)
return false;
}
std::cout << "Loader: Pack checksum: 0x" << std::hex << checksum << std::dec
@@ -168,7 +168,7 @@ namespace Resource {
}
// Load assets.yaml from pack
auto Loader::loadAssetsConfig() const -> std::string { // NOLINT(readability-convert-member-functions-to-static)
auto Loader::loadAssetsConfig() const -> std::string {
if (!initialized_ || !resource_pack_ || !resource_pack_->isLoaded()) {
std::cerr << "Loader: Cannot load assets config - pack not loaded\n";
return "";
+15 -23
View File
@@ -9,20 +9,19 @@
#include <filesystem>
#include <fstream>
#include <iostream>
#include <numeric>
namespace Resource {
// Calculate CRC32 checksum for data verification
auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t { // NOLINT(readability-convert-member-functions-to-static)
uint32_t checksum = 0x12345678;
for (unsigned char byte : data) {
checksum = ((checksum << 5) + checksum) + byte;
}
return checksum;
auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
return std::accumulate(data.begin(), data.end(), uint32_t{0x12345678}, [](uint32_t acc, unsigned char byte) -> uint32_t {
return ((acc << 5) + acc) + byte;
});
}
// XOR encryption (symmetric - same function for encrypt/decrypt)
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) { // NOLINT(readability-identifier-naming)
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
if (key.empty()) {
return;
}
@@ -31,13 +30,13 @@ namespace Resource {
}
}
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) { // NOLINT(readability-identifier-naming)
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
// XOR is symmetric
encryptData(data, key);
}
// Read entire file into memory
auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> {
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) {
std::cerr << "ResourcePack: Failed to open file: " << filepath << '\n';
@@ -57,7 +56,7 @@ namespace Resource {
}
// Add a single file to the pack
auto Pack::addFile(const std::string& filepath, const std::string& pack_name) // NOLINT(readability-convert-member-functions-to-static)
auto Pack::addFile(const std::string& filepath, const std::string& pack_name)
-> bool {
auto file_data = readFile(filepath);
if (file_data.empty()) {
@@ -74,15 +73,13 @@ namespace Resource {
data_.insert(data_.end(), file_data.begin(), file_data.end());
resources_[pack_name] = entry;
std::cout << "Added: " << pack_name << " (" << file_data.size() << " bytes)\n";
return true;
}
// Add all files from a directory recursively
auto Pack::addDirectory(const std::string& dir_path, // NOLINT(readability-convert-member-functions-to-static)
auto Pack::addDirectory(const std::string& dir_path,
const std::string& base_path) -> bool {
namespace fs = std::filesystem; // NOLINT(readability-identifier-naming)
namespace fs = std::filesystem;
if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
std::cerr << "ResourcePack: Directory not found: " << dir_path << '\n';
@@ -105,7 +102,6 @@ namespace Resource {
// Skip development files
if (relative_path.find(".world") != std::string::npos ||
relative_path.find(".tsx") != std::string::npos) {
std::cout << "Skipping development file: " << relative_path << '\n';
continue;
}
@@ -117,7 +113,7 @@ namespace Resource {
}
// Save the pack to a file
auto Pack::savePack(const std::string& pack_file) -> bool { // NOLINT(readability-convert-member-functions-to-static)
auto Pack::savePack(const std::string& pack_file) -> bool {
std::ofstream file(pack_file, std::ios::binary);
if (!file) {
std::cerr << "ResourcePack: Failed to create pack file: " << pack_file << '\n';
@@ -154,10 +150,6 @@ namespace Resource {
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
file.write(reinterpret_cast<const char*>(encrypted_data.data()), data_size);
std::cout << "\nPack saved successfully: " << pack_file << '\n';
std::cout << "Resources: " << resource_count << '\n';
std::cout << "Total size: " << data_size << " bytes\n";
return true;
}
@@ -229,7 +221,7 @@ namespace Resource {
}
// Get a resource by name
auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> { // NOLINT(readability-convert-member-functions-to-static)
auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> {
auto it = resources_.find(filename);
if (it == resources_.end()) {
return {};
@@ -263,7 +255,7 @@ namespace Resource {
}
// Get list of all resources
auto Pack::getResourceList() const -> std::vector<std::string> { // NOLINT(readability-convert-member-functions-to-static)
auto Pack::getResourceList() const -> std::vector<std::string> {
std::vector<std::string> list;
list.reserve(resources_.size());
for (const auto& [name, entry] : resources_) {
@@ -274,7 +266,7 @@ namespace Resource {
}
// Calculate overall pack checksum for validation
auto Pack::calculatePackChecksum() const -> uint32_t { // NOLINT(readability-convert-member-functions-to-static)
auto Pack::calculatePackChecksum() const -> uint32_t {
if (!loaded_ || data_.empty()) {
return 0;
}
+8 -6
View File
@@ -10,19 +10,21 @@
#include "game/gameplay/room.hpp" // Para Room::Data
// Forward declarations
struct JA_Music_t;
struct JA_Sound_t;
namespace Ja {
struct Music;
struct Sound;
} // namespace Ja
// Estructura para almacenar ficheros de sonido y su nombre
struct SoundResource {
std::string name; // Nombre del sonido
JA_Sound_t* sound{nullptr}; // Objeto con el sonido
std::string name; // Nombre del sonido
Ja::Sound* sound{nullptr}; // Objeto con el sonido
};
// Estructura para almacenar ficheros musicales y su nombre
struct MusicResource {
std::string name; // Nombre de la musica
JA_Music_t* music{nullptr}; // Objeto con la música
std::string name; // Nombre de la musica
Ja::Music* music{nullptr}; // Objeto con la música
};
// Estructura para almacenar objetos Surface y su nombre
+1 -1
View File
@@ -32,7 +32,7 @@ auto Debug::get() -> Debug* {
}
// Dibuja en pantalla
void Debug::render() { // NOLINT(readability-make-member-function-const)
void Debug::render() {
auto text = Resource::Cache::get()->getText("aseprite");
int y = y_;
int w = 0;
+26 -22
View File
@@ -45,21 +45,25 @@
#include <pwd.h>
#endif
// Constructor
Director::Director() {
std::cout << "Game start" << '\n';
namespace {
auto getExecutablePath() -> std::string {
#ifdef __EMSCRIPTEN__
// En Emscripten els assets estan al root del filesystem virtual (/data, /config)
executable_path_ = "";
// En Emscripten els assets estan al root del filesystem virtual (/data, /config)
return "";
#else
// Obtiene la ruta del ejecutable
std::string base = SDL_GetBasePath();
if (!base.empty() && base.back() == '/') {
base.pop_back();
}
executable_path_ = base;
std::string base = SDL_GetBasePath();
if (!base.empty() && base.back() == '/') {
base.pop_back();
}
return base;
#endif
}
} // namespace
// Constructor
Director::Director()
: executable_path_(getExecutablePath()) {
std::cout << "Game start" << '\n';
// Crea la carpeta del sistema donde guardar datos
createSystemFolder("jailgames");
@@ -68,7 +72,7 @@ Director::Director() {
// Crea el subdirectorio shaders/ dentro de system_folder_ sin modificar system_folder_
{
std::string shaders_dir = system_folder_ + "/shaders";
struct stat st = {.st_dev = 0};
struct stat st{};
if (stat(shaders_dir.c_str(), &st) == -1) {
errno = 0;
#ifdef _WIN32
@@ -144,7 +148,7 @@ Director::Director() {
#endif
// Configura la ruta y carga las opciones desde un fichero
Options::setConfigFile(Resource::List::get()->get("config.yaml")); // NOLINT(readability-static-accessed-through-instance)
Options::setConfigFile(Resource::List::get()->get("config.yaml"));
Options::loadFromFile();
#ifdef __EMSCRIPTEN__
@@ -160,11 +164,11 @@ Director::Director() {
#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::setPostFXFile(Resource::List::get()->get("postfx.yaml"));
Options::loadPostFXFromFile();
// Configura la ruta y carga los presets del shader CrtPi
Options::setCrtPiFile(Resource::List::get()->get("crtpi.yaml")); // NOLINT(readability-static-accessed-through-instance)
Options::setCrtPiFile(Resource::List::get()->get("crtpi.yaml"));
Options::loadCrtPiFromFile();
// En mode quiosc, forçar pantalla completa independentment de la configuració
@@ -192,7 +196,7 @@ Director::Director() {
Input::init(gamecontroller_db);
#else
// In development, use Asset as normal
Input::init(Resource::List::get()->get("gamecontrollerdb.txt")); // NOLINT(readability-static-accessed-through-instance) Carga configuración de controles
Input::init(Resource::List::get()->get("gamecontrollerdb.txt")); // Carga configuración de controles
#endif
// Aplica las teclas y botones del gamepad configurados desde Options
@@ -236,13 +240,13 @@ void Director::finishBoot() {
#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)
std::string locale_key = Resource::List::get()->get(Options::language + ".yaml");
auto locale_bytes = Resource::Helper::loadFile(locale_key);
std::string locale_content(locale_bytes.begin(), locale_bytes.end());
Locale::initFromContent(locale_content);
}
#else
Locale::init(Resource::List::get()->get(Options::language + ".yaml")); // NOLINT(readability-static-accessed-through-instance)
Locale::init(Resource::List::get()->get(Options::language + ".yaml"));
#endif
// Special handling for cheevos.bin - also needs filesystem path
@@ -286,7 +290,7 @@ Director::~Director() {
}
// Crea la carpeta del sistema donde guardar datos
void Director::createSystemFolder(const std::string& folder) { // NOLINT(readability-convert-member-functions-to-static)
void Director::createSystemFolder(const std::string& folder) {
#ifdef __EMSCRIPTEN__
// En Emscripten utilitzem MEMFS (no persistent entre sessions).
// No cal crear directoris: MEMFS els crea automàticament en escriure-hi.
@@ -315,7 +319,7 @@ void Director::createSystemFolder(const std::string& folder) { // NOLINT(readab
}
#endif
struct stat st = {.st_dev = 0};
struct stat st{};
if (stat(system_folder_.c_str(), &st) == -1) {
errno = 0;
#ifdef _WIN32
@@ -348,7 +352,7 @@ void Director::createSystemFolder(const std::string& folder) { // NOLINT(readab
}
// Carga la configuración de assets desde assets.yaml
void Director::setFileList() { // NOLINT(readability-convert-member-functions-to-static)
void Director::setFileList() {
// Determinar el prefijo de ruta según la plataforma
#ifdef MACOS_BUNDLE
const std::string PREFIX = "/../Resources";
+4 -4
View File
@@ -13,7 +13,7 @@ namespace GlobalEvents {
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;
bool gamepad_button_pressed = false;
} // namespace
// Comprueba los eventos que se pueden producir en cualquier sección del juego.
@@ -56,7 +56,7 @@ namespace GlobalEvents {
const bool RESERVE_BACK = IS_BACK;
#endif
if (!RESERVE_BACK && !IS_SHOULDER) {
gamepad_button_pressed_ = true;
gamepad_button_pressed = true;
}
}
@@ -72,8 +72,8 @@ namespace GlobalEvents {
}
auto consumeGamepadButtonPressed() -> bool {
const bool RESULT = gamepad_button_pressed_;
gamepad_button_pressed_ = false;
const bool RESULT = gamepad_button_pressed;
gamepad_button_pressed = false;
return RESULT;
}
} // namespace GlobalEvents
-3
View File
@@ -26,13 +26,10 @@ namespace Defaults::Video {
constexpr Screen::Filter FILTER = Screen::Filter::NEAREST; // Filtro por defecto
constexpr bool VERTICAL_SYNC = true; // Vsync activado por defecto
constexpr bool SHADER_ENABLED = false; // Shaders de post-procesado desactivados por defecto
constexpr bool SUPERSAMPLING = false; // Supersampling desactivado por defecto
constexpr bool INTEGER_SCALE = true; // Escalado entero activado por defecto
constexpr bool KEEP_ASPECT = true; // Mantener aspecto activado por defecto
constexpr const char* PALETTE_NAME = "zx-spectrum"; // Paleta por defecto
constexpr const char* PALETTE_SORT = "original"; // Modo de ordenación de paleta por defecto
constexpr bool LINEAR_UPSCALE = false; // Upscale NEAREST por defecto
constexpr int DOWNSCALE_ALGO = 1; // Downscale Lanczos2 por defecto
constexpr bool GPU_ACCELERATION = true; // Aceleración GPU activada por defecto
} // namespace Defaults::Video
+3 -6
View File
@@ -15,13 +15,10 @@
// Constructor
EditorStatusBar::EditorStatusBar(std::string room_number, std::string room_name)
: room_number_(std::move(room_number)),
: surface_(std::make_shared<Surface>(Options::game.width, 6.0F * Tile::SIZE)),
surface_dest_{.x = 0, .y = Options::game.height - (6.0F * Tile::SIZE), .w = Options::game.width, .h = 6.0F * Tile::SIZE},
room_number_(std::move(room_number)),
room_name_(std::move(room_name)) {
const float SURFACE_WIDTH = Options::game.width;
constexpr float SURFACE_HEIGHT = 6.0F * Tile::SIZE; // 48 pixels, igual que el scoreboard
surface_ = std::make_shared<Surface>(SURFACE_WIDTH, SURFACE_HEIGHT);
surface_dest_ = {.x = 0, .y = Options::game.height - SURFACE_HEIGHT, .w = SURFACE_WIDTH, .h = SURFACE_HEIGHT};
}
// Pinta la barra de estado en pantalla
+15 -15
View File
@@ -383,7 +383,7 @@ void MapEditor::render() {
}
// Maneja eventos del editor
void MapEditor::handleEvent(const SDL_Event& event) { // NOLINT(readability-function-cognitive-complexity)
void MapEditor::handleEvent(const SDL_Event& event) {
// Si el tile picker está abierto, los eventos van a él
if (tile_picker_.isOpen()) {
tile_picker_.handleEvent(event);
@@ -549,7 +549,7 @@ void MapEditor::handleMouseDown(float game_x, float game_y) {
}
// Procesa soltar el ratón: commit del drag
void MapEditor::handleMouseUp() { // NOLINT(readability-function-cognitive-complexity)
void MapEditor::handleMouseUp() {
if (drag_.target == DragTarget::NONE) { return; }
const int IDX = drag_.index;
@@ -832,7 +832,7 @@ void MapEditor::updateMousePosition() {
}
// Actualiza la información de la barra de estado
void MapEditor::updateStatusBarInfo() { // NOLINT(readability-function-cognitive-complexity)
void MapEditor::updateStatusBarInfo() {
if (!statusbar_) { return; }
statusbar_->setMouseTile(mouse_tile_x_, mouse_tile_y_);
@@ -876,7 +876,7 @@ void MapEditor::updateStatusBarInfo() { // NOLINT(readability-function-cognitiv
const auto& e = room_data_.enemies[selected_enemy_];
std::string anim = e.animation_path;
auto dot = anim.rfind('.');
if (dot != std::string::npos) { anim = anim.substr(0, dot); }
if (dot != std::string::npos) { anim.resize(dot); }
line2 = "enemy " + std::to_string(selected_enemy_) + ": " + anim + " " + e.color;
line3 = "vx:" + std::to_string(static_cast<int>(e.vx)) +
@@ -945,7 +945,7 @@ auto MapEditor::getSetCompletions() const -> std::vector<std::string> {
}
// Modifica una propiedad del enemigo seleccionado
auto MapEditor::setEnemyProperty(const std::string& property, const std::string& value) -> std::string { // NOLINT(readability-function-cognitive-complexity)
auto MapEditor::setEnemyProperty(const std::string& property, const std::string& value) -> std::string {
if (!active_) { return "Editor not active"; }
if (!hasSelectedEnemy()) { return "No enemy selected"; }
@@ -1125,7 +1125,7 @@ auto MapEditor::duplicateEnemy() -> std::string {
}
// Modifica una propiedad de la habitación
auto MapEditor::setRoomProperty(const std::string& property, const std::string& value) -> std::string { // NOLINT(readability-function-cognitive-complexity)
auto MapEditor::setRoomProperty(const std::string& property, const std::string& value) -> std::string {
if (!active_) { return "Editor not active"; }
std::string val = toLower(value);
@@ -1268,7 +1268,7 @@ auto MapEditor::setRoomProperty(const std::string& property, const std::string&
}
// Crea una nueva habitación
auto MapEditor::createNewRoom(const std::string& direction) -> std::string { // NOLINT(readability-function-cognitive-complexity)
auto MapEditor::createNewRoom(const std::string& direction) -> std::string {
if (!active_) { return "Editor not active"; }
// Validar dirección si se proporcionó
@@ -1278,7 +1278,7 @@ auto MapEditor::createNewRoom(const std::string& direction) -> std::string { //
// Comprobar que no hay ya una room en esa dirección
if (!direction.empty()) {
std::string* existing = nullptr;
const std::string* existing = nullptr;
if (direction == "UP") {
existing = &room_data_.upper_room;
} else if (direction == "DOWN") {
@@ -1294,7 +1294,7 @@ auto MapEditor::createNewRoom(const std::string& direction) -> std::string { //
}
// Encontrar el primer número libre (reutiliza huecos)
auto& rooms = Resource::Cache::get()->getRooms();
const auto& rooms = Resource::Cache::get()->getRooms();
std::set<int> used;
for (const auto& r : rooms) {
try {
@@ -1406,7 +1406,7 @@ auto MapEditor::createNewRoom(const std::string& direction) -> std::string { //
}
// Elimina la habitación actual
auto MapEditor::deleteRoom() -> std::string { // NOLINT(readability-function-cognitive-complexity)
auto MapEditor::deleteRoom() -> std::string {
if (!active_) { return "Editor not active"; }
std::string deleted_name = room_path_;
@@ -1425,11 +1425,11 @@ auto MapEditor::deleteRoom() -> std::string { // NOLINT(readability-function-co
if (target == "0") {
// Buscar la primera room que no sea esta
for (const auto& r : Resource::Cache::get()->getRooms()) {
if (r.name != deleted_name) {
target = r.name;
break;
}
const auto& rooms = Resource::Cache::get()->getRooms();
auto it = std::ranges::find_if(rooms,
[&deleted_name](const auto& r) { return r.name != deleted_name; });
if (it != rooms.end()) {
target = it->name;
}
}
if (target == "0") { return "Cannot delete: no other room to navigate to"; }
+1 -1
View File
@@ -65,7 +65,7 @@ class MapEditor {
void openTilePicker(const std::string& tileset_name, int current_tile);
private:
static MapEditor* instance_; // NOLINT(readability-identifier-naming) [SINGLETON] Objeto privado
static MapEditor* instance_;
MapEditor(); // Constructor
~MapEditor(); // Destructor
+1 -1
View File
@@ -79,7 +79,7 @@ void MiniMap::buildTileColorTable(const std::string& tileset_name) {
// Posiciona las rooms en un grid usando BFS desde las conexiones
void MiniMap::layoutRooms() {
auto& rooms = Resource::Cache::get()->getRooms();
const auto& rooms = Resource::Cache::get()->getRooms();
if (rooms.empty()) { return; }
// Mapa de nombre → Room::Data
+1 -1
View File
@@ -36,7 +36,7 @@ auto RoomSaver::conveyorBeltToString(int direction) -> std::string {
}
// Genera el YAML completo como texto con formato compacto
auto RoomSaver::buildYAML(const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string { // NOLINT(readability-function-cognitive-complexity)
auto RoomSaver::buildYAML(const fkyaml::node& original_yaml, const Room::Data& room_data) -> std::string {
std::ostringstream out;
// --- Cabecera: nombre como comentario ---
+5 -3
View File
@@ -26,7 +26,8 @@ Enemy::Enemy(const Data& enemy)
const int FLIP = (should_flip_ && enemy.vx < 0.0F) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
const int MIRROR = should_mirror_ ? SDL_FLIP_VERTICAL : SDL_FLIP_NONE;
sprite_->setFlip(static_cast<SDL_FlipMode>(FLIP | MIRROR)); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange) SDL flags are designed for bitwise OR
// NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange): SDL_FlipMode és un enum dissenyat com a bitmask (FLIP_NONE=0, FLIP_HORIZONTAL=1, FLIP_VERTICAL=2). El cast del OR és el patró d'ús previst per la API de SDL.
sprite_->setFlip(static_cast<SDL_FlipMode>(FLIP | MIRROR));
collider_ = getRect();
@@ -63,14 +64,15 @@ void Enemy::resetToInitialPosition(const Data& data) {
const int FLIP = (should_flip_ && data.vx < 0.0F) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
const int MIRROR = should_mirror_ ? SDL_FLIP_VERTICAL : SDL_FLIP_NONE;
sprite_->setFlip(static_cast<SDL_FlipMode>(FLIP | MIRROR)); // NOLINT(clang-analyzer-optin.core.EnumCastOutOfRange)
// NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange): SDL_FlipMode és un enum bitmask (vegeu nota al constructor); el cast del OR és el patró d'ús de SDL
sprite_->setFlip(static_cast<SDL_FlipMode>(FLIP | MIRROR));
collider_ = getRect();
}
#endif
// Comprueba si ha llegado al limite del recorrido para darse media vuelta
void Enemy::checkPath() { // NOLINT(readability-make-member-function-const)
void Enemy::checkPath() {
if (sprite_->getPosX() > x2_ || sprite_->getPosX() < x1_) {
// Recoloca
if (sprite_->getPosX() > x2_) {
+2 -2
View File
@@ -29,14 +29,14 @@ void Item::update(float delta_time) {
}
// Pinta el objeto en pantalla
void Item::render() const { // NOLINT(readability-convert-member-functions-to-static)
void Item::render() const {
// Calcula el índice de color basado en el tiempo acumulado
const int INDEX = static_cast<int>(time_accumulator_ / COLOR_CHANGE_INTERVAL) % static_cast<int>(color_.size());
sprite_->render(1, color_.at(INDEX));
}
// Obtiene su ubicación
auto Item::getPos() -> SDL_FPoint { // NOLINT(readability-convert-member-functions-to-static)
auto Item::getPos() -> SDL_FPoint {
const SDL_FPoint P = {.x = sprite_->getX(), .y = sprite_->getY()};
return P;
}
+7 -9
View File
@@ -463,7 +463,7 @@ void Player::applyGravity(float delta_time) {
}
// Establece la animación del jugador
void Player::animate(float delta_time) { // NOLINT(readability-make-member-function-const)
void Player::animate(float delta_time) {
if (vx_ != 0) {
sprite_->update(delta_time);
}
@@ -479,7 +479,7 @@ void Player::handleJumpEnd() {
}
// Calcula y reproduce el sonido de salto basado en tiempo transcurrido
void Player::playJumpSound(float delta_time) { // NOLINT(readability-convert-member-functions-to-static)
void Player::playJumpSound(float delta_time) {
size_t sound_index;
if (jump_sound_ctrl_.shouldPlay(delta_time, sound_index)) {
if (sound_index < jumping_sound_.size()) {
@@ -489,7 +489,7 @@ void Player::playJumpSound(float delta_time) { // NOLINT(readability-convert-me
}
// Calcula y reproduce el sonido de caída basado en distancia vertical recorrida
void Player::playFallSound(float delta_time) { // NOLINT(readability-convert-member-functions-to-static)
void Player::playFallSound(float delta_time) {
size_t sound_index;
if (fall_sound_ctrl_.shouldPlay(delta_time, y_, sound_index)) {
if (sound_index < falling_sound_.size()) {
@@ -666,7 +666,7 @@ void Player::updateFeet() {
}
// Inicializa los sonidos de salto y caida
void Player::initSounds() { // NOLINT(readability-convert-member-functions-to-static)
void Player::initSounds() {
for (int i = 0; i < 24; ++i) {
std::string sound_file = "jump" + std::to_string(i + 1) + ".wav";
jumping_sound_[i] = Resource::Cache::get()->getSound(sound_file);
@@ -708,7 +708,7 @@ auto Player::JumpSoundController::shouldPlay(float delta_time, size_t& out_index
if (target_index > current_index) {
current_index = target_index;
out_index = current_index;
return true; // NOLINT(readability-simplify-boolean-expr)
return true;
}
return false;
@@ -787,7 +787,7 @@ void Player::setSkin(const std::string& skin_name) {
}
// Inicializa el sprite del jugador
void Player::initSprite(const std::string& animations_path) { // NOLINT(readability-convert-member-functions-to-static)
void Player::initSprite(const std::string& animations_path) {
const auto& animation_data = Resource::Cache::get()->getAnimationData(animations_path);
sprite_ = std::make_unique<AnimatedSprite>(animation_data);
sprite_->setWidth(WIDTH);
@@ -836,8 +836,6 @@ void Player::updateVelocity() {
sprite_->setFlip(Flip::RIGHT);
break;
case Direction::NONE:
vx_ = 0.0F;
break;
default:
vx_ = 0.0F;
break;
@@ -907,7 +905,7 @@ void Player::resetSoundControllersOnLanding() {
}
// Devuelve el rectangulo de proyeccion
auto Player::getProjection(Direction direction, float displacement) -> SDL_FRect { // NOLINT(readability-convert-member-functions-to-static)
auto Player::getProjection(Direction direction, float displacement) -> SDL_FRect {
switch (direction) {
case Direction::LEFT:
return {
+16 -13
View File
@@ -2,10 +2,11 @@
#include <SDL3/SDL.h>
#include <array> // Para array
#include <limits> // Para numeric_limits
#include <memory> // Para shared_ptr, __shared_ptr_access
#include <string> // Para string
#include <array> // Para array
#include <cstdint> // Para uint8_t
#include <limits> // Para numeric_limits
#include <memory> // Para shared_ptr, __shared_ptr_access
#include <string> // Para string
#include <utility>
#include "core/rendering/sprite/animated_sprite.hpp" // Para SAnimatedSprite
@@ -13,19 +14,21 @@
#include "game/options.hpp" // Para Cheat, Options, options
#include "utils/defines.hpp" // Para BORDER_TOP, BLOCK
#include "utils/utils.hpp" // Para Color
struct JA_Sound_t; // lines 13-13
namespace Ja {
struct Sound;
}
class Player {
public:
// --- Enums y Structs ---
enum class State {
enum class State : std::uint8_t {
ON_GROUND, // En suelo plano o conveyor belt
ON_SLOPE, // En rampa/pendiente
JUMPING,
FALLING,
};
enum class Direction {
enum class Direction : std::uint8_t {
LEFT,
RIGHT,
UP,
@@ -155,12 +158,12 @@ class Player {
int last_grounded_position_ = 0; // Ultima posición en Y en la que se estaba en contacto con el suelo (hace doble función: tracking de caída + altura inicial del salto)
// --- Variables de renderizado y sonido ---
Uint8 color_ = 0; // Color del jugador
std::array<JA_Sound_t*, 24> jumping_sound_{}; // Array con todos los sonidos del salto
std::array<JA_Sound_t*, 14> falling_sound_{}; // Array con todos los sonidos de la caída
JumpSoundController jump_sound_ctrl_; // Controlador de sonidos de salto
FallSoundController fall_sound_ctrl_; // Controlador de sonidos de caída
int fall_start_position_ = 0; // Posición Y al iniciar la caída
Uint8 color_ = 0; // Color del jugador
std::array<Ja::Sound*, 24> jumping_sound_{}; // Array con todos los sonidos del salto
std::array<Ja::Sound*, 14> falling_sound_{}; // Array con todos los sonidos de la caída
JumpSoundController jump_sound_ctrl_; // Controlador de sonidos de salto
FallSoundController fall_sound_ctrl_; // Controlador de sonidos de caída
int fall_start_position_ = 0; // Posición Y al iniciar la caída
void handleConveyorBelts();
void handleShouldFall();
+5 -5
View File
@@ -16,7 +16,7 @@
Cheevos* Cheevos::cheevos = nullptr;
// [SINGLETON] Crearemos el objeto con esta función estática
void Cheevos::init(const std::string& file) { // NOLINT(readability-convert-member-functions-to-static)
void Cheevos::init(const std::string& file) {
Cheevos::cheevos = new Cheevos(file);
}
@@ -43,9 +43,9 @@ Cheevos::~Cheevos() {
}
// Inicializa los logros
void Cheevos::init() { // NOLINT(readability-convert-member-functions-to-static)
void Cheevos::init() {
cheevos_list_.clear();
auto* loc = Locale::get();
const auto* loc = Locale::get();
cheevos_list_.emplace_back(Achievement{.id = 1, .caption = loc->get("achievements.c1"), .description = loc->get("achievements.d1"), .icon = 2});
cheevos_list_.emplace_back(Achievement{.id = 2, .caption = loc->get("achievements.c2"), .description = loc->get("achievements.d2"), .icon = 2});
cheevos_list_.emplace_back(Achievement{.id = 3, .caption = loc->get("achievements.c3"), .description = loc->get("achievements.d3"), .icon = 2});
@@ -61,7 +61,7 @@ void Cheevos::init() { // NOLINT(readability-convert-member-functions-to-static
}
// Busca un logro por id y devuelve el indice
auto Cheevos::find(int id) -> int { // NOLINT(readability-convert-member-functions-to-static)
auto Cheevos::find(int id) -> int {
for (int i = 0; i < (int)cheevos_list_.size(); ++i) {
if (cheevos_list_[i].id == id) {
return i;
@@ -101,7 +101,7 @@ void Cheevos::setUnobtainable(int id) {
}
// Carga el estado de los logros desde un fichero
void Cheevos::loadFromFile() { // NOLINT(readability-convert-member-functions-to-static)
void Cheevos::loadFromFile() {
std::ifstream file(file_, std::ios::binary);
// El fichero no existe
+40 -57
View File
@@ -36,7 +36,7 @@ auto CollisionMap::getTile(SDL_FPoint point) const -> Tile {
}
// Devuelve el tipo de tile que hay en ese indice
auto CollisionMap::getTile(int index) const -> Tile { // NOLINT(readability-convert-member-functions-to-static)
auto CollisionMap::getTile(int index) const -> Tile {
const bool ON_RANGE = (index > -1) && (index < (int)tile_map_.size());
if (ON_RANGE) {
@@ -107,33 +107,24 @@ auto CollisionMap::getSlopeHeight(SDL_FPoint p, Tile slope) -> int {
// === Queries de colisión ===
// Comprueba las colisiones con paredes derechas
auto CollisionMap::checkRightSurfaces(const SDL_FRect& rect) -> int { // NOLINT(readability-convert-member-functions-to-static)
for (const auto& s : right_walls_) {
if (checkCollision(s, rect)) {
return s.x;
}
}
return Collision::NONE;
auto CollisionMap::checkRightSurfaces(const SDL_FRect& rect) -> int {
auto it = std::ranges::find_if(right_walls_,
[&rect](const auto& s) { return checkCollision(s, rect); });
return (it != right_walls_.end()) ? it->x : Collision::NONE;
}
// Comprueba las colisiones con paredes izquierdas
auto CollisionMap::checkLeftSurfaces(const SDL_FRect& rect) -> int { // NOLINT(readability-convert-member-functions-to-static)
for (const auto& s : left_walls_) {
if (checkCollision(s, rect)) {
return s.x;
}
}
return Collision::NONE;
auto CollisionMap::checkLeftSurfaces(const SDL_FRect& rect) -> int {
auto it = std::ranges::find_if(left_walls_,
[&rect](const auto& s) { return checkCollision(s, rect); });
return (it != left_walls_.end()) ? it->x : Collision::NONE;
}
// Comprueba las colisiones con techos
auto CollisionMap::checkTopSurfaces(const SDL_FRect& rect) -> int { // NOLINT(readability-convert-member-functions-to-static)
for (const auto& s : top_floors_) {
if (checkCollision(s, rect)) {
return s.y;
}
}
return Collision::NONE;
auto CollisionMap::checkTopSurfaces(const SDL_FRect& rect) -> int {
auto it = std::ranges::find_if(top_floors_,
[&rect](const auto& s) { return checkCollision(s, rect); });
return (it != top_floors_.end()) ? it->y : Collision::NONE;
}
// Comprueba las colisiones punto con techos
@@ -144,23 +135,17 @@ auto CollisionMap::checkTopSurfaces(const SDL_FPoint& p) -> bool {
}
// Comprueba las colisiones con suelos
auto CollisionMap::checkBottomSurfaces(const SDL_FRect& rect) -> int { // NOLINT(readability-convert-member-functions-to-static)
for (const auto& s : bottom_floors_) {
if (checkCollision(s, rect)) {
return s.y;
}
}
return Collision::NONE;
auto CollisionMap::checkBottomSurfaces(const SDL_FRect& rect) -> int {
auto it = std::ranges::find_if(bottom_floors_,
[&rect](const auto& s) { return checkCollision(s, rect); });
return (it != bottom_floors_.end()) ? it->y : Collision::NONE;
}
// Comprueba las colisiones con conveyor belts
auto CollisionMap::checkAutoSurfaces(const SDL_FRect& rect) -> int { // NOLINT(readability-convert-member-functions-to-static)
for (const auto& s : conveyor_belt_floors_) {
if (checkCollision(s, rect)) {
return s.y;
}
}
return Collision::NONE;
auto CollisionMap::checkAutoSurfaces(const SDL_FRect& rect) -> int {
auto it = std::ranges::find_if(conveyor_belt_floors_,
[&rect](const auto& s) { return checkCollision(s, rect); });
return (it != conveyor_belt_floors_.end()) ? it->y : Collision::NONE;
}
// Comprueba las colisiones punto con conveyor belts
@@ -171,7 +156,7 @@ auto CollisionMap::checkConveyorBelts(const SDL_FPoint& p) -> bool {
}
// Comprueba las colisiones línea con rampas izquierdas
auto CollisionMap::checkLeftSlopes(const LineVertical& line) -> int { // NOLINT(readability-convert-member-functions-to-static)
auto CollisionMap::checkLeftSlopes(const LineVertical& line) -> int {
for (const auto& slope : left_slopes_) {
const auto P = checkCollision(slope, line);
if (P.x != -1) {
@@ -189,7 +174,7 @@ auto CollisionMap::checkLeftSlopes(const SDL_FPoint& p) -> bool {
}
// Comprueba las colisiones línea con rampas derechas
auto CollisionMap::checkRightSlopes(const LineVertical& line) -> int { // NOLINT(readability-convert-member-functions-to-static)
auto CollisionMap::checkRightSlopes(const LineVertical& line) -> int {
for (const auto& slope : right_slopes_) {
const auto P = checkCollision(slope, line);
if (P.x != -1) {
@@ -207,19 +192,17 @@ auto CollisionMap::checkRightSlopes(const SDL_FPoint& p) -> bool {
}
// Obtiene puntero a slope en un punto (prioriza left_slopes_ sobre right_slopes_)
auto CollisionMap::getSlopeAtPoint(const SDL_FPoint& p) const -> const LineDiagonal* { // NOLINT(readability-convert-member-functions-to-static)
// Primero busca en rampas izquierdas
for (const auto& slope : left_slopes_) {
if (checkCollision(p, slope)) {
return &slope;
}
auto CollisionMap::getSlopeAtPoint(const SDL_FPoint& p) const -> const LineDiagonal* {
auto pred = [&p](const auto& slope) { return static_cast<bool>(checkCollision(p, slope)); };
auto left_it = std::ranges::find_if(left_slopes_, pred);
if (left_it != left_slopes_.end()) {
return &(*left_it);
}
// Luego busca en rampas derechas
for (const auto& slope : right_slopes_) {
if (checkCollision(p, slope)) {
return &slope;
}
auto right_it = std::ranges::find_if(right_slopes_, pred);
if (right_it != right_slopes_.end()) {
return &(*right_it);
}
// No hay colisión con ninguna slope
@@ -229,7 +212,7 @@ auto CollisionMap::getSlopeAtPoint(const SDL_FPoint& p) const -> const LineDiago
// === Helpers para recopilar tiles ===
// Helper: recopila tiles inferiores (muros sin muro debajo)
auto CollisionMap::collectBottomTiles() -> std::vector<int> { // NOLINT(readability-make-member-function-const)
auto CollisionMap::collectBottomTiles() -> std::vector<int> {
std::vector<int> tile;
// Busca todos los tiles de tipo muro que no tengan debajo otro muro
@@ -251,7 +234,7 @@ auto CollisionMap::collectBottomTiles() -> std::vector<int> { // NOLINT(readabi
}
// Helper: recopila tiles superiores (muros o pasables sin muro encima)
auto CollisionMap::collectTopTiles() -> std::vector<int> { // NOLINT(readability-make-member-function-const)
auto CollisionMap::collectTopTiles() -> std::vector<int> {
std::vector<int> tile;
// Busca todos los tiles de tipo muro o pasable que no tengan encima un muro
@@ -273,7 +256,7 @@ auto CollisionMap::collectTopTiles() -> std::vector<int> { // NOLINT(readabilit
}
// Helper: recopila tiles animados (para superficies automaticas/conveyor belts)
auto CollisionMap::collectAnimatedTiles() -> std::vector<int> { // NOLINT(readability-make-member-function-const)
auto CollisionMap::collectAnimatedTiles() -> std::vector<int> {
std::vector<int> tile;
// Busca todos los tiles de tipo animado
@@ -298,7 +281,7 @@ auto CollisionMap::collectAnimatedTiles() -> std::vector<int> { // NOLINT(reada
}
// Helper: construye lineas horizontales a partir de tiles consecutivos
void CollisionMap::buildHorizontalLines(const std::vector<int>& tiles, std::vector<LineHorizontal>& lines, bool is_bottom_surface) { // NOLINT(readability-convert-member-functions-to-static)
void CollisionMap::buildHorizontalLines(const std::vector<int>& tiles, std::vector<LineHorizontal>& lines, bool is_bottom_surface) {
if (tiles.size() <= 1) {
return;
}
@@ -354,7 +337,7 @@ void CollisionMap::setTopSurfaces() {
}
// Calcula las superficies laterales izquierdas
void CollisionMap::setLeftSurfaces() { // NOLINT(readability-make-member-function-const)
void CollisionMap::setLeftSurfaces() {
std::vector<int> tile;
// Busca todos los tiles de tipo muro que no tienen a su izquierda un tile de tipo muro
@@ -394,7 +377,7 @@ void CollisionMap::setLeftSurfaces() { // NOLINT(readability-make-member-functi
}
// Calcula las superficies laterales derechas
void CollisionMap::setRightSurfaces() { // NOLINT(readability-make-member-function-const)
void CollisionMap::setRightSurfaces() {
std::vector<int> tile;
// Busca todos los tiles de tipo muro que no tienen a su derecha un tile de tipo muro
@@ -434,7 +417,7 @@ void CollisionMap::setRightSurfaces() { // NOLINT(readability-make-member-funct
}
// Encuentra todas las rampas que suben hacia la izquierda
void CollisionMap::setLeftSlopes() { // NOLINT(readability-make-member-function-const)
void CollisionMap::setLeftSlopes() {
// Recorre la habitación entera por filas buscando tiles de tipo t_slope_l
std::vector<int> found;
for (int i = 0; i < (int)tile_map_.size(); ++i) {
@@ -469,7 +452,7 @@ void CollisionMap::setLeftSlopes() { // NOLINT(readability-make-member-function
}
// Encuentra todas las rampas que suben hacia la derecha
void CollisionMap::setRightSlopes() { // NOLINT(readability-make-member-function-const)
void CollisionMap::setRightSlopes() {
// Recorre la habitación entera por filas buscando tiles de tipo t_slope_r
std::vector<int> found;
for (int i = 0; i < (int)tile_map_.size(); ++i) {
+3 -2
View File
@@ -2,7 +2,8 @@
#include <SDL3/SDL.h>
#include <vector> // Para vector
#include <cstdint> // Para uint8_t
#include <vector> // Para vector
#include "utils/utils.hpp" // Para LineHorizontal, LineDiagonal, LineVertical
@@ -18,7 +19,7 @@
class CollisionMap {
public:
// Enumeración de tipos de tile (para colisiones)
enum class Tile {
enum class Tile : std::uint8_t {
EMPTY,
WALL,
PASSABLE,
+2 -2
View File
@@ -6,7 +6,7 @@
#include "utils/utils.hpp" // Para checkCollision
// Añade un enemigo a la colección
void EnemyManager::addEnemy(std::shared_ptr<Enemy> enemy) { // NOLINT(readability-identifier-naming)
void EnemyManager::addEnemy(std::shared_ptr<Enemy> enemy) {
enemies_.push_back(std::move(enemy));
}
@@ -16,7 +16,7 @@ void EnemyManager::clear() {
}
// Elimina el último enemigo de la colección
void EnemyManager::removeLastEnemy() { // NOLINT(readability-convert-member-functions-to-static)
void EnemyManager::removeLastEnemy() {
if (!enemies_.empty()) {
enemies_.pop_back();
}
+2 -2
View File
@@ -14,7 +14,7 @@ ItemManager::ItemManager(std::string room_name, std::shared_ptr<Scoreboard::Data
}
// Añade un item a la colección
void ItemManager::addItem(std::shared_ptr<Item> item) { // NOLINT(readability-identifier-naming)
void ItemManager::addItem(std::shared_ptr<Item> item) {
items_.push_back(std::move(item));
}
@@ -45,7 +45,7 @@ void ItemManager::setPaused(bool paused) {
}
// Comprueba si hay colisión con algún item
auto ItemManager::checkCollision(SDL_FRect& rect) -> bool { // NOLINT(readability-convert-member-functions-to-static)
auto ItemManager::checkCollision(SDL_FRect& rect) -> bool {
for (int i = 0; i < static_cast<int>(items_.size()); ++i) {
if (::checkCollision(rect, items_.at(i)->getCollider())) {
// Registra el item como recogido
+3 -3
View File
@@ -32,7 +32,7 @@ auto ItemTracker::hasBeenPicked(const std::string& name, SDL_FPoint pos) -> bool
}
// Añade el objeto a la lista de objetos cogidos
void ItemTracker::addItem(const std::string& name, SDL_FPoint pos) { // NOLINT(readability-convert-member-functions-to-static)
void ItemTracker::addItem(const std::string& name, SDL_FPoint pos) {
// Comprueba si el objeto no ha sido recogido con anterioridad
if (!hasBeenPicked(name, pos)) {
// Primero busca si ya hay una entrada con ese nombre
@@ -47,7 +47,7 @@ void ItemTracker::addItem(const std::string& name, SDL_FPoint pos) { // NOLINT(
}
// Busca una entrada en la lista por nombre
auto ItemTracker::findByName(const std::string& name) -> int { // NOLINT(readability-convert-member-functions-to-static)
auto ItemTracker::findByName(const std::string& name) -> int {
int i = 0;
for (const auto& item : items_) {
@@ -61,7 +61,7 @@ auto ItemTracker::findByName(const std::string& name) -> int { // NOLINT(readab
}
// Busca una entrada en la lista por posición
auto ItemTracker::findByPos(int index, SDL_FPoint pos) -> int { // NOLINT(readability-convert-member-functions-to-static)
auto ItemTracker::findByPos(int index, SDL_FPoint pos) -> int {
int i = 0;
for (const auto& item : items_[index].pos) {
+6 -6
View File
@@ -82,7 +82,7 @@ void Room::initializeRoom(const Data& room) {
}
// Abre la jail para poder entrar
void Room::openTheJail() { // NOLINT(readability-convert-member-functions-to-static)
void Room::openTheJail() {
if (data_->jail_is_open && number_ == Defaults::Game::Room::END_ROOM) {
// Elimina el último enemigo (Bry debe ser el último enemigo definido en el fichero)
if (!enemy_manager_->isEmpty()) {
@@ -163,7 +163,7 @@ void Room::setItemColors(const std::string& color1, const std::string& color2) {
#endif
// Actualiza las variables y objetos de la habitación
void Room::update(float delta_time) { // NOLINT(readability-make-member-function-const)
void Room::update(float delta_time) {
if (is_paused_) {
// Si está en modo pausa no se actualiza nada
return;
@@ -187,7 +187,7 @@ void Room::setPaused(bool value) {
}
// Devuelve la cadena del fichero de la habitación contigua segun el borde
auto Room::getRoom(Border border) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
auto Room::getRoom(Border border) -> std::string {
switch (border) {
case Border::TOP:
return upper_room_;
@@ -203,14 +203,14 @@ auto Room::getRoom(Border border) -> std::string { // NOLINT(readability-conver
}
// Devuelve el tipo de tile que hay en ese pixel
auto Room::getTile(SDL_FPoint point) -> Tile { // NOLINT(readability-convert-member-functions-to-static)
auto Room::getTile(SDL_FPoint point) -> Tile {
// Delega a CollisionMap y convierte el resultado
const auto COLLISION_TILE = collision_map_->getTile(point);
return static_cast<Tile>(COLLISION_TILE);
}
// Devuelve el tipo de tile en un índice del tilemap
auto Room::getTile(int index) -> Tile { // NOLINT(readability-convert-member-functions-to-static)
auto Room::getTile(int index) -> Tile {
// Delega a CollisionMap y convierte el resultado
const auto COLLISION_TILE = collision_map_->getTile(index);
return static_cast<Tile>(COLLISION_TILE);
@@ -296,6 +296,6 @@ auto Room::getSlopeAtPoint(const SDL_FPoint& p) const -> const LineDiagonal* {
}
// Carga una habitación desde un archivo YAML (delegado a RoomLoader)
auto Room::loadYAML(const std::string& file_path, bool verbose) -> Data { // NOLINT(readability-convert-member-functions-to-static)
auto Room::loadYAML(const std::string& file_path, bool verbose) -> Data {
return RoomLoader::loadYAML(file_path, verbose);
}
+6 -5
View File
@@ -2,9 +2,10 @@
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include <cstdint> // Para uint8_t
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "game/entities/enemy.hpp" // Para EnemyData
#include "game/entities/item.hpp" // Para ItemData
@@ -20,7 +21,7 @@ class TilemapRenderer;
class Room {
public:
// -- Enumeraciones y estructuras ---
enum class Border : int {
enum class Border : std::uint8_t {
TOP = 0,
RIGHT = 1,
BOTTOM = 2,
@@ -28,7 +29,7 @@ class Room {
NONE = 4
};
enum class Tile {
enum class Tile : std::uint8_t {
EMPTY,
WALL,
PASSABLE,
+14 -20
View File
@@ -10,7 +10,7 @@
#include "utils/utils.hpp" // Para stringToColor
// Convierte room connection de YAML a formato interno
auto RoomLoader::convertRoomConnection(const std::string& value) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
auto RoomLoader::convertRoomConnection(const std::string& value) -> std::string {
if (value == "null" || value.empty()) {
return "0";
}
@@ -22,7 +22,7 @@ auto RoomLoader::convertRoomConnection(const std::string& value) -> std::string
}
// Convierte string de autoSurface a int
auto RoomLoader::convertAutoSurface(const fkyaml::node& node) -> int { // NOLINT(readability-convert-member-functions-to-static)
auto RoomLoader::convertAutoSurface(const fkyaml::node& node) -> int {
if (node.is_integer()) {
return node.get_value<int>();
}
@@ -39,21 +39,19 @@ auto RoomLoader::convertAutoSurface(const fkyaml::node& node) -> int { // NOLIN
}
// Convierte un tilemap 2D a vector 1D flat
auto RoomLoader::flattenTilemap(const std::vector<std::vector<int>>& tilemap_2d) -> std::vector<int> { // NOLINT(readability-convert-member-functions-to-static, readability-named-parameter)
auto RoomLoader::flattenTilemap(const std::vector<std::vector<int>>& tilemap_2d) -> std::vector<int> {
std::vector<int> tilemap_flat;
tilemap_flat.reserve(512); // 16 rows × 32 cols
for (const auto& row : tilemap_2d) {
for (int tile : row) {
tilemap_flat.push_back(tile);
}
std::ranges::copy(row, std::back_inserter(tilemap_flat));
}
return tilemap_flat;
}
// Parsea la configuración general de la habitación
void RoomLoader::parseRoomConfig(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name) { // NOLINT(readability-convert-member-functions-to-static)
void RoomLoader::parseRoomConfig(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name) {
if (!yaml.contains("room")) {
return;
}
@@ -120,7 +118,7 @@ void RoomLoader::parseRoomConnections(const fkyaml::node& conn_node, Room::Data&
}
// Parsea el tilemap de la habitación
void RoomLoader::parseTilemap(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name, bool verbose) { // NOLINT(readability-convert-member-functions-to-static)
void RoomLoader::parseTilemap(const fkyaml::node& yaml, Room::Data& room, const std::string& file_name, bool verbose) {
if (!yaml.contains("tilemap")) {
std::cerr << "Warning: No tilemap found in " << file_name << '\n';
return;
@@ -135,12 +133,8 @@ void RoomLoader::parseTilemap(const fkyaml::node& yaml, Room::Data& room, const
for (const auto& row_node : tilemap_node) {
std::vector<int> row;
row.reserve(32);
for (const auto& tile_node : row_node) {
row.push_back(tile_node.get_value<int>());
}
tilemap_2d.push_back(row);
std::ranges::transform(row_node, std::back_inserter(row), [](const auto& tile_node) { return tile_node.template get_value<int>(); });
tilemap_2d.push_back(std::move(row));
}
// Convert to 1D flat array
@@ -152,7 +146,7 @@ void RoomLoader::parseTilemap(const fkyaml::node& yaml, Room::Data& room, const
}
// Parsea los límites de movimiento de un enemigo
void RoomLoader::parseEnemyBoundaries(const fkyaml::node& bounds_node, Enemy::Data& enemy) { // NOLINT(readability-convert-member-functions-to-static)
void RoomLoader::parseEnemyBoundaries(const fkyaml::node& bounds_node, Enemy::Data& enemy) {
// Nuevo formato: position1 y position2
if (bounds_node.contains("position1")) {
const auto& pos1 = bounds_node["position1"];
@@ -189,7 +183,7 @@ void RoomLoader::parseEnemyBoundaries(const fkyaml::node& bounds_node, Enemy::Da
}
// Parsea los datos de un enemigo individual
auto RoomLoader::parseEnemyData(const fkyaml::node& enemy_node) -> Enemy::Data { // NOLINT(readability-convert-member-functions-to-static)
auto RoomLoader::parseEnemyData(const fkyaml::node& enemy_node) -> Enemy::Data {
Enemy::Data enemy;
// Animation path
@@ -246,7 +240,7 @@ auto RoomLoader::parseEnemyData(const fkyaml::node& enemy_node) -> Enemy::Data {
}
// Parsea la lista de enemigos de la habitación
void RoomLoader::parseEnemies(const fkyaml::node& yaml, Room::Data& room, bool verbose) { // NOLINT(readability-convert-member-functions-to-static)
void RoomLoader::parseEnemies(const fkyaml::node& yaml, Room::Data& room, bool verbose) {
if (!yaml.contains("enemies") || yaml["enemies"].is_null()) {
return;
}
@@ -263,7 +257,7 @@ void RoomLoader::parseEnemies(const fkyaml::node& yaml, Room::Data& room, bool v
}
// Parsea los datos de un item individual
auto RoomLoader::parseItemData(const fkyaml::node& item_node, const Room::Data& room) -> Item::Data { // NOLINT(readability-convert-member-functions-to-static)
auto RoomLoader::parseItemData(const fkyaml::node& item_node, const Room::Data& room) -> Item::Data {
Item::Data item;
// Tileset file
@@ -300,7 +294,7 @@ auto RoomLoader::parseItemData(const fkyaml::node& item_node, const Room::Data&
}
// Parsea la lista de items de la habitación
void RoomLoader::parseItems(const fkyaml::node& yaml, Room::Data& room, bool verbose) { // NOLINT(readability-convert-member-functions-to-static)
void RoomLoader::parseItems(const fkyaml::node& yaml, Room::Data& room, bool verbose) {
if (!yaml.contains("items") || yaml["items"].is_null()) {
return;
}
@@ -336,7 +330,7 @@ auto RoomLoader::loadFromString(const std::string& yaml_content, const std::stri
#endif
// Carga un archivo de room en formato YAML
auto RoomLoader::loadYAML(const std::string& file_path, bool verbose) -> Room::Data { // NOLINT(readability-convert-member-functions-to-static)
auto RoomLoader::loadYAML(const std::string& file_path, bool verbose) -> Room::Data {
Room::Data room;
// Extract filename for logging
+1 -1
View File
@@ -70,7 +70,7 @@ class RoomLoader {
* @param tilemap_2d Array 2D de tiles (16 rows × 32 cols)
* @return Vector 1D flat con 512 elementos
*/
static auto flattenTilemap(const std::vector<std::vector<int>>& tilemap_2d) -> std::vector<int>; // NOLINT(readability-avoid-const-params-in-decls)
static auto flattenTilemap(const std::vector<std::vector<int>>& tilemap_2d) -> std::vector<int>;
/**
* @brief Parsea la configuración general de la habitación
+3 -3
View File
@@ -3,17 +3,17 @@
#include <algorithm> // Para std::ranges::any_of
// Comprueba si la habitación ya ha sido visitada
auto RoomTracker::hasBeenVisited(const std::string& name) -> bool { // NOLINT(readability-convert-member-functions-to-static)
auto RoomTracker::hasBeenVisited(const std::string& name) -> bool {
return std::ranges::any_of(rooms_, [&name](const auto& l) -> bool { return l == name; });
}
// Añade la habitación a la lista
auto RoomTracker::addRoom(const std::string& name) -> bool { // NOLINT(readability-convert-member-functions-to-static)
auto RoomTracker::addRoom(const std::string& name) -> bool {
// Comprueba si la habitación ya ha sido visitada
if (!hasBeenVisited(name)) {
// En caso contrario añádela a la lista
rooms_.push_back(name);
return true; // NOLINT(readability-simplify-boolean-expr)
return true;
}
return false;
+8 -9
View File
@@ -36,9 +36,8 @@ Scoreboard::Scoreboard(std::shared_ptr<Data> data)
// Inicializa el vector de colores
const std::vector<std::string> COLORS = {"blue", "magenta", "green", "cyan", "yellow", "white", "bright_blue", "bright_magenta", "bright_green", "bright_cyan", "bright_yellow", "bright_white"};
for (const auto& color : COLORS) {
color_.push_back(stringToColor(color));
}
color_.reserve(COLORS.size());
std::ranges::transform(COLORS, std::back_inserter(color_), [](const auto& color) { return stringToColor(color); });
}
// Pinta el objeto en pantalla
@@ -64,7 +63,7 @@ void Scoreboard::update(float delta_time) {
}
// Obtiene el tiempo transcurrido de partida
auto Scoreboard::getTime() -> Scoreboard::ClockData { // NOLINT(readability-convert-member-functions-to-static)
auto Scoreboard::getTime() -> Scoreboard::ClockData {
const Uint32 TIME_ELAPSED = SDL_GetTicks() - data_->ini_clock - paused_time_elapsed_;
ClockData time;
@@ -160,22 +159,22 @@ void Scoreboard::fillTexture() {
auto text = Resource::Cache::get()->getText("smb2");
const std::string TIME_TEXT = std::to_string((clock_.minutes % 100) / 10) + std::to_string(clock_.minutes % 10) + clock_.separator + std::to_string((clock_.seconds % 60) / 10) + std::to_string(clock_.seconds % 10);
const std::string ITEMS_TEXT = std::to_string(data_->items / 100) + std::to_string((data_->items % 100) / 10) + std::to_string(data_->items % 10);
text->writeColored(ITEMS_LABEL_X, LINE1_Y, Locale::get()->get("scoreboard.items"), data_->color); // NOLINT(readability-static-accessed-through-instance)
text->writeColored(ITEMS_LABEL_X, LINE1_Y, Locale::get()->get("scoreboard.items"), data_->color);
text->writeColored(ITEMS_VALUE_X, LINE1_Y, ITEMS_TEXT, items_color_);
text->writeColored(TIME_LABEL_X, LINE1_Y, Locale::get()->get("scoreboard.time"), data_->color); // NOLINT(readability-static-accessed-through-instance)
text->writeColored(TIME_LABEL_X, LINE1_Y, Locale::get()->get("scoreboard.time"), data_->color);
text->writeColored(TIME_VALUE_X, LINE1_Y, TIME_TEXT, stringToColor("white"));
const std::string ROOMS_TEXT = std::to_string(data_->rooms / 100) + std::to_string((data_->rooms % 100) / 10) + std::to_string(data_->rooms % 10);
text->writeColored(ROOMS_LABEL_X, LINE2_Y, Locale::get()->get("scoreboard.rooms"), stringToColor("white")); // NOLINT(readability-static-accessed-through-instance)
text->writeColored(ROOMS_LABEL_X, LINE2_Y, Locale::get()->get("scoreboard.rooms"), stringToColor("white"));
text->writeColored(ROOMS_VALUE_X, LINE2_Y, ROOMS_TEXT, stringToColor("white"));
// Indicadores de trucos activos (fuente 8bithud)
auto cheat_text = Resource::Cache::get()->getText("8bithud");
if (Options::cheats.infinite_lives == Options::Cheat::State::ENABLED) {
cheat_text->writeColored(CHEAT_INF_LIVES_X, CHEAT_INF_LIVES_Y, Locale::get()->get("scoreboard.cheat_infinite_lives"), data_->color); // NOLINT(readability-static-accessed-through-instance)
cheat_text->writeColored(CHEAT_INF_LIVES_X, CHEAT_INF_LIVES_Y, Locale::get()->get("scoreboard.cheat_infinite_lives"), data_->color);
}
if (Options::cheats.invincible == Options::Cheat::State::ENABLED) {
cheat_text->writeColored(CHEAT_INVINCIBLE_X, CHEAT_INVINCIBLE_Y, Locale::get()->get("scoreboard.cheat_invincibility"), data_->color); // NOLINT(readability-static-accessed-through-instance)
cheat_text->writeColored(CHEAT_INVINCIBLE_X, CHEAT_INVINCIBLE_Y, Locale::get()->get("scoreboard.cheat_invincibility"), data_->color);
}
// Deja el renderizador como estaba
+5 -5
View File
@@ -29,7 +29,7 @@ void Stats::init()
}
// Añade una muerte a las estadisticas
void Stats::addDeath(const std::string& name) { // NOLINT(readability-convert-member-functions-to-static)
void Stats::addDeath(const std::string& name) {
// Primero busca si ya hay una entrada con ese nombre
const int INDEX = findByName(name, buffer_list_);
if (INDEX != -1) {
@@ -47,7 +47,7 @@ void Stats::addDeath(const std::string& name) { // NOLINT(readability-convert-m
}
// Añade una visita a las estadisticas
void Stats::addVisit(const std::string& name) { // NOLINT(readability-convert-member-functions-to-static)
void Stats::addVisit(const std::string& name) {
// Primero busca si ya hay una entrada con ese nombre
const int INDEX = findByName(name, buffer_list_);
if (INDEX != -1) {
@@ -65,7 +65,7 @@ void Stats::addVisit(const std::string& name) { // NOLINT(readability-convert-m
}
// Busca una entrada en la lista por nombre
auto Stats::findByName(const std::string& name, const std::vector<RoomData>& list) -> int { // NOLINT(readability-convert-member-functions-to-static)
auto Stats::findByName(const std::string& name, const std::vector<RoomData>& list) -> int {
int i = 0;
for (const auto& l : list) {
@@ -79,7 +79,7 @@ auto Stats::findByName(const std::string& name, const std::vector<RoomData>& lis
}
// Carga las estadisticas desde un fichero
void Stats::loadFromFile(const std::string& file_path, std::vector<RoomData>& list) { // NOLINT(readability-convert-member-functions-to-static)
void Stats::loadFromFile(const std::string& file_path, std::vector<RoomData>& list) {
list.clear();
// Variables para manejar el fichero
@@ -149,7 +149,7 @@ void Stats::saveToFile(const std::string& file_path, const std::vector<RoomData>
}
// Calcula cual es la habitación con más muertes
void Stats::checkWorstNightmare() { // NOLINT(readability-convert-member-functions-to-static)
void Stats::checkWorstNightmare() {
int deaths = 0;
for (const auto& item : list_) {
if (item.died > deaths) {
+5 -6
View File
@@ -15,9 +15,8 @@ TilemapRenderer::TilemapRenderer(std::vector<int> tile_map, int tile_set_width,
tile_set_width_(tile_set_width),
tileset_surface_(std::move(tileset_surface)),
bg_color_(std::move(bg_color)),
conveyor_belt_direction_(conveyor_belt_direction) {
// Crear la surface del mapa
map_surface_ = std::make_shared<Surface>(PlayArea::WIDTH, PlayArea::HEIGHT);
conveyor_belt_direction_(conveyor_belt_direction),
map_surface_(std::make_shared<Surface>(PlayArea::WIDTH, PlayArea::HEIGHT)) {
}
// Inicializa el renderizador
@@ -134,7 +133,7 @@ void TilemapRenderer::setTile(int index, int tile_value) {
#endif
// Pinta el mapa estático y debug lines
void TilemapRenderer::fillMapTexture(const CollisionMap* collision_map) { // NOLINT(readability-convert-member-functions-to-static)
void TilemapRenderer::fillMapTexture([[maybe_unused]] const CollisionMap* collision_map) {
const Uint8 COLOR = stringToColor(bg_color_);
auto previous_renderer = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(map_surface_);
@@ -176,7 +175,7 @@ void TilemapRenderer::fillMapTexture(const CollisionMap* collision_map) { // NO
}
// Localiza todos los tiles animados
void TilemapRenderer::setAnimatedTiles(const CollisionMap* collision_map) { // NOLINT(readability-convert-member-functions-to-static)
void TilemapRenderer::setAnimatedTiles(const CollisionMap* collision_map) {
// Recorre la habitación entera por filas buscando tiles de tipo t_animated
for (int i = 0; i < (int)tile_map_.size(); ++i) {
const auto TILE_TYPE = collision_map->getTile(i);
@@ -199,7 +198,7 @@ void TilemapRenderer::setAnimatedTiles(const CollisionMap* collision_map) { //
}
// Actualiza tiles animados
void TilemapRenderer::updateAnimatedTiles() { // NOLINT(readability-make-member-function-const)
void TilemapRenderer::updateAnimatedTiles() {
const int NUM_FRAMES = 4;
// Calcular frame actual basado en tiempo
+161 -283
View File
@@ -2,9 +2,11 @@
#include <SDL3/SDL.h>
#include <algorithm> // Para ranges::transform
#include <filesystem> // Para create_directories
#include <fstream> // Para ifstream, ofstream
#include <iostream> // Para cout, cerr
#include <iterator> // Para back_inserter
#include <string> // Para string
#include <unordered_map> // Para unordered_map
@@ -323,31 +325,6 @@ namespace Options {
}
}
// Helper: carga la sección supersampling desde YAML
void loadSupersamplingConfigFromYaml(const fkyaml::node& ss_node) {
if (ss_node.contains("enabled")) {
try {
video.supersampling.enabled = ss_node["enabled"].get_value<bool>();
} catch (...) {
video.supersampling.enabled = Defaults::Video::SUPERSAMPLING;
}
}
if (ss_node.contains("linear_upscale")) {
try {
video.supersampling.linear_upscale = ss_node["linear_upscale"].get_value<bool>();
} catch (...) {
video.supersampling.linear_upscale = Defaults::Video::LINEAR_UPSCALE;
}
}
if (ss_node.contains("downscale_algo")) {
try {
video.supersampling.downscale_algo = ss_node["downscale_algo"].get_value<int>();
} catch (...) {
video.supersampling.downscale_algo = Defaults::Video::DOWNSCALE_ALGO;
}
}
}
// Helper: carga la sección shader desde YAML
void loadShaderConfigFromYaml(const fkyaml::node& sh_node) {
if (sh_node.contains("enabled")) {
@@ -368,12 +345,14 @@ namespace Options {
if (sh_node.contains("current_postfx_preset")) {
try {
video.shader.current_postfx_preset_name = sh_node["current_postfx_preset"].get_value<std::string>();
} catch (...) {}
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
}
}
if (sh_node.contains("current_crtpi_preset")) {
try {
video.shader.current_crtpi_preset_name = sh_node["current_crtpi_preset"].get_value<std::string>();
} catch (...) {}
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
}
}
}
@@ -435,9 +414,6 @@ namespace Options {
if (vid.contains("gpu")) {
loadGPUConfigFromYaml(vid["gpu"]);
}
if (vid.contains("supersampling")) {
loadSupersamplingConfigFromYaml(vid["supersampling"]);
}
if (vid.contains("shader")) {
loadShaderConfigFromYaml(vid["shader"]);
}
@@ -566,45 +542,42 @@ namespace Options {
}
// Carga configuración de audio desde YAML
void loadAudioConfigFromYaml(const fkyaml::node& yaml) { // NOLINT(readability-function-cognitive-complexity)
namespace {
// Llig parent[key] cap a dst si existeix; ignora errors de format (conserva el default).
template <typename T>
void readYamlField(const fkyaml::node& parent, const char* key, T& dst) {
if (!parent.contains(key)) { return; }
try {
dst = parent[key].template get_value<T>();
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
}
}
// Versió específica per a volums (clamp a [0,1])
void readYamlVolume(const fkyaml::node& parent, const char* key, float& dst) {
if (!parent.contains(key)) { return; }
try {
dst = std::clamp(parent[key].get_value<float>(), 0.0F, 1.0F);
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
}
}
} // namespace
void loadAudioConfigFromYaml(const fkyaml::node& yaml) {
if (!yaml.contains("audio")) { return; }
const auto& a = yaml["audio"];
if (a.contains("enabled")) {
try {
audio.enabled = a["enabled"].get_value<bool>();
} catch (...) {}
}
if (a.contains("volume")) {
try {
audio.volume = std::clamp(a["volume"].get_value<float>(), 0.0F, 1.0F);
} catch (...) {}
}
readYamlField(a, "enabled", audio.enabled);
readYamlVolume(a, "volume", audio.volume);
if (a.contains("music")) {
const auto& m = a["music"];
if (m.contains("enabled")) {
try {
audio.music.enabled = m["enabled"].get_value<bool>();
} catch (...) {}
}
if (m.contains("volume")) {
try {
audio.music.volume = std::clamp(m["volume"].get_value<float>(), 0.0F, 1.0F);
} catch (...) {}
}
readYamlField(m, "enabled", audio.music.enabled);
readYamlVolume(m, "volume", audio.music.volume);
}
if (a.contains("sound")) {
const auto& s = a["sound"];
if (s.contains("enabled")) {
try {
audio.sound.enabled = s["enabled"].get_value<bool>();
} catch (...) {}
}
if (s.contains("volume")) {
try {
audio.sound.volume = std::clamp(s["volume"].get_value<float>(), 0.0F, 1.0F);
} catch (...) {}
}
readYamlField(s, "enabled", audio.sound.enabled);
readYamlVolume(s, "volume", audio.sound.volume);
}
}
@@ -786,10 +759,6 @@ namespace Options {
file << " gpu:\n";
file << " acceleration: " << (video.gpu.acceleration ? "true" : "false") << " # Usar aceleración hardware GPU (false = SDL fallback)\n";
file << " preferred_driver: \"" << video.gpu.preferred_driver << "\" # Driver GPU específico (empty = auto, aplica solo si gpu_acceleration: true)\n";
file << " supersampling:\n";
file << " enabled: " << (video.supersampling.enabled ? "true" : "false") << "\n";
file << " linear_upscale: " << (video.supersampling.linear_upscale ? "true" : "false") << "\n";
file << " downscale_algo: " << video.supersampling.downscale_algo << " # 0=bilinear, 1=Lanczos2, 2=Lanczos3\n";
file << " shader:\n";
file << " enabled: " << (video.shader.enabled ? "true" : "false") << "\n";
file << " current_shader: " << (video.shader.current_shader == Rendering::ShaderType::CRTPI ? "crtpi" : "postfx") << "\n";
@@ -891,7 +860,8 @@ namespace Options {
if (node.contains(key)) {
try {
target = node[key].get_value<float>();
} catch (...) {}
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
}
}
}
@@ -920,14 +890,26 @@ namespace Options {
}
parseFloatField(p, "vignette", preset.vignette);
parseFloatField(p, "scanlines", preset.scanlines);
parseFloatField(p, "chroma", preset.chroma);
// Compat: 'chroma' antic → assignar a min i max
if (p.contains("chroma")) {
try {
const auto LEGACY = p["chroma"].get_value<float>();
preset.chroma_min = LEGACY;
preset.chroma_max = LEGACY;
} catch (...) { /* @INTENTIONAL: camp malformat → conservem defaults */
}
}
parseFloatField(p, "chroma_min", preset.chroma_min);
parseFloatField(p, "chroma_max", preset.chroma_max);
parseFloatField(p, "mask", preset.mask);
parseFloatField(p, "gamma", preset.gamma);
parseFloatField(p, "curvature", preset.curvature);
parseFloatField(p, "bleeding", preset.bleeding);
parseFloatField(p, "flicker", preset.flicker);
// Nota: 'supersampling' era un campo por-preset (eliminado). Si existe
// en el fichero del usuario se ignora silenciosamente (compatible).
parseFloatField(p, "scan_dark_ratio", preset.scan_dark_ratio);
parseFloatField(p, "scan_dark_floor", preset.scan_dark_floor);
parseFloatField(p, "scan_edge_soft", preset.scan_edge_soft);
// Nota: 'supersampling' era un camp obsolet — s'ignora silenciosament.
postfx_presets.push_back(preset);
}
}
@@ -965,82 +947,47 @@ namespace Options {
file << "# Each preset defines the intensity of post-processing effects (0.0 to 1.0).\n";
file << "# vignette: screen darkening at the edges\n";
file << "# scanlines: horizontal scanline effect\n";
file << "# chroma: chromatic aberration (RGB color fringing)\n";
file << "# chroma_min / chroma_max: chromatic aberration. Si min == max, queda estàtic.\n";
file << "# mask: phosphor dot mask (RGB subpixel pattern)\n";
file << "# gamma: gamma correction input 2.4 / output 2.2\n";
file << "# curvature: CRT barrel distortion\n";
file << "# bleeding: NTSC horizontal colour bleeding\n";
file << "# flicker: phosphor CRT flicker ~50 Hz (0.0 = off, 1.0 = max)\n";
file << "# Note: supersampling is a global toggle in config.yaml, not per-preset.\n";
file << "# flicker: phosphor CRT flicker ~50 Hz\n";
file << "# scan_dark_ratio / scan_dark_floor / scan_edge_soft: forma de les scanlines.\n";
file << "\n";
const std::vector<PostFXPreset> DEFAULTS = {
{.name = "CRT", .vignette = 0.6F, .scanlines = 0.7F, .chroma_min = 0.15F, .chroma_max = 0.15F, .mask = 0.6F, .gamma = 0.8F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F},
{.name = "NTSC", .vignette = 0.4F, .scanlines = 0.5F, .chroma_min = 0.2F, .chroma_max = 0.2F, .mask = 0.4F, .gamma = 0.5F, .curvature = 0.0F, .bleeding = 0.6F, .flicker = 0.0F},
{.name = "CURVED", .vignette = 0.5F, .scanlines = 0.6F, .chroma_min = 0.1F, .chroma_max = 0.1F, .mask = 0.5F, .gamma = 0.7F, .curvature = 0.8F, .bleeding = 0.0F, .flicker = 0.0F},
{.name = "SCANLINES", .vignette = 0.0F, .scanlines = 0.8F, .chroma_min = 0.0F, .chroma_max = 0.0F, .mask = 0.0F, .gamma = 0.0F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F},
{.name = "SUBTLE", .vignette = 0.3F, .scanlines = 0.4F, .chroma_min = 0.05F, .chroma_max = 0.05F, .mask = 0.0F, .gamma = 0.3F, .curvature = 0.0F, .bleeding = 0.0F, .flicker = 0.0F},
{.name = "CRT LIVE", .vignette = 0.5F, .scanlines = 0.6F, .chroma_min = 0.1F, .chroma_max = 0.3F, .mask = 0.3F, .gamma = 0.4F, .curvature = 0.3F, .bleeding = 0.4F, .flicker = 0.8F},
};
file << "presets:\n";
file << " - name: \"CRT\"\n";
file << " vignette: 0.6\n";
file << " scanlines: 0.7\n";
file << " chroma: 0.15\n";
file << " mask: 0.6\n";
file << " gamma: 0.8\n";
file << " curvature: 0.0\n";
file << " bleeding: 0.0\n";
file << " flicker: 0.0\n";
file << " - name: \"NTSC\"\n";
file << " vignette: 0.4\n";
file << " scanlines: 0.5\n";
file << " chroma: 0.2\n";
file << " mask: 0.4\n";
file << " gamma: 0.5\n";
file << " curvature: 0.0\n";
file << " bleeding: 0.6\n";
file << " flicker: 0.0\n";
file << " - name: \"CURVED\"\n";
file << " vignette: 0.5\n";
file << " scanlines: 0.6\n";
file << " chroma: 0.1\n";
file << " mask: 0.5\n";
file << " gamma: 0.7\n";
file << " curvature: 0.8\n";
file << " bleeding: 0.0\n";
file << " flicker: 0.0\n";
file << " - name: \"SCANLINES\"\n";
file << " vignette: 0.0\n";
file << " scanlines: 0.8\n";
file << " chroma: 0.0\n";
file << " mask: 0.0\n";
file << " gamma: 0.0\n";
file << " curvature: 0.0\n";
file << " bleeding: 0.0\n";
file << " flicker: 0.0\n";
file << " - name: \"SUBTLE\"\n";
file << " vignette: 0.3\n";
file << " scanlines: 0.4\n";
file << " chroma: 0.05\n";
file << " mask: 0.0\n";
file << " gamma: 0.3\n";
file << " curvature: 0.0\n";
file << " bleeding: 0.0\n";
file << " flicker: 0.0\n";
file << " - name: \"CRT LIVE\"\n";
file << " vignette: 0.5\n";
file << " scanlines: 0.6\n";
file << " chroma: 0.3\n";
file << " mask: 0.3\n";
file << " gamma: 0.4\n";
file << " curvature: 0.3\n";
file << " bleeding: 0.4\n";
file << " flicker: 0.8\n";
for (const auto& preset : DEFAULTS) {
file << " - name: \"" << preset.name << "\"\n";
file << " vignette: " << preset.vignette << "\n";
file << " scanlines: " << preset.scanlines << "\n";
file << " chroma_min: " << preset.chroma_min << "\n";
file << " chroma_max: " << preset.chroma_max << "\n";
file << " mask: " << preset.mask << "\n";
file << " gamma: " << preset.gamma << "\n";
file << " curvature: " << preset.curvature << "\n";
file << " bleeding: " << preset.bleeding << "\n";
file << " flicker: " << preset.flicker << "\n";
file << " scan_dark_ratio: " << preset.scan_dark_ratio << "\n";
file << " scan_dark_floor: " << preset.scan_dark_floor << "\n";
file << " scan_edge_soft: " << preset.scan_edge_soft << "\n";
}
file.close();
std::cout << "PostFX file created with defaults: " << postfx_file_path << '\n';
// Cargar los presets recién creados
postfx_presets.clear();
postfx_presets.push_back({"CRT", 0.6F, 0.7F, 0.3F, 0.6F, 0.8F, 0.0F, 0.0F, 0.0F});
postfx_presets.push_back({"NTSC", 0.4F, 0.5F, 0.2F, 0.4F, 0.5F, 0.0F, 0.6F, 0.0F});
postfx_presets.push_back({"CURVED", 0.5F, 0.6F, 0.1F, 0.5F, 0.7F, 0.8F, 0.0F, 0.0F});
postfx_presets.push_back({"SCANLINES", 0.0F, 0.8F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F, 0.0F});
postfx_presets.push_back({"SUBTLE", 0.3F, 0.4F, 0.05F, 0.0F, 0.3F, 0.0F, 0.0F, 0.0F});
postfx_presets.push_back({"CRT LIVE", 0.5F, 0.6F, 0.3F, 0.3F, 0.4F, 0.3F, 0.4F, 0.8F});
postfx_presets = DEFAULTS;
video.shader.current_postfx_preset = 0;
return true;
@@ -1050,110 +997,91 @@ namespace Options {
crtpi_file_path = path;
}
// Defaults dels 4 presets CrtPi (DEFAULT, CURVED, SHARP, MINIMAL)
static auto defaultCrtPiPresets() -> std::vector<CrtPiPreset> {
return {
{.name = "DEFAULT", .scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = true, .enable_multisample = true, .enable_gamma = true, .enable_curvature = false, .enable_sharper = false},
{.name = "CURVED", .scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = true, .enable_multisample = true, .enable_gamma = true, .enable_curvature = true, .enable_sharper = false},
{.name = "SHARP", .scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = true, .enable_multisample = false, .enable_gamma = true, .enable_curvature = false, .enable_sharper = true},
{.name = "MINIMAL", .scanline_weight = 8.0F, .scanline_gap_brightness = 0.05F, .bloom_factor = 2.0F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 1.00F, .curvature_x = 0.0F, .curvature_y = 0.0F, .mask_type = 0, .enable_scanlines = true, .enable_multisample = false, .enable_gamma = false, .enable_curvature = false, .enable_sharper = false}};
}
// Escriu el fitxer CrtPi amb capçalera + els 4 presets default. Retorna false si no pot obrir.
static auto writeCrtPiDefaultFile(const std::string& path, const std::vector<CrtPiPreset>& presets) -> bool {
const std::filesystem::path P(path);
if (P.has_parent_path()) {
std::error_code ec;
std::filesystem::create_directories(P.parent_path(), ec);
}
std::ofstream out(path);
if (!out.is_open()) { return false; }
out << "# JailDoctor's Dilemma - CrtPi Shader Presets\n";
out << "# scanline_weight: ajuste gaussiano (mayor = scanlines mas estrechas, default 6.0)\n";
out << "# scanline_gap_brightness: brillo minimo entre scanlines (0.0-1.0, default 0.12)\n";
out << "# bloom_factor: factor de brillo para zonas iluminadas (default 3.5)\n";
out << "# input_gamma: gamma de entrada - linealizacion (default 2.4)\n";
out << "# output_gamma: gamma de salida - codificacion (default 2.2)\n";
out << "# mask_brightness: brillo sub-pixeles de la mascara de fosforo (default 0.80)\n";
out << "# curvature_x/y: distorsion barrel CRT (0.0 = plana)\n";
out << "# mask_type: 0=ninguna, 1=verde/magenta, 2=RGB fosforo\n";
out << "# enable_scanlines/multisample/gamma/curvature/sharper: true/false\n";
out << "\npresets:\n";
for (const auto& p : presets) {
out << " - name: \"" << p.name << "\"\n";
out << " scanline_weight: " << p.scanline_weight << "\n";
out << " scanline_gap_brightness: " << p.scanline_gap_brightness << "\n";
out << " bloom_factor: " << p.bloom_factor << "\n";
out << " input_gamma: " << p.input_gamma << "\n";
out << " output_gamma: " << p.output_gamma << "\n";
out << " mask_brightness: " << p.mask_brightness << "\n";
out << " curvature_x: " << p.curvature_x << "\n";
out << " curvature_y: " << p.curvature_y << "\n";
out << " mask_type: " << p.mask_type << "\n";
out << " enable_scanlines: " << (p.enable_scanlines ? "true" : "false") << "\n";
out << " enable_multisample: " << (p.enable_multisample ? "true" : "false") << "\n";
out << " enable_gamma: " << (p.enable_gamma ? "true" : "false") << "\n";
out << " enable_curvature: " << (p.enable_curvature ? "true" : "false") << "\n";
out << " enable_sharper: " << (p.enable_sharper ? "true" : "false") << "\n";
}
return true;
}
// Parseja un node YAML a un CrtPiPreset usant els helpers genèrics
static auto parseCrtPiPreset(const fkyaml::node& p) -> CrtPiPreset {
CrtPiPreset preset;
readYamlField(p, "name", preset.name);
parseFloatField(p, "scanline_weight", preset.scanline_weight);
parseFloatField(p, "scanline_gap_brightness", preset.scanline_gap_brightness);
parseFloatField(p, "bloom_factor", preset.bloom_factor);
parseFloatField(p, "input_gamma", preset.input_gamma);
parseFloatField(p, "output_gamma", preset.output_gamma);
parseFloatField(p, "mask_brightness", preset.mask_brightness);
parseFloatField(p, "curvature_x", preset.curvature_x);
parseFloatField(p, "curvature_y", preset.curvature_y);
readYamlField(p, "mask_type", preset.mask_type);
readYamlField(p, "enable_scanlines", preset.enable_scanlines);
readYamlField(p, "enable_multisample", preset.enable_multisample);
readYamlField(p, "enable_gamma", preset.enable_gamma);
readYamlField(p, "enable_curvature", preset.enable_curvature);
readYamlField(p, "enable_sharper", preset.enable_sharper);
return preset;
}
// Carga los presets del shader CrtPi desde el fichero. Crea defaults si no existe.
auto loadCrtPiFromFile() -> bool { // NOLINT(readability-function-cognitive-complexity)
auto loadCrtPiFromFile() -> bool {
crtpi_presets.clear();
std::ifstream file(crtpi_file_path);
if (!file.good()) {
std::cout << "CrtPi file not found, creating default: " << crtpi_file_path << '\n';
// Crear directorio padre si no existe
const std::filesystem::path P(crtpi_file_path);
if (P.has_parent_path()) {
std::error_code ec;
std::filesystem::create_directories(P.parent_path(), ec);
}
// Escribir defaults
std::ofstream out(crtpi_file_path);
if (!out.is_open()) {
std::cerr << "Error: Cannot create CrtPi file: " << crtpi_file_path << '\n';
// Cargar defaults en memoria aunque no se pueda escribir
crtpi_presets.push_back({"DEFAULT", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, false, false});
crtpi_presets.push_back({"CURVED", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, true, false});
crtpi_presets.push_back({"SHARP", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, false, true, false, true});
crtpi_presets.push_back({"MINIMAL", 8.0F, 0.05F, 2.0F, 2.4F, 2.2F, 1.00F, 0.0F, 0.0F, 0, true, false, false, false, false});
video.shader.current_crtpi_preset = 0;
return true;
}
out << "# JailDoctor's Dilemma - CrtPi Shader Presets\n";
out << "# scanline_weight: ajuste gaussiano (mayor = scanlines mas estrechas, default 6.0)\n";
out << "# scanline_gap_brightness: brillo minimo entre scanlines (0.0-1.0, default 0.12)\n";
out << "# bloom_factor: factor de brillo para zonas iluminadas (default 3.5)\n";
out << "# input_gamma: gamma de entrada - linealizacion (default 2.4)\n";
out << "# output_gamma: gamma de salida - codificacion (default 2.2)\n";
out << "# mask_brightness: brillo sub-pixeles de la mascara de fosforo (default 0.80)\n";
out << "# curvature_x/y: distorsion barrel CRT (0.0 = plana)\n";
out << "# mask_type: 0=ninguna, 1=verde/magenta, 2=RGB fosforo\n";
out << "# enable_scanlines/multisample/gamma/curvature/sharper: true/false\n";
out << "\n";
out << "presets:\n";
out << " - name: \"DEFAULT\"\n";
out << " scanline_weight: 6.0\n";
out << " scanline_gap_brightness: 0.12\n";
out << " bloom_factor: 3.5\n";
out << " input_gamma: 2.4\n";
out << " output_gamma: 2.2\n";
out << " mask_brightness: 0.80\n";
out << " curvature_x: 0.05\n";
out << " curvature_y: 0.10\n";
out << " mask_type: 2\n";
out << " enable_scanlines: true\n";
out << " enable_multisample: true\n";
out << " enable_gamma: true\n";
out << " enable_curvature: false\n";
out << " enable_sharper: false\n";
out << " - name: \"CURVED\"\n";
out << " scanline_weight: 6.0\n";
out << " scanline_gap_brightness: 0.12\n";
out << " bloom_factor: 3.5\n";
out << " input_gamma: 2.4\n";
out << " output_gamma: 2.2\n";
out << " mask_brightness: 0.80\n";
out << " curvature_x: 0.05\n";
out << " curvature_y: 0.10\n";
out << " mask_type: 2\n";
out << " enable_scanlines: true\n";
out << " enable_multisample: true\n";
out << " enable_gamma: true\n";
out << " enable_curvature: true\n";
out << " enable_sharper: false\n";
out << " - name: \"SHARP\"\n";
out << " scanline_weight: 6.0\n";
out << " scanline_gap_brightness: 0.12\n";
out << " bloom_factor: 3.5\n";
out << " input_gamma: 2.4\n";
out << " output_gamma: 2.2\n";
out << " mask_brightness: 0.80\n";
out << " curvature_x: 0.05\n";
out << " curvature_y: 0.10\n";
out << " mask_type: 2\n";
out << " enable_scanlines: true\n";
out << " enable_multisample: false\n";
out << " enable_gamma: true\n";
out << " enable_curvature: false\n";
out << " enable_sharper: true\n";
out << " - name: \"MINIMAL\"\n";
out << " scanline_weight: 8.0\n";
out << " scanline_gap_brightness: 0.05\n";
out << " bloom_factor: 2.0\n";
out << " input_gamma: 2.4\n";
out << " output_gamma: 2.2\n";
out << " mask_brightness: 1.00\n";
out << " curvature_x: 0.0\n";
out << " curvature_y: 0.0\n";
out << " mask_type: 0\n";
out << " enable_scanlines: true\n";
out << " enable_multisample: false\n";
out << " enable_gamma: false\n";
out << " enable_curvature: false\n";
out << " enable_sharper: false\n";
out.close();
std::cout << "CrtPi file created with defaults: " << crtpi_file_path << '\n';
crtpi_presets.push_back({"DEFAULT", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, false, false});
crtpi_presets.push_back({"CURVED", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, true, false});
crtpi_presets.push_back({"SHARP", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, false, true, false, true});
crtpi_presets.push_back({"MINIMAL", 8.0F, 0.05F, 2.0F, 2.4F, 2.2F, 1.00F, 0.0F, 0.0F, 0, true, false, false, false, false});
crtpi_presets = defaultCrtPiPresets();
video.shader.current_crtpi_preset = 0;
if (!writeCrtPiDefaultFile(crtpi_file_path, crtpi_presets)) {
std::cerr << "Error: Cannot create CrtPi file: " << crtpi_file_path << '\n';
return true; // defaults en memòria igualment
}
std::cout << "CrtPi file created with defaults: " << crtpi_file_path << '\n';
return true;
}
@@ -1162,71 +1090,21 @@ namespace Options {
try {
auto yaml = fkyaml::node::deserialize(content);
if (yaml.contains("presets")) {
const auto& presets = yaml["presets"];
for (const auto& p : presets) {
CrtPiPreset preset;
if (p.contains("name")) {
preset.name = p["name"].get_value<std::string>();
}
parseFloatField(p, "scanline_weight", preset.scanline_weight);
parseFloatField(p, "scanline_gap_brightness", preset.scanline_gap_brightness);
parseFloatField(p, "bloom_factor", preset.bloom_factor);
parseFloatField(p, "input_gamma", preset.input_gamma);
parseFloatField(p, "output_gamma", preset.output_gamma);
parseFloatField(p, "mask_brightness", preset.mask_brightness);
parseFloatField(p, "curvature_x", preset.curvature_x);
parseFloatField(p, "curvature_y", preset.curvature_y);
if (p.contains("mask_type")) {
try {
preset.mask_type = p["mask_type"].get_value<int>();
} catch (...) {}
}
if (p.contains("enable_scanlines")) {
try {
preset.enable_scanlines = p["enable_scanlines"].get_value<bool>();
} catch (...) {}
}
if (p.contains("enable_multisample")) {
try {
preset.enable_multisample = p["enable_multisample"].get_value<bool>();
} catch (...) {}
}
if (p.contains("enable_gamma")) {
try {
preset.enable_gamma = p["enable_gamma"].get_value<bool>();
} catch (...) {}
}
if (p.contains("enable_curvature")) {
try {
preset.enable_curvature = p["enable_curvature"].get_value<bool>();
} catch (...) {}
}
if (p.contains("enable_sharper")) {
try {
preset.enable_sharper = p["enable_sharper"].get_value<bool>();
} catch (...) {}
}
crtpi_presets.push_back(preset);
}
const auto& presets_node = yaml["presets"];
std::ranges::transform(presets_node, std::back_inserter(crtpi_presets), parseCrtPiPreset);
}
// Resolver el nombre del preset a índice
if (!crtpi_presets.empty()) {
resolveCrtPiPresetName();
} else {
video.shader.current_crtpi_preset = 0;
}
std::cout << "CrtPi file loaded: " << crtpi_presets.size() << " preset(s)\n";
return true;
} catch (const fkyaml::exception& e) {
std::cerr << "Error parsing CrtPi YAML: " << e.what() << '\n';
// Cargar defaults en memoria en caso de error
crtpi_presets.clear();
crtpi_presets.push_back({"DEFAULT", 6.0F, 0.12F, 3.5F, 2.4F, 2.2F, 0.80F, 0.05F, 0.10F, 2, true, true, true, false, false});
crtpi_presets.push_back(defaultCrtPiPresets().front()); // només DEFAULT en cas d'error
video.shader.current_crtpi_preset = 0;
return false;
}
+14 -17
View File
@@ -81,13 +81,6 @@ namespace Options {
std::string preferred_driver; // Driver GPU preferido; vacío = auto. Aplica en el próximo arranque.
};
// Estructura para las opciones de supersampling
struct Supersampling {
bool enabled{Defaults::Video::SUPERSAMPLING}; // Indica si el supersampling 3× está activo
bool linear_upscale{Defaults::Video::LINEAR_UPSCALE}; // Upscale LINEAR (true) o NEAREST (false)
int downscale_algo{Defaults::Video::DOWNSCALE_ALGO}; // 0=bilinear, 1=Lanczos2, 2=Lanczos3
};
// Estructura para las opciones de shader (dentro de Video)
struct ShaderConfig {
bool enabled{Defaults::Video::SHADER_ENABLED}; // Indica si se usan shaders de post-procesado
@@ -110,7 +103,6 @@ namespace Options {
std::string info; // Información sobre el modo de vídeo
Border border{}; // Borde de la pantalla
GPU gpu{}; // Opciones de aceleración GPU
Supersampling supersampling{}; // Opciones de supersampling
ShaderConfig shader{}; // Opciones de shader post-procesado
};
@@ -151,15 +143,20 @@ namespace Options {
// Estructura para un preset de PostFX
struct PostFXPreset {
std::string name; // Nombre del preset
float vignette{0.6F}; // Intensidad de la viñeta (0.0 = ninguna, 1.0 = máxima)
float scanlines{0.7F}; // Intensidad de las scanlines (0.0 = desactivadas, 1.0 = máximas)
float chroma{0.15F}; // Intensidad de la aberración cromática (0.0 = ninguna, 1.0 = máxima)
float mask{0.0F}; // Intensidad de la máscara de fósforo RGB (0.0 = desactivada, 1.0 = máxima)
float gamma{0.0F}; // Corrección gamma input 2.4 / output 2.2 (0.0 = off, 1.0 = plena)
float curvature{0.0F}; // Distorsión barrel CRT (0.0 = plana, 1.0 = máxima curvatura)
float bleeding{0.0F}; // Sangrado de color NTSC horizontal Y/C (0.0 = off, 1.0 = máximo)
float flicker{0.0F}; // Parpadeo de fósforo CRT ~50 Hz (0.0 = off, 1.0 = máximo)
std::string name; // Nombre del preset
float vignette{0.6F}; // Intensidad de la viñeta (0.0 = ninguna, 1.0 = máxima)
float scanlines{0.7F}; // Intensidad de las scanlines
float chroma_min{0.15F}; // Aberració cromàtica mínima (sempre present)
float chroma_max{0.15F}; // Si != chroma_min → pulsa sinusoidalment
float mask{0.0F}; // Máscara de fósforo RGB
float gamma{0.0F}; // Corrección gamma (0=off, 1=full)
float curvature{0.0F}; // Distorsión barrel CRT
float bleeding{0.0F}; // Sangrado de color NTSC
float flicker{0.0F}; // Parpadeo de fósforo ~50 Hz
// Forma de les scanlines — 3 subpíxels per fila lògica per defecte.
float scan_dark_ratio{0.333F};
float scan_dark_floor{0.42F};
float scan_edge_soft{1.0F};
};
// Estructura para un preset del shader CRT-Pi
+4 -2
View File
@@ -1,5 +1,7 @@
#pragma once
#include <cstdint> // Para uint8_t
/*
Namespace SceneManager: gestiona el flujo entre las diferentes escenas del juego.
@@ -10,7 +12,7 @@
namespace SceneManager {
// --- Escenas del programa ---
enum class Scene {
enum class Scene : std::uint8_t {
BOOT_LOADER, // Carga inicial de recursos dirigida por iterate()
LOGO, // Pantalla del logo
LOADING_SCREEN, // Pantalla de carga
@@ -26,7 +28,7 @@ namespace SceneManager {
};
// --- Opciones para transiciones entre escenas ---
enum class Options {
enum class Options : std::uint8_t {
NONE, // Sin opciones especiales
LOGO_TO_LOADING_SCREEN, // Del logo a la intro
LOGO_TO_TITLE, // Del logo al título
+2 -2
View File
@@ -49,8 +49,8 @@ void Credits::handleInput() {
}
// Inicializa los textos
void Credits::iniTexts() { // NOLINT(readability-convert-member-functions-to-static)
auto* loc = Locale::get();
void Credits::iniTexts() {
const auto* loc = Locale::get();
texts_.clear();
texts_.push_back({.label = "", .color = static_cast<Uint8>(PaletteColor::WHITE)});
+5 -4
View File
@@ -2,9 +2,10 @@
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include <cstdint> // Para uint8_t
#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
@@ -24,7 +25,7 @@ class Credits : public Scene {
private:
// --- Tipos anidados ---
enum class State {
enum class State : std::uint8_t {
REVEALING_TEXT,
PAUSE_1,
REVEALING_TEXT_2,
+3 -3
View File
@@ -169,10 +169,10 @@ void Ending::updateState(float delta_time) {
}
// Inicializa los textos
void Ending::iniTexts() { // NOLINT(readability-convert-member-functions-to-static)
void Ending::iniTexts() {
// Vector con los textos (traducidos según el idioma activo)
std::vector<TextAndPosition> texts;
auto* loc = Locale::get();
const auto* loc = Locale::get();
// Escena #0
texts.push_back({.caption = loc->get("ending.t0"), .pos = 32});
@@ -273,7 +273,7 @@ void Ending::iniPics() {
}
// Inicializa las escenas
void Ending::iniScenes() { // NOLINT(readability-convert-member-functions-to-static)
void Ending::iniScenes() {
// Variable para los tiempos
int trigger;
constexpr int LAPSE = 80;
+5 -4
View File
@@ -2,9 +2,10 @@
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include <cstdint> // Para uint8_t
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "game/scenes/scene.hpp" // Para Scene
class Sprite; // lines 8-8
@@ -24,7 +25,7 @@ class Ending : public Scene {
private:
// --- Enumeraciones ---
enum class State {
enum class State : std::uint8_t {
WARMING_UP,
SCENE_0,
SCENE_1,
+5 -6
View File
@@ -31,9 +31,8 @@ Ending2::Ending2()
// Inicializa el vector de colores
const std::vector<std::string> COLORS = {"white", "yellow", "cyan", "green", "magenta", "red", "blue", "black"};
for (const auto& color : COLORS) {
colors_.push_back(stringToColor(color));
}
colors_.reserve(COLORS.size());
std::ranges::transform(COLORS, std::back_inserter(colors_), [](const auto& color) { return stringToColor(color); });
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK)); // Cambia el color del borde
iniSpriteList(); // Inicializa la lista de sprites
@@ -392,7 +391,7 @@ void Ending2::placeSprites() const {
}
// Crea los sprites con las texturas con los textos
void Ending2::createSpriteTexts() { // NOLINT(readability-convert-member-functions-to-static)
void Ending2::createSpriteTexts() {
// Crea los sprites de texto a partir de la lista
for (size_t i = 0; i < sprite_list_.size(); ++i) {
auto text = Resource::Cache::get()->getText("smb2");
@@ -401,7 +400,7 @@ void Ending2::createSpriteTexts() { // NOLINT(readability-convert-member-functi
std::string txt = sprite_list_[i];
std::ranges::replace(txt, '_', ' '); // Reemplaza '_' por ' '
if (txt == "player") {
txt = Locale::get()->get("ending2.jaildoctor"); // NOLINT(readability-static-accessed-through-instance) Reemplaza "player" por nombre localizado
txt = Locale::get()->get("ending2.jaildoctor"); // Reemplaza "player" por nombre localizado
}
// Calcula las dimensiones del texto
@@ -433,7 +432,7 @@ void Ending2::createSpriteTexts() { // NOLINT(readability-convert-member-functi
}
// Crea los sprites con las texturas con los textos del final
void Ending2::createTexts() { // NOLINT(readability-convert-member-functions-to-static)
void Ending2::createTexts() {
// Crea los primeros textos
std::vector<std::string> list;
list.emplace_back(Locale::get()->get("ending2.starring"));
+5 -4
View File
@@ -2,9 +2,10 @@
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include <cstdint> // Para uint8_t
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "core/rendering/sprite/dissolve_sprite.hpp" // Para SurfaceDissolveSprite
#include "game/scenes/scene.hpp" // Para Scene
@@ -25,7 +26,7 @@ class Ending2 : public Scene {
private:
// --- Enumeraciones ---
enum class EndingState : int {
enum class EndingState : std::uint8_t {
PRE_CREDITS, // Estado previo a los créditos
CREDITS, // Estado de los créditos
POST_CREDITS, // Estado posterior a los créditos
+22 -27
View File
@@ -2,7 +2,8 @@
#include <SDL3/SDL.h>
#include <cmath> // Para std::sqrt, std::min
#include <cmath> // Para std::sqrt, std::min
#include <numeric> // Para std::accumulate
#include <utility>
#include <vector> // Para vector
@@ -213,10 +214,10 @@ void Game::handleEvent(const SDL_Event& event) {
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)
Notifier::get()->show({Locale::get()->get("game.editor_disabled")});
} else {
GameControl::enter_editor();
Notifier::get()->show({Locale::get()->get("game.editor_enabled")}); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get("game.editor_enabled")});
}
} 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());
@@ -261,9 +262,9 @@ void Game::handleInput() {
}
// Input de pausa solo en estado PLAYING
if (Input::get()->checkAction(InputAction::PAUSE, Input::DO_NOT_ALLOW_REPEAT)) { // NOLINT(readability-static-accessed-through-instance)
if (Input::get()->checkAction(InputAction::PAUSE, Input::DO_NOT_ALLOW_REPEAT)) {
togglePause();
Notifier::get()->show({paused_ ? Locale::get()->get("game.paused") : Locale::get()->get("game.running")}); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({paused_ ? Locale::get()->get("game.paused") : Locale::get()->get("game.running")});
}
GlobalInputs::handle();
@@ -542,7 +543,7 @@ void Game::renderPostFadeEnding() {
static void toggleCheat(Options::Cheat::State& cheat, const std::string& label) {
cheat = (cheat == Options::Cheat::State::ENABLED) ? Options::Cheat::State::DISABLED : Options::Cheat::State::ENABLED;
const bool ENABLED = (cheat == Options::Cheat::State::ENABLED);
Notifier::get()->show({label + (ENABLED ? Locale::get()->get("game.enabled") : Locale::get()->get("game.disabled"))}, Notifier::Style::DEFAULT, -1, true); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({label + (ENABLED ? Locale::get()->get("game.enabled") : Locale::get()->get("game.disabled"))}, Notifier::Style::DEFAULT, -1, true);
}
// Pone la información de debug en pantalla
@@ -573,7 +574,7 @@ void Game::renderDebugInfo() {
}
// Comprueba los eventos
void Game::handleDebugEvents(const SDL_Event& event) { // NOLINT(readability-convert-member-functions-to-static)
void Game::handleDebugEvents(const SDL_Event& event) {
if (event.type == SDL_EVENT_KEY_DOWN && static_cast<int>(event.key.repeat) == 0) {
switch (event.key.key) {
case SDLK_R:
@@ -597,21 +598,21 @@ void Game::handleDebugEvents(const SDL_Event& event) { // NOLINT(readability-co
break;
case SDLK_1:
toggleCheat(Options::cheats.infinite_lives, Locale::get()->get("game.cheat_infinite_lives")); // NOLINT(readability-static-accessed-through-instance)
toggleCheat(Options::cheats.infinite_lives, Locale::get()->get("game.cheat_infinite_lives"));
player_->setColor();
break;
case SDLK_2:
toggleCheat(Options::cheats.invincible, Locale::get()->get("game.cheat_invincible")); // NOLINT(readability-static-accessed-through-instance)
toggleCheat(Options::cheats.invincible, Locale::get()->get("game.cheat_invincible"));
player_->setColor();
break;
case SDLK_3:
toggleCheat(Options::cheats.jail_is_open, Locale::get()->get("game.cheat_jail_open")); // NOLINT(readability-static-accessed-through-instance)
toggleCheat(Options::cheats.jail_is_open, Locale::get()->get("game.cheat_jail_open"));
break;
case SDLK_7:
Notifier::get()->show({Locale::get()->get("achievements.header"), Locale::get()->get("achievements.c11")}, Notifier::Style::CHEEVO, -1, false, "F7"); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Locale::get()->get("achievements.header"), Locale::get()->get("achievements.c11")}, Notifier::Style::CHEEVO, -1, false, "F7");
break;
case SDLK_0: {
@@ -620,7 +621,7 @@ void Game::handleDebugEvents(const SDL_Event& event) { // NOLINT(readability-co
invincible_before_debug_ = (Options::cheats.invincible == Options::Cheat::State::ENABLED);
}
Debug::get()->toggleEnabled();
Notifier::get()->show({Debug::get()->isEnabled() ? Locale::get()->get("game.debug_enabled") : Locale::get()->get("game.debug_disabled")}); // NOLINT(readability-static-accessed-through-instance)
Notifier::get()->show({Debug::get()->isEnabled() ? Locale::get()->get("game.debug_enabled") : Locale::get()->get("game.debug_disabled")});
room_->redrawMap();
if (Debug::get()->isEnabled()) {
Options::cheats.invincible = Options::Cheat::State::ENABLED;
@@ -727,7 +728,7 @@ auto Game::changeRoom(const std::string& room_path) -> bool {
}
// Verifica que exista el fichero que se va a cargar
if (!Resource::List::get()->get(room_path).empty()) { // NOLINT(readability-static-accessed-through-instance)
if (!Resource::List::get()->get(room_path).empty()) {
// Crea un objeto habitación nuevo a partir del fichero
room_ = std::make_shared<Room>(room_path, scoreboard_data_);
@@ -829,7 +830,7 @@ void Game::killPlayer() {
}
// Pone el color del marcador en función del color del borde de la habitación
void Game::setScoreBoardColor() { // NOLINT(readability-convert-member-functions-to-static)
void Game::setScoreBoardColor() {
// Obtiene el color del borde
const Uint8 BORDER_COLOR = room_->getBorderColor();
@@ -865,14 +866,8 @@ auto Game::checkEndGame() -> bool {
// Obtiene la cantidad total de items que hay en el mapeado del juego
auto Game::getTotalItems() -> int {
int items = 0;
auto rooms = Resource::Cache::get()->getRooms();
for (const auto& room : rooms) {
items += room.room->items.size();
}
return items;
const auto& rooms = Resource::Cache::get()->getRooms();
return static_cast<int>(std::accumulate(rooms.begin(), rooms.end(), size_t{0}, [](size_t acc, const auto& room) { return acc + room.room->items.size(); }));
}
// Pone el juego en pausa
@@ -910,12 +905,12 @@ void Game::checkRestoringJail(float delta_time) {
}
// Inicializa las estadísticas
void Game::initStats() { // NOLINT(readability-convert-member-functions-to-static)
void Game::initStats() {
stats_->init();
}
// Crea la textura con el nombre de la habitación
void Game::fillRoomNameTexture() { // NOLINT(readability-convert-member-functions-to-static)
void Game::fillRoomNameTexture() {
// Pone la textura como destino de renderizado
auto previuos_renderer = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(room_name_surface_);
@@ -932,7 +927,7 @@ void Game::fillRoomNameTexture() { // NOLINT(readability-convert-member-functio
}
// Comprueba algunos logros
void Game::checkSomeCheevos() { // NOLINT(readability-convert-member-functions-to-static)
void Game::checkSomeCheevos() {
auto* cheevos = Cheevos::get();
// Logros sobre la cantidad de items
@@ -966,7 +961,7 @@ void Game::checkSomeCheevos() { // NOLINT(readability-convert-member-functions-
}
// Comprueba los logros de completar el juego
void Game::checkEndGameCheevos() { // NOLINT(readability-convert-member-functions-to-static)
void Game::checkEndGameCheevos() {
auto* cheevos = Cheevos::get();
// "Complete the game"
@@ -990,7 +985,7 @@ void Game::checkEndGameCheevos() { // NOLINT(readability-convert-member-functio
}
// Inicializa al jugador
void Game::initPlayer(const Player::SpawnData& spawn_point, std::shared_ptr<Room> room) { // NOLINT(readability-convert-member-functions-to-static)
void Game::initPlayer(const Player::SpawnData& spawn_point, std::shared_ptr<Room> room) {
const bool IGNORE_INPUT = player_ != nullptr && player_->getIgnoreInput();
std::string player_animations = Player::skinToAnimationPath(Options::game.player_skin);
const Player::Data PLAYER{.spawn_data = spawn_point, .animations_path = player_animations, .room = std::move(room)};
+3 -2
View File
@@ -2,6 +2,7 @@
#include <SDL3/SDL.h>
#include <cstdint> // Para uint8_t
#include <initializer_list> // Para initializer_list
#include <memory> // Para shared_ptr
#include <string> // Para string
@@ -19,12 +20,12 @@ class Surface;
class Game : public Scene {
public:
// --- Estructuras ---
enum class Mode {
enum class Mode : std::uint8_t {
DEMO,
GAME
};
enum class State {
enum class State : std::uint8_t {
PLAYING, // Normal gameplay
BLACK_SCREEN, // Black screen after death (0.30s)
GAME_OVER, // Intermediate state before changing scene
+7 -8
View File
@@ -38,9 +38,8 @@ GameOver::GameOver()
// Inicializa el vector de colores (de brillante a oscuro para fade)
const std::vector<std::string> COLORS = {"white", "yellow", "cyan", "green", "magenta", "red", "blue", "black"};
for (const auto& color : COLORS) {
colors_.push_back(stringToColor(color));
}
colors_.reserve(COLORS.size());
std::ranges::transform(COLORS, std::back_inserter(colors_), [](const auto& color) { return stringToColor(color); });
color_ = colors_.back(); // Empieza en black
}
@@ -68,8 +67,8 @@ void GameOver::render() {
auto text = Resource::Cache::get()->getText("smb2");
// Escribe el texto de GAME OVER
auto* loc = Locale::get();
text->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, TEXT_Y, loc->get("game_over.title"), 1, color_); // NOLINT(readability-static-accessed-through-instance)
const auto* loc = Locale::get();
text->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, TEXT_Y, loc->get("game_over.title"), 1, color_);
// Dibuja los sprites (ya posicionados en el constructor, solo ajustamos Y)
player_sprite_->setPosY(TEXT_Y + SPRITE_Y_OFFSET);
@@ -79,11 +78,11 @@ void GameOver::render() {
// Escribe el texto con las habitaciones y los items
const std::string ITEMS_TEXT = std::to_string(Options::stats.items / 100) + std::to_string((Options::stats.items % 100) / 10) + std::to_string(Options::stats.items % 10);
const std::string ROOMS_TEXT = std::to_string(Options::stats.rooms / 100) + std::to_string((Options::stats.rooms % 100) / 10) + std::to_string(Options::stats.rooms % 10);
text->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, TEXT_Y + ITEMS_Y_OFFSET, loc->get("game_over.items") + ITEMS_TEXT, 1, color_); // NOLINT(readability-static-accessed-through-instance)
text->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, TEXT_Y + ROOMS_Y_OFFSET, loc->get("game_over.rooms") + ROOMS_TEXT, 1, color_); // NOLINT(readability-static-accessed-through-instance)
text->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, TEXT_Y + ITEMS_Y_OFFSET, loc->get("game_over.items") + ITEMS_TEXT, 1, color_);
text->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, TEXT_Y + ROOMS_Y_OFFSET, loc->get("game_over.rooms") + ROOMS_TEXT, 1, color_);
// Escribe el texto con "Tu peor pesadilla"
text->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, TEXT_Y + NIGHTMARE_TITLE_Y_OFFSET, loc->get("game_over.worst_nightmare"), 1, color_); // NOLINT(readability-static-accessed-through-instance)
text->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, TEXT_Y + NIGHTMARE_TITLE_Y_OFFSET, loc->get("game_over.worst_nightmare"), 1, color_);
text->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, GameCanvas::CENTER_X, TEXT_Y + NIGHTMARE_TEXT_Y_OFFSET, Options::stats.worst_nightmare, 1, color_);
// Vuelca el contenido del renderizador en pantalla
+4 -3
View File
@@ -2,8 +2,9 @@
#include <SDL3/SDL.h>
#include <memory> // Para shared_ptr
#include <vector> // Para vector
#include <cstdint> // Para uint8_t
#include <memory> // Para shared_ptr
#include <vector> // Para vector
#include "game/scenes/scene.hpp" // Para Scene
class AnimatedSprite; // lines 7-7
@@ -21,7 +22,7 @@ class GameOver : public Scene {
private:
// --- Enumeraciones ---
enum class State {
enum class State : std::uint8_t {
WAITING, // Espera inicial antes de empezar
FADE_IN, // Fade in de colores desde black
DISPLAY, // Mostrando contenido con color brillante
+4 -4
View File
@@ -67,7 +67,7 @@ void LoadingScreen::handleInput() {
}
// Inicializa el array de índices de líneas (imita el direccionamiento de memoria del Spectrum)
void LoadingScreen::initLineIndexArray() { // NOLINT(readability-convert-member-functions-to-static)
void LoadingScreen::initLineIndexArray() {
for (int i = 0; i < MONO_TOTAL_LINES; ++i) {
if (i < 64) { // Primer bloque de 2K
line_index_[i] = ((i % 8) * 8) + (i / 8);
@@ -181,7 +181,7 @@ void LoadingScreen::updateState(float delta_time) {
}
// Gestiona la carga monocromática (time-based simplificado)
void LoadingScreen::updateMonoLoad(float delta_time) {
void LoadingScreen::updateMonoLoad(float /*delta_time*/) {
// Calcular progreso lineal (0.0 - 1.0)
float progress = state_time_ / LOADING_MONO_DURATION;
progress = std::min(progress, 1.0F);
@@ -234,7 +234,7 @@ void LoadingScreen::updateMonoLoad(float delta_time) {
}
// Gestiona la carga en color
void LoadingScreen::updateColorLoad(float delta_time) {
void LoadingScreen::updateColorLoad(float /*delta_time*/) {
// Calcular progreso lineal (0.0 - 1.0)
float progress = state_time_ / LOADING_COLOR_DURATION;
progress = std::min(progress, 1.0F);
@@ -440,7 +440,7 @@ void LoadingScreen::renderBorder() {
}
// Escribe el nombre del programa
void LoadingScreen::printProgramName() { // NOLINT(readability-convert-member-functions-to-static)
void LoadingScreen::printProgramName() {
auto previous_renderer = Screen::get()->getRendererSurface();
Screen::get()->setRendererSurface(screen_surface_);
program_sprite_->render();
+5 -4
View File
@@ -2,8 +2,9 @@
#include <SDL3/SDL.h>
#include <array> // Para std::array
#include <memory> // Para shared_ptr
#include <array> // Para std::array
#include <cstdint> // Para uint8_t
#include <memory> // Para shared_ptr
#include "game/scenes/scene.hpp" // Para Scene
#include "utils/delta_timer.hpp" // Para DeltaTimer
@@ -24,7 +25,7 @@ class LoadingScreen : public Scene {
private:
// --- Enumeraciones ---
// Estados de la secuencia de carga
enum class State {
enum class State : std::uint8_t {
SILENT1, // Pausa inicial antes de empezar
HEADER1, // Cabecera
DATA1, // Datos
@@ -37,7 +38,7 @@ class LoadingScreen : public Scene {
};
// Tipos de borde para la pantalla de carga
enum class Border {
enum class Border : std::uint8_t {
NONE,
YELLOW_AND_BLUE,
RED_AND_CYAN,
+7 -11
View File
@@ -66,7 +66,7 @@ void Logo::handleInput() {
}
// Gestiona el logo de JAILGAME
void Logo::updateJAILGAMES(float delta_time) {
void Logo::updateJAILGAMES(float /*delta_time*/) {
// Solo actualizar durante el estado JAILGAMES_SLIDE_IN
if (state_ != State::JAILGAMES_SLIDE_IN) {
return;
@@ -91,7 +91,7 @@ void Logo::updateJAILGAMES(float delta_time) {
}
// Calcula el índice de color según el progreso (0.0-1.0)
auto Logo::getColorIndex(float progress) const -> int { // NOLINT(readability-convert-member-functions-to-static)
auto Logo::getColorIndex(float progress) const -> int {
// Asegurar que progress esté en el rango [0.0, 1.0]
progress = std::clamp(progress, 0.0F, 1.0F);
@@ -209,7 +209,7 @@ void Logo::update() {
}
// Dibuja en pantalla
void Logo::render() { // NOLINT(readability-convert-member-functions-to-static)
void Logo::render() {
// Prepara para empezar a dibujar en la textura de juego
Screen::get()->start();
Screen::get()->clearSurface(static_cast<Uint8>(PaletteColor::BLACK));
@@ -238,9 +238,6 @@ void Logo::endSection() {
break;
case SceneManager::Options::LOGO_TO_LOADING_SCREEN:
SceneManager::current = SceneManager::Scene::LOADING_SCREEN;
break;
default:
SceneManager::current = SceneManager::Scene::LOADING_SCREEN;
break;
@@ -248,7 +245,7 @@ void Logo::endSection() {
}
// Inicializa el vector de colores
void Logo::initColors() { // NOLINT(readability-convert-member-functions-to-static)
void Logo::initColors() {
// Inicializa el vector de colores
const std::vector<Uint8> COLORS = {
static_cast<Uint8>(PaletteColor::BLACK),
@@ -259,13 +256,12 @@ void Logo::initColors() { // NOLINT(readability-convert-member-functions-to-sta
static_cast<Uint8>(PaletteColor::CYAN),
static_cast<Uint8>(PaletteColor::YELLOW),
static_cast<Uint8>(PaletteColor::BRIGHT_WHITE)};
for (const auto& color : COLORS) {
color_.push_back(color);
}
color_.reserve(COLORS.size());
std::ranges::copy(COLORS, std::back_inserter(color_));
}
// Crea los sprites de cada linea
void Logo::initSprites() { // NOLINT(readability-convert-member-functions-to-static)
void Logo::initSprites() {
const float WIDTH = jailgames_surface_->getWidth();
jailgames_initial_x_.reserve(jailgames_surface_->getHeight());
+2 -1
View File
@@ -2,6 +2,7 @@
#include <SDL3/SDL.h>
#include <cstdint> // Para uint8_t
#include <functional> // Para std::function
#include <memory> // Para shared_ptr
#include <vector> // Para vector
@@ -17,7 +18,7 @@ class Logo : public Scene {
using EasingFunction = std::function<float(float)>; // Función de easing (permite lambdas)
// --- Enumeraciones ---
enum class State {
enum class State : std::uint8_t {
INITIAL, // Espera inicial
JAILGAMES_SLIDE_IN, // Las líneas de JAILGAMES se deslizan hacia el centro
SINCE_1998_FADE_IN, // Aparición gradual del texto "Since 1998"
+18 -31
View File
@@ -51,7 +51,7 @@ Title::Title()
}
// Destructor
Title::~Title() { // NOLINT(modernize-use-equals-default)
Title::~Title() {
loading_screen_surface_->resetSubPalette();
title_surface_->resetSubPalette();
}
@@ -164,7 +164,7 @@ void Title::handleMainMenuKeyPress(SDL_Keycode key) {
}
// Comprueba las entradas
void Title::handleInput(float delta_time) {
void Title::handleInput(float /*delta_time*/) {
Input::get()->update();
// Permitir cancelar remap con ESC/CANCEL
@@ -452,7 +452,7 @@ void Title::iterate() {
}
// Crea y rellena la textura para mostrar los logros
void Title::createCheevosTexture() { // NOLINT(readability-convert-member-functions-to-static)
void Title::createCheevosTexture() {
// Define la zona central del menu (entre el logo y la marquesina)
constexpr int MENU_ZONE_Y = 73; // Top of menu zone
constexpr int MENU_ZONE_HEIGHT = 102; // Height of menu zone
@@ -476,7 +476,7 @@ void Title::createCheevosTexture() { // NOLINT(readability-convert-member-funct
cheevos_surface_->clear(CHEEVOS_BG_COLOR);
// Escribe la lista de logros en la textura
const std::string CHEEVOS_OWNER = Locale::get()->get("title.projects"); // NOLINT(readability-static-accessed-through-instance)
const std::string CHEEVOS_OWNER = Locale::get()->get("title.projects");
const std::string CHEEVOS_LIST_CAPTION = CHEEVOS_OWNER + " (" + std::to_string(Cheevos::get()->getTotalUnlockedAchievements()) + " / " + std::to_string(Cheevos::get()->size()) + ")";
int pos = 2;
TEXT->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, cheevos_surface_->getWidth() / 2, pos, CHEEVOS_LIST_CAPTION, 1, stringToColor("bright_green"));
@@ -544,7 +544,7 @@ void Title::renderMainMenu() {
const int TOTAL_HEIGHT = 3 * SPACING; // 3 espacios entre 4 items
const int START_Y = MENU_CENTER_Y - (TOTAL_HEIGHT / 2);
auto* loc = Locale::get();
const auto* loc = Locale::get();
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y, loc->get("title.menu.play"), 1, COLOR);
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y + SPACING, loc->get("title.menu.keyboard"), 1, COLOR);
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y + (2 * SPACING), loc->get("title.menu.joystick"), 1, COLOR);
@@ -642,7 +642,7 @@ auto Title::isKeyValid(SDL_Scancode scancode) -> bool {
}
// Verifica si una tecla ya fue usada en pasos anteriores
auto Title::isKeyDuplicate(SDL_Scancode scancode, int current_step) -> bool { // NOLINT(readability-convert-member-functions-to-static)
auto Title::isKeyDuplicate(SDL_Scancode scancode, int current_step) -> bool {
for (int i = 0; i < current_step; i++) {
if (temp_keys_[i] == scancode) {
return true;
@@ -652,21 +652,8 @@ auto Title::isKeyDuplicate(SDL_Scancode scancode, int current_step) -> bool { /
}
// Retorna el nombre de la accion para el paso actual
auto Title::getActionName(int step) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
switch (step) {
case 0:
return "LEFT";
case 1:
return "RIGHT";
case 2:
return "JUMP";
default:
return "UNKNOWN";
}
}
// Aplica y guarda las teclas redefinidas
void Title::applyKeyboardRemap() { // NOLINT(readability-convert-member-functions-to-static)
void Title::applyKeyboardRemap() {
// Guardar las nuevas teclas en Options::controls
Options::keyboard_controls.key_left = temp_keys_[0];
Options::keyboard_controls.key_right = temp_keys_[1];
@@ -696,7 +683,7 @@ void Title::renderKeyboardRemap() const {
const int START_Y = MENU_CENTER_Y - (2 * TEXT_SIZE); // Centrado aproximado
// Mensaje principal: "PRESS KEY FOR [ACTION]" o "KEYS DEFINED" si completado
auto* loc = Locale::get();
const auto* loc = Locale::get();
if (remap_step_ >= 3) {
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y, loc->get("title.keys.defined"), 1, COLOR);
} else {
@@ -707,17 +694,17 @@ void Title::renderKeyboardRemap() const {
const int KEYS_START_Y = START_Y + (2 * LINE_SPACING);
if (remap_step_ > 0) {
const std::string LEFT_KEY = SDL_GetScancodeName(temp_keys_[0]);
const std::string LEFT_MSG = loc->get("title.keys.label0") + LEFT_KEY; // NOLINT(readability-static-accessed-through-instance)
const std::string LEFT_MSG = loc->get("title.keys.label0") + LEFT_KEY;
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, KEYS_START_Y, LEFT_MSG, 1, COLOR);
}
if (remap_step_ > 1) {
const std::string RIGHT_KEY = SDL_GetScancodeName(temp_keys_[1]);
const std::string RIGHT_MSG = loc->get("title.keys.label1") + RIGHT_KEY; // NOLINT(readability-static-accessed-through-instance)
const std::string RIGHT_MSG = loc->get("title.keys.label1") + RIGHT_KEY;
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, KEYS_START_Y + LINE_SPACING, RIGHT_MSG, 1, COLOR);
}
if (remap_step_ >= 3) {
const std::string JUMP_KEY = SDL_GetScancodeName(temp_keys_[2]);
const std::string JUMP_MSG = loc->get("title.keys.label2") + JUMP_KEY; // NOLINT(readability-static-accessed-through-instance)
const std::string JUMP_MSG = loc->get("title.keys.label2") + JUMP_KEY;
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, KEYS_START_Y + (2 * LINE_SPACING), JUMP_MSG, 1, COLOR);
}
@@ -744,7 +731,7 @@ void Title::renderJoystickRemap() const {
const int START_Y = MENU_CENTER_Y - (2 * TEXT_SIZE); // Centrado aproximado
// Mensaje principal: "PRESS BUTTON FOR [ACTION]" o "BUTTONS DEFINED" si completado
auto* loc = Locale::get();
const auto* loc = Locale::get();
if (remap_step_ >= 3) {
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, START_Y, loc->get("title.buttons.defined"), 1, COLOR);
} else {
@@ -755,17 +742,17 @@ void Title::renderJoystickRemap() const {
const int BUTTONS_START_Y = START_Y + (2 * LINE_SPACING);
if (remap_step_ > 0) {
const std::string LEFT_BTN = getButtonName(temp_buttons_[0]);
const std::string LEFT_MSG = loc->get("title.keys.label0") + LEFT_BTN; // NOLINT(readability-static-accessed-through-instance)
const std::string LEFT_MSG = loc->get("title.keys.label0") + LEFT_BTN;
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, BUTTONS_START_Y, LEFT_MSG, 1, COLOR);
}
if (remap_step_ > 1) {
const std::string RIGHT_BTN = getButtonName(temp_buttons_[1]);
const std::string RIGHT_MSG = loc->get("title.keys.label1") + RIGHT_BTN; // NOLINT(readability-static-accessed-through-instance)
const std::string RIGHT_MSG = loc->get("title.keys.label1") + RIGHT_BTN;
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, BUTTONS_START_Y + LINE_SPACING, RIGHT_MSG, 1, COLOR);
}
if (remap_step_ >= 3) {
const std::string JUMP_BTN = getButtonName(temp_buttons_[2]);
const std::string JUMP_MSG = loc->get("title.keys.label2") + JUMP_BTN; // NOLINT(readability-static-accessed-through-instance)
const std::string JUMP_MSG = loc->get("title.keys.label2") + JUMP_BTN;
menu_text_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, PlayArea::CENTER_X, BUTTONS_START_Y + (2 * LINE_SPACING), JUMP_MSG, 1, COLOR);
}
@@ -837,7 +824,7 @@ void Title::handleJoystickRemap(const SDL_Event& event) {
}
// Valida si un botón está duplicado
auto Title::isButtonDuplicate(int button, int current_step) -> bool { // NOLINT(readability-convert-member-functions-to-static)
auto Title::isButtonDuplicate(int button, int current_step) -> bool {
for (int i = 0; i < current_step; ++i) {
if (temp_buttons_[i] == button) {
return true;
@@ -847,7 +834,7 @@ auto Title::isButtonDuplicate(int button, int current_step) -> bool { // NOLINT
}
// Aplica y guarda los botones del gamepad redefinidos
void Title::applyJoystickRemap() { // NOLINT(readability-convert-member-functions-to-static)
void Title::applyJoystickRemap() {
// Guardar los nuevos botones en Options::gamepad_controls
Options::gamepad_controls.button_left = temp_buttons_[0];
Options::gamepad_controls.button_right = temp_buttons_[1];
@@ -861,7 +848,7 @@ void Title::applyJoystickRemap() { // NOLINT(readability-convert-member-functio
}
// Retorna el nombre amigable del botón del gamepad
auto Title::getButtonName(int button) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
auto Title::getButtonName(int button) -> std::string {
// Triggers especiales
if (button == Input::TRIGGER_L2_AS_BUTTON) {
return "L2";
+6 -6
View File
@@ -2,10 +2,11 @@
#include <SDL3/SDL.h>
#include <array> // Para std::array
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include <array> // Para std::array
#include <cstdint> // Para uint8_t
#include <memory> // Para shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "game/scene_manager.hpp" // Para SceneManager::Scene
#include "game/scenes/scene.hpp" // Para Scene
@@ -34,7 +35,7 @@ class Title : public Scene {
bool enabled{false}; // Solo se escriben y mueven si estan habilitadas
};
enum class State {
enum class State : std::uint8_t {
SHOW_LOADING_SCREEN,
FADE_LOADING_SCREEN,
MAIN_MENU,
@@ -88,7 +89,6 @@ class Title : public Scene {
auto isButtonDuplicate(int button, int current_step) -> bool; // Valida si un boton esta duplicado
void applyKeyboardRemap(); // Aplica y guarda las teclas redefinidas
void applyJoystickRemap(); // Aplica y guarda los botones del gamepad redefinidos
static auto getActionName(int step) -> std::string; // Retorna el nombre de la accion (LEFT/RIGHT/JUMP)
static auto getButtonName(int button) -> std::string; // Retorna el nombre amigable del boton del gamepad
void createCheevosTexture(); // Crea y rellena la surface para mostrar los logros
void resetCheevosScroll(); // Resetea el scroll de la lista de logros
+152 -135
View File
@@ -4,6 +4,7 @@
#include <algorithm> // Para ranges::transform
#include <cctype> // Para toupper
#include <numeric> // Para std::accumulate
#include <sstream> // Para std::istringstream
#include <string> // Para string
#include <vector> // Para vector
@@ -65,9 +66,11 @@ auto Console::wrapText(const std::string& text) const -> std::vector<std::string
std::istringstream word_stream(segment);
std::string word;
while (word_stream >> word) {
const std::string TEST = current_line.empty() ? word : (current_line + ' ' + word);
if (text_->length(TEST) <= MAX_PX) {
current_line = TEST;
std::string test = current_line;
if (!test.empty()) { test += ' '; }
test += word;
if (text_->length(test) <= MAX_PX) {
current_line = std::move(test);
} else {
if (!current_line.empty()) { result.push_back(current_line); }
current_line = word;
@@ -164,73 +167,79 @@ void Console::redrawText() {
}
// Actualiza la animación de la consola
void Console::update(float delta_time) { // NOLINT(readability-function-cognitive-complexity)
if (status_ == Status::HIDDEN) {
// Parpadeig del cursor (només quan ACTIVE)
void Console::updateCursorBlink(float delta_time) {
cursor_timer_ += delta_time;
const float THRESHOLD = cursor_visible_ ? CURSOR_ON_TIME : CURSOR_OFF_TIME;
if (cursor_timer_ >= THRESHOLD) {
cursor_timer_ = 0.0F;
cursor_visible_ = !cursor_visible_;
}
}
// Revelat lletra a lletra de msg_lines_ (només quan ACTIVE)
void Console::updateTypewriter(float delta_time) {
const int TOTAL_CHARS = std::accumulate(msg_lines_.begin(), msg_lines_.end(), 0, [](int acc, const auto& line) { return acc + static_cast<int>(line.size()); });
if (typewriter_chars_ >= TOTAL_CHARS) { return; }
typewriter_timer_ += delta_time;
while (typewriter_timer_ >= TYPEWRITER_CHAR_DELAY && typewriter_chars_ < TOTAL_CHARS) {
typewriter_timer_ -= TYPEWRITER_CHAR_DELAY;
++typewriter_chars_;
}
}
// Animació d'altura quan msg_lines_ canvia (només quan ACTIVE i height_ != target_height_)
void Console::updateResizeAnimation(float delta_time) {
if (anim_progress_ == 0.0F) {
// Iniciar animació de resize
anim_start_ = height_;
anim_end_ = target_height_;
}
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 nou tamany (xicoteta: 256×~18-72px)
const float WIDTH = Options::game.width;
surface_ = std::make_shared<Surface>(WIDTH, height_);
sprite_->setSurface(surface_);
}
// Animació RISING/VANISHING (basada en temps amb easing)
void Console::updateOpenCloseAnimation(float delta_time) {
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) { return; }
y_ = anim_end_;
anim_progress_ = 0.0F;
if (status_ == Status::RISING) {
status_ = Status::ACTIVE;
return;
}
status_ = Status::HIDDEN;
// Resetear el missatge una vegada completament oculta
msg_lines_ = {std::string(CONSOLE_NAME) + " " + std::string(CONSOLE_VERSION)};
target_height_ = calcTargetHeight(static_cast<int>(msg_lines_.size()));
}
void Console::update(float delta_time) {
if (status_ == Status::HIDDEN) { return; }
// Parpadeo del cursor (solo cuando activa)
if (status_ == Status::ACTIVE) {
cursor_timer_ += delta_time;
const float THRESHOLD = cursor_visible_ ? CURSOR_ON_TIME : CURSOR_OFF_TIME;
if (cursor_timer_ >= THRESHOLD) {
cursor_timer_ = 0.0F;
cursor_visible_ = !cursor_visible_;
updateCursorBlink(delta_time);
updateTypewriter(delta_time);
if (height_ != target_height_) {
updateResizeAnimation(delta_time);
}
}
// Efecto typewriter: revelar letras una a una (solo cuando ACTIVE)
if (status_ == Status::ACTIVE) {
int total_chars = 0;
for (const auto& line : msg_lines_) { total_chars += static_cast<int>(line.size()); }
if (typewriter_chars_ < total_chars) {
typewriter_timer_ += delta_time;
while (typewriter_timer_ >= TYPEWRITER_CHAR_DELAY && typewriter_chars_ < total_chars) {
typewriter_timer_ -= TYPEWRITER_CHAR_DELAY;
++typewriter_chars_;
}
}
}
// Animación de altura (resize cuando msg_lines_ cambia); solo en ACTIVE
if (status_ == Status::ACTIVE && height_ != target_height_) {
if (anim_progress_ == 0.0F) {
// Iniciar animación de resize
anim_start_ = height_;
anim_end_ = target_height_;
}
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;
surface_ = std::make_shared<Surface>(WIDTH, height_);
sprite_->setSurface(surface_);
}
// Redibujar texto cada frame
redrawText();
// 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;
} 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()));
}
}
updateOpenCloseAnimation(delta_time);
}
SDL_FRect rect = {.x = 0, .y = y_, .w = Options::game.width, .h = height_};
@@ -286,84 +295,94 @@ void Console::toggle() {
}
// Procesa el evento SDL: entrada de texto, Backspace, Enter
void Console::handleEvent(const SDL_Event& event) { // NOLINT(readability-function-cognitive-complexity)
// Insereix caràcters imprimibles a input_line_
void Console::handleTextInput(const SDL_Event& event) {
// Filtrar caràcters de control (tab, newline, etc.)
if (static_cast<unsigned char>(event.text.text[0]) < 32) { return; }
if (static_cast<int>(input_line_.size()) < MAX_LINE_CHARS) {
input_line_ += event.text.text;
}
tab_matches_.clear();
}
// Navega enrere a l'historial (cap a comandes més antigues)
void Console::handleHistoryUp() {
tab_matches_.clear();
if (history_index_ >= static_cast<int>(history_.size()) - 1) { return; }
if (history_index_ == -1) { saved_input_ = input_line_; }
++history_index_;
input_line_ = history_[static_cast<size_t>(history_index_)];
}
// Navega cap al present a l'historial (cap a comandes més recents)
void Console::handleHistoryDown() {
tab_matches_.clear();
if (history_index_ < 0) { return; }
--history_index_;
input_line_ = (history_index_ == -1) ? saved_input_ : history_[static_cast<size_t>(history_index_)];
}
// Autocompletat per TAB: calcula candidats si cal i cicla
void Console::handleTab() {
if (tab_matches_.empty()) {
std::string upper;
for (const unsigned char C : input_line_) { upper += static_cast<char>(std::toupper(C)); }
const size_t SPACE_POS = upper.rfind(' ');
if (SPACE_POS == std::string::npos) {
// Mode comanda: cicla keywords visibles que comencen pel prefix
const auto KEYWORDS = registry_.getVisibleKeywords();
std::ranges::copy_if(KEYWORDS, std::back_inserter(tab_matches_), [&upper](const auto& kw) { return upper.empty() || kw.starts_with(upper); });
} else {
const std::string BASE_CMD = upper.substr(0, SPACE_POS);
const std::string SUB_PREFIX = upper.substr(SPACE_POS + 1);
const auto OPTS = registry_.getCompletions(BASE_CMD);
for (const auto& arg : OPTS) {
if (!SUB_PREFIX.empty() && !std::string_view{arg}.starts_with(SUB_PREFIX)) { continue; }
std::string match = BASE_CMD;
match += ' ';
match += arg;
tab_matches_.push_back(std::move(match));
}
}
tab_index_ = -1;
}
if (tab_matches_.empty()) { return; }
tab_index_ = (tab_index_ + 1) % static_cast<int>(tab_matches_.size());
std::string result = tab_matches_[static_cast<size_t>(tab_index_)];
std::ranges::transform(result, result.begin(), [](char c) { return static_cast<char>(std::tolower(static_cast<unsigned char>(c))); });
input_line_ = result;
}
void Console::handleEvent(const SDL_Event& event) {
if (status_ != Status::ACTIVE) { return; }
if (event.type == SDL_EVENT_TEXT_INPUT) {
// Filtrar caracteres de control (tab, newline, etc.)
if (static_cast<unsigned char>(event.text.text[0]) < 32) { return; }
if (static_cast<int>(input_line_.size()) < MAX_LINE_CHARS) {
input_line_ += event.text.text;
}
tab_matches_.clear();
handleTextInput(event);
return;
}
if (event.type != SDL_EVENT_KEY_DOWN) { return; }
if (event.type == SDL_EVENT_KEY_DOWN) {
switch (event.key.scancode) {
case SDL_SCANCODE_BACKSPACE:
tab_matches_.clear();
if (!input_line_.empty()) { input_line_.pop_back(); }
break;
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER:
processCommand();
break;
case SDL_SCANCODE_UP:
// Navegar hacia atrás en el historial
tab_matches_.clear();
if (history_index_ < static_cast<int>(history_.size()) - 1) {
if (history_index_ == -1) { saved_input_ = input_line_; }
++history_index_;
input_line_ = history_[static_cast<size_t>(history_index_)];
}
break;
case SDL_SCANCODE_DOWN:
// Navegar hacia el presente en el historial
tab_matches_.clear();
if (history_index_ >= 0) {
--history_index_;
input_line_ = (history_index_ == -1)
? saved_input_
: history_[static_cast<size_t>(history_index_)];
}
break;
case SDL_SCANCODE_TAB: {
if (tab_matches_.empty()) {
// Calcular el input actual en mayúsculas
std::string upper;
for (unsigned char c : input_line_) { upper += static_cast<char>(std::toupper(c)); }
const size_t SPACE_POS = upper.rfind(' ');
if (SPACE_POS == std::string::npos) {
// Modo comando: ciclar keywords visibles que empiecen por el prefijo
for (const auto& kw : registry_.getVisibleKeywords()) {
if (upper.empty() || kw.starts_with(upper)) {
tab_matches_.emplace_back(kw);
}
}
} else {
const std::string BASE_CMD = upper.substr(0, SPACE_POS);
const std::string SUB_PREFIX = upper.substr(SPACE_POS + 1);
const auto OPTS = registry_.getCompletions(BASE_CMD);
for (const auto& arg : OPTS) {
if (SUB_PREFIX.empty() || std::string_view{arg}.starts_with(SUB_PREFIX)) {
tab_matches_.emplace_back(BASE_CMD + " " + arg);
}
}
}
tab_index_ = -1;
}
if (tab_matches_.empty()) { break; }
tab_index_ = (tab_index_ + 1) % static_cast<int>(tab_matches_.size());
std::string result = tab_matches_[static_cast<size_t>(tab_index_)];
for (char& c : result) { c = static_cast<char>(std::tolower(static_cast<unsigned char>(c))); }
input_line_ = result;
break;
}
default:
break;
}
switch (event.key.scancode) {
case SDL_SCANCODE_BACKSPACE:
tab_matches_.clear();
if (!input_line_.empty()) { input_line_.pop_back(); }
break;
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER:
processCommand();
break;
case SDL_SCANCODE_UP:
handleHistoryUp();
break;
case SDL_SCANCODE_DOWN:
handleHistoryDown();
break;
case SDL_SCANCODE_TAB:
handleTab();
break;
default:
break;
}
}
@@ -403,9 +422,7 @@ void Console::processCommand() {
// Typewriter: instantáneo si el comando lo requiere, letra a letra si no
if (instant) {
int total = 0;
for (const auto& l : msg_lines_) { total += static_cast<int>(l.size()); }
typewriter_chars_ = total;
typewriter_chars_ = std::accumulate(msg_lines_.begin(), msg_lines_.end(), 0, [](int acc, const auto& l) { return acc + static_cast<int>(l.size()); });
} else {
typewriter_chars_ = 0;
}

Some files were not shown because too many files have changed in this diff Show More