Compare commits

..

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 09:52:28 +02:00
125 changed files with 14671 additions and 16631 deletions
+76 -54
View File
@@ -2,84 +2,106 @@ Checks:
- readability-*
- modernize-*
- performance-*
- bugprone-unchecked-optional-access
- bugprone-sizeof-expression
- bugprone-suspicious-missing-comma
- bugprone-suspicious-index
- bugprone-undefined-memory-manipulation
- bugprone-use-after-move
- bugprone-out-of-bound-access
- bugprone-*
- -readability-identifier-length
- -readability-magic-numbers
- -bugprone-narrowing-conversions
- -performance-enum-size
- -performance-inefficient-string-concatenation
- -bugprone-integer-division
- -bugprone-easily-swappable-parameters
- -modernize-avoid-c-arrays,-warnings-as-errors
- -bugprone-narrowing-conversions
- -modernize-avoid-c-arrays
WarningsAsErrors: '*'
# Solo incluir archivos de tu código fuente (external tiene su propio .clang-tidy)
# Excluye jail_audio.hpp del análisis
HeaderFilterRegex: 'source/(?!core/audio/jail_audio\.hpp|core/rendering/sdl3gpu/.*_spv\.h).*'
# Headers nostres (excloem source/external/ que conté dependències de tercers no editables)
HeaderFilterRegex: 'source/(core|game|utils)/'
FormatStyle: file
CheckOptions:
# Variables locales en snake_case
- { key: readability-identifier-naming.VariableCase, value: lower_case }
# bugprone-empty-catch: aceptar catches vacíos marcados con @INTENTIONAL en un comentario
- { key: bugprone-empty-catch.IgnoreCatchWithKeywords, value: '@INTENTIONAL' }
# Miembros privados en snake_case con sufijo _
# =====================================================================
# 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 }
# Parámetros de función
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
# 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: _ }
# =====================================================================
# MIEMBROS DE CLASE NO-CONST
# =====================================================================
# Privados: snake_case con sufijo _
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
# Miembros protegidos en snake_case con sufijo _
# Protegidos: 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)
# Públicos: 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
- { 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 }
# Constantes globales en UPPER_CASE
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
# Variables constexpr globales en UPPER_CASE
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_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
+105 -52
View File
@@ -3,6 +3,11 @@
cmake_minimum_required(VERSION 3.10)
project(jaildoctors_dilemma VERSION 1.00)
# Tipus de build per defecte (Debug) si no se n'ha especificat cap
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
endif()
# Establecer estándar de C++
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
@@ -11,17 +16,24 @@ set(CMAKE_CXX_STANDARD_REQUIRED True)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# --- GENERACIÓN DE VERSIÓN AUTOMÁTICA ---
find_package(Git QUIET)
if(GIT_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
else()
set(GIT_HASH "unknown")
# Si GIT_HASH se ha pasado desde fuera (p.ej. desde el Makefile via -DGIT_HASH=xxx),
# lo usamos tal cual. Esto evita problemas con Docker/emscripten, donde git aborta por
# "dubious ownership" en el volumen montado. En builds locales sin -DGIT_HASH, se
# resuelve aquí ejecutando git directamente.
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
find_package(Git QUIET)
if(GIT_FOUND)
execute_process(
COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)
endif()
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
set(GIT_HASH "unknown")
endif()
endif()
# Configurar archivo de versión
@@ -31,6 +43,7 @@ configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/versi
set(APP_SOURCES
# Core - Audio
source/core/audio/audio.cpp
source/core/audio/audio_adapter.cpp
# Core - Input
source/core/input/global_inputs.cpp
@@ -87,6 +100,7 @@ set(APP_SOURCES
source/game/gameplay/tilemap_renderer.cpp
# Game - Scenes
source/game/scenes/boot_loader.cpp
source/game/scenes/credits.cpp
source/game/scenes/ending.cpp
source/game/scenes/ending2.cpp
@@ -133,7 +147,7 @@ if(EMSCRIPTEN)
FetchContent_Declare(
SDL3
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
GIT_TAG release-3.2.12
GIT_TAG release-3.4.4
GIT_SHALLOW TRUE
)
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
@@ -151,32 +165,24 @@ if(NOT APPLE AND NOT EMSCRIPTEN)
find_program(GLSLC_EXE NAMES glslc)
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/data/shaders")
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu")
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv")
set(SHADER_POSTFX_VERT_SRC "${SHADERS_DIR}/postfx.vert")
set(SHADER_POSTFX_FRAG_SRC "${SHADERS_DIR}/postfx.frag")
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}"
)
@@ -200,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()
@@ -239,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
@@ -266,29 +271,32 @@ elseif(APPLE)
endif()
elseif(EMSCRIPTEN)
target_compile_definitions(${PROJECT_NAME} PRIVATE EMSCRIPTEN_BUILD)
# -fexceptions: habilita excepcions C++ (fkyaml, std::runtime_error...) — sense això qualsevol throw crida abort()
target_compile_options(${PROJECT_NAME} PRIVATE -fexceptions)
target_link_options(${PROJECT_NAME} PRIVATE
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/data@/data"
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/config@/config"
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt@/gamecontrollerdb.txt"
-fexceptions
-sALLOW_MEMORY_GROWTH=1
-sMAX_WEBGL_VERSION=2
-sINITIAL_MEMORY=67108864
-sASSERTIONS=1
# ASYNCIFY només per permetre emscripten_sleep(0) durant la precàrrega de recursos
# (el bucle principal del joc ja usa SDL3 Callback API, no depèn d'ASYNCIFY).
-sASYNCIFY=1
)
set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".html")
elseif(UNIX AND NOT APPLE)
target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD)
endif()
# Especificar la ubicación del ejecutable (en desktop; a wasm queda a build/wasm/)
if(NOT EMSCRIPTEN)
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
endif()
# --- 5. STATIC ANALYSIS TARGETS ---
# Buscar herramientas de análisis estático
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
find_program(CLANG_FORMAT_EXE NAMES clang-format)
find_program(CPPCHECK_EXE NAMES cppcheck)
# Recopilar todos los archivos fuente para formateo
file(GLOB_RECURSE ALL_SOURCE_FILES
@@ -300,10 +308,11 @@ file(GLOB_RECURSE ALL_SOURCE_FILES
# Excluir directorio external del análisis
list(FILTER ALL_SOURCE_FILES EXCLUDE REGEX ".*/external/.*")
# Para clang-tidy, también excluir jail_audio.hpp
set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES})
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*jail_audio\\.hpp$")
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*_spv\\.h$")
# Para cppcheck, pasar solo .cpp (los headers se procesan transitivamente).
set(CPPCHECK_SOURCES ${ALL_SOURCE_FILES})
list(FILTER CPPCHECK_SOURCES INCLUDE REGEX ".*\\.cpp$")
# Targets de clang-tidy
if(CLANG_TIDY_EXE)
@@ -349,31 +358,75 @@ else()
message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles")
endif()
# --- 6. PACK RESOURCES TARGETS (no en Emscripten: s'utilitza --preload-file) ---
# Target de cppcheck
if(CPPCHECK_EXE)
add_custom_target(cppcheck
COMMAND ${CPPCHECK_EXE}
--enable=warning,style,performance,portability
--std=c++20
--language=c++
--inline-suppr
--suppress=missingIncludeSystem
--suppress=toomanyconfigs
--suppress=*:*/source/external/*
--suppress=*:*/source/core/rendering/sdl3gpu/spv/*
--quiet
-I ${CMAKE_SOURCE_DIR}/source
${CPPCHECK_SOURCES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Running cppcheck..."
)
else()
message(STATUS "cppcheck no encontrado - target 'cppcheck' no disponible")
endif()
# --- 6. EINA STANDALONE: pack_resources (no en Emscripten: s'utilitza --preload-file) ---
# Executable auxiliar que empaqueta `data/` a `resources.pack`.
# No es compila per defecte (EXCLUDE_FROM_ALL). Build explícit:
# cmake --build build --target pack_resources
if(NOT EMSCRIPTEN)
set(PACK_TOOL_SOURCES
${CMAKE_SOURCE_DIR}/tools/pack_resources/pack_resources.cpp
${CMAKE_SOURCE_DIR}/source/core/resources/resource_pack.cpp
add_executable(pack_resources EXCLUDE_FROM_ALL
tools/pack_resources/pack_resources.cpp
source/core/resources/resource_pack.cpp
)
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
add_executable(pack_tool ${PACK_TOOL_SOURCES})
target_include_directories(pack_tool PRIVATE ${CMAKE_SOURCE_DIR}/source)
set_target_properties(pack_tool PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/tools/pack_resources
)
file(GLOB_RECURSE DATA_FILES "${CMAKE_SOURCE_DIR}/data/*")
# Regeneració automàtica de resources.pack en cada build si canvia data/.
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
set(RESOURCE_PACK "${CMAKE_BINARY_DIR}/resources.pack")
add_custom_command(
OUTPUT "${CMAKE_SOURCE_DIR}/resources.pack"
COMMAND $<TARGET_FILE:pack_tool>
"${CMAKE_SOURCE_DIR}/data"
"${CMAKE_SOURCE_DIR}/resources.pack"
DEPENDS pack_tool ${DATA_FILES}
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
COMMENT "Generando resources.pack desde data/..."
OUTPUT ${RESOURCE_PACK}
COMMAND $<TARGET_FILE:pack_resources>
"${CMAKE_SOURCE_DIR}/data"
"${RESOURCE_PACK}"
DEPENDS pack_resources ${DATA_FILES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Empaquetant data/ → resources.pack"
VERBATIM
)
add_custom_target(pack DEPENDS "${CMAKE_SOURCE_DIR}/resources.pack")
add_dependencies(${PROJECT_NAME} pack)
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
add_dependencies(${PROJECT_NAME} resource_pack)
# --- 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()
+226 -69
View File
@@ -3,14 +3,13 @@
# ==============================================================================
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
DIR_SOURCES := $(addsuffix /, $(DIR_ROOT)source)
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
DIR_TOOLS := $(addsuffix /, $(DIR_ROOT)tools)
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
@@ -20,8 +19,6 @@ RESOURCE_FILE := release/windows/jdd.res
# ==============================================================================
# TOOLS
# ==============================================================================
DIR_PACK_TOOL := $(DIR_TOOLS)pack_resources
SHADER_SCRIPT := $(DIR_ROOT)tools/shaders/compile_spirv.sh
SHADER_CMAKE := $(DIR_ROOT)tools/shaders/compile_spirv.cmake
SHADERS_DIR := $(DIR_ROOT)data/shaders
HEADERS_DIR := $(DIR_ROOT)source/core/rendering/sdl3gpu
@@ -35,9 +32,23 @@ endif
# VERSION (extracted from defines.hpp)
# ==============================================================================
ifeq ($(OS),Windows_NT)
VERSION := v$(shell powershell -Command "(Select-String -Path 'source/utils/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value")
VERSION := $(shell powershell -Command "(Select-String -Path 'source/utils/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value")
else
VERSION := v$(shell grep 'constexpr const char\* VERSION' source/utils/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
VERSION := $(shell grep 'constexpr const char\* VERSION' source/utils/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
endif
# ==============================================================================
# GIT HASH (computat al host, passat a CMake via -DGIT_HASH)
# Evita que CMake haja de cridar git des de Docker/emscripten on falla per
# "dubious ownership" del volum muntat.
# ==============================================================================
ifeq ($(OS),Windows_NT)
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>NUL)
else
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>/dev/null)
endif
ifeq ($(GIT_HASH),)
GIT_HASH := unknown
endif
# ==============================================================================
@@ -52,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
@@ -84,35 +95,74 @@ else
UNAME_S := $(shell uname -s)
endif
# ==============================================================================
# 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)
# 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
HAS_NINJA := $(shell ninja --version 2>/dev/null)
ifneq ($(HAS_NINJA),)
CMAKE_GEN := -G "Ninja"
else
CMAKE_GEN :=
endif
endif
# ==============================================================================
# COMPILACIÓN CON CMAKE
# ==============================================================================
all:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build
debug:
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH)
@cmake --build build
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
@@ -120,23 +170,22 @@ else
endif
# ==============================================================================
# REGLAS PARA HERRAMIENTA DE EMPAQUETADO Y RESOURCES.PACK
# EMPAQUETADO DE RECURSOS (build previ de l'eina + execució)
# ==============================================================================
pack_tool:
@$(MAKE) -C $(DIR_PACK_TOOL)
resources.pack: pack_tool
@$(MAKE) -C $(DIR_PACK_TOOL) pack
pack:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target pack_resources
@./build/pack_resources data build/resources.pack
# ==============================================================================
# COMPILACIÓN PARA WINDOWS (RELEASE)
# ==============================================================================
windows_release:
_windows-release:
@echo off
@echo Creando release para Windows - Version: $(VERSION)
# Compila con cmake (genera shaders, resources.pack y ejecutable)
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build
# Crea carpeta de distribución y carpeta temporal 'RELEASE_FOLDER'
@@ -145,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)'"
@@ -164,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
@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)"
@@ -186,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)"
@@ -201,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
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON
@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
@@ -258,11 +343,11 @@ macos_release:
# ==============================================================================
# COMPILACIÓN PARA LINUX (RELEASE)
# ==============================================================================
linux_release:
_linux-release:
@echo "Creando release para Linux - Version: $(VERSION)"
# Compila con cmake (genera shaders, resources.pack y ejecutable)
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build
# Elimina carpeta temporal previa y la recrea (crea dist/ si no existe)
@@ -270,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)"
@@ -291,22 +376,82 @@ linux_release:
wasm:
@echo "Compilando para WebAssembly - Version: $(VERSION)"
docker run --rm \
--user $(shell id -u):$(shell id -g) \
-v $(DIR_ROOT):/src \
-w /src \
emscripten/emsdk:latest \
bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release && cmake --build build/wasm"
bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm"
$(MKDIR) "$(DIST_DIR)/wasm"
cp build/wasm/$(TARGET_NAME).html $(DIST_DIR)/wasm/
cp build/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/
cp build/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/
cp build/wasm/$(TARGET_NAME).data $(DIST_DIR)/wasm/
@echo "Output: $(DIST_DIR)/wasm/"
scp $(DIST_DIR)/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/$(TARGET_NAME).data \
maverick:/home/sergio/gitea/web_jailgames/static/games/jaildoctors-dilemma/wasm/
ssh maverick 'cd /home/sergio/gitea/web_jailgames && ./deploy.sh'
@echo "Deployed to maverick"
# Versió Debug del build wasm: arrenca directament a la GAME (sense logo/loading/title)
# i activa l'editor i la consola. Sortida a dist/wasm-debug/.
wasm-debug:
@echo "Compilando WebAssembly Debug - Version: $(VERSION)"
docker run --rm \
-v $(DIR_ROOT):/src \
-w /src \
emscripten/emsdk:latest \
bash -c "emcmake cmake -S . -B build/wasm-debug -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm-debug"
$(MKDIR) "$(DIST_DIR)/wasm-debug"
cp build/wasm-debug/$(TARGET_NAME).html $(DIST_DIR)/wasm-debug/
cp build/wasm-debug/$(TARGET_NAME).js $(DIST_DIR)/wasm-debug/
cp build/wasm-debug/$(TARGET_NAME).wasm $(DIST_DIR)/wasm-debug/
cp build/wasm-debug/$(TARGET_NAME).data $(DIST_DIR)/wasm-debug/
@echo "Output: $(DIST_DIR)/wasm-debug/"
# ==============================================================================
# ==============================================================================
# CODE QUALITY (delegados a cmake)
# ==============================================================================
format:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target format
format-check:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target format-check
tidy:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target tidy
tidy-fix:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target tidy-fix
cppcheck:
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
@cmake --build build --target cppcheck
# ==============================================================================
# GIT HOOKS
# ==============================================================================
hooks-install:
@git config core.hooksPath .githooks
@echo "Git hooks activats: $(shell pwd)/.githooks"
# DESCARGA DE GAMECONTROLLERDB
# ==============================================================================
controllerdb:
@echo "Descargando gamecontrollerdb.txt..."
curl -fsSL https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt \
-o gamecontrollerdb.txt
@echo "gamecontrollerdb.txt actualizado"
# ==============================================================================
# REGLAS ESPECIALES
# ==============================================================================
# Regla para mostrar la versión actual
show_version:
show-version:
@echo "Version actual: $(VERSION)"
# Regla de ayuda
@@ -318,20 +463,32 @@ 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 windows_release - Crear release para Windows"
@echo " make linux_release - Crear release para Linux"
@echo " make macos_release - Crear release para macOS"
@echo " make wasm - Crear release per a WebAssembly (requereix Docker)"
@echo " make wasm-debug - Crear build Debug per a WebAssembly (entra directe a la GAME)"
@echo ""
@echo " Herramientas:"
@echo " make compile_shaders - Compilar shaders SPIR-V"
@echo " make pack_tool - Compilar herramienta de empaquetado"
@echo " make resources.pack - Generar pack de recursos desde data/"
@echo " make 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:"
@echo " make format - Formatear codigo con clang-format"
@echo " make format-check - Verificar formato sin modificar"
@echo " make tidy - Analisis estatico con clang-tidy"
@echo " make tidy-fix - Analisis estatico con auto-fix"
@echo " make cppcheck - Analisis estatico con cppcheck"
@echo ""
@echo " Otros:"
@echo " make show_version - Mostrar version actual ($(VERSION))"
@echo " make 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 compile_shaders pack_tool resources.pack 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
+3 -12
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)"
@@ -201,7 +192,7 @@ categories:
DEBUG: [MODE, START]
DEBUG MODE: [ON, OFF]
DEBUG START: [HERE, ROOM, POS, SCENE]
DEBUG START SCENE: [LOGO, LOADING, TITLE, CREDITS, GAME, ENDING, ENDING2]
DEBUG START SCENE: [LOGO, LOADING, TITLE, CREDITS, GAME, DEMO, ENDING, ENDING2]
- keyword: ITEMS
handler: cmd_items
@@ -220,9 +211,9 @@ categories:
- keyword: SCENE
handler: cmd_scene
description: Change scene
usage: "SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|ENDING|ENDING2|RESTART]"
usage: "SCENE [LOGO|LOADING|TITLE|CREDITS|GAME|DEMO|ENDING|ENDING2|RESTART]"
completions:
SCENE: [LOGO, LOADING, TITLE, CREDITS, GAME, ENDING, ENDING2, RESTART]
SCENE: [LOGO, LOADING, TITLE, CREDITS, GAME, DEMO, ENDING, ENDING2, RESTART]
- keyword: EDIT
handler: cmd_edit
+3 -2
View File
@@ -8,6 +8,7 @@ title:
keyboard: "2. REDEFINIR TECLES"
joystick: "3. REDEFINIR MANDO"
projects: "4. PROJECTES"
press_to_play: "PREM PER JUGAR"
keys:
prompt0: "PREM UNA TECLA PER A ESQUERRA"
prompt1: "PREM UNA TECLA PER A DRETA"
@@ -103,6 +104,8 @@ achievements:
ui:
press_again_menu: "PREM DE NOU PER TORNAR AL MENÚ"
press_again_exit: "PREM DE NOU PER EIXIR"
gamepad_connected: "CONNECTAT"
gamepad_disconnected: "DESCONNECTAT"
border_enabled: "VORA ACTIVADA"
border_disabled: "VORA DESACTIVADA"
fullscreen_enabled: "PANTALLA COMPLETA ACTIVADA"
@@ -113,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"
+3 -2
View File
@@ -8,6 +8,7 @@ title:
keyboard: "2. REDEFINE KEYBOARD"
joystick: "3. REDEFINE JOYSTICK"
projects: "4. PROJECTS"
press_to_play: "PRESS TO PLAY"
keys:
prompt0: "PRESS KEY FOR LEFT"
prompt1: "PRESS KEY FOR RIGHT"
@@ -103,6 +104,8 @@ achievements:
ui:
press_again_menu: "PRESS AGAIN TO RETURN TO MENU"
press_again_exit: "PRESS AGAIN TO EXIT"
gamepad_connected: "CONNECTED"
gamepad_disconnected: "DISCONNECTED"
border_enabled: "BORDER ENABLED"
border_disabled: "BORDER DISABLED"
fullscreen_enabled: "FULLSCREEN ENABLED"
@@ -113,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.*
+251 -153
View File
File diff suppressed because it is too large Load Diff
+89 -66
View File
@@ -1,19 +1,33 @@
#include "audio.hpp"
#include "core/audio/audio.hpp"
#include <SDL3/SDL.h> // Para SDL_LogInfo, SDL_LogCategory, SDL_G...
#include <SDL3/SDL.h> // Para SDL_GetError, SDL_Init
#include <algorithm> // Para clamp
#include <iostream> // Para std::cout
// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp)
// Implementación de stb_vorbis (debe estar ANTES de incluir jail_audio.hpp).
// clang-format off
#undef STB_VORBIS_HEADER_ONLY
#include "external/stb_vorbis.h"
// 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
#undef C
#undef R
#undef PLAYBACK_MONO
#undef PLAYBACK_LEFT
#undef PLAYBACK_RIGHT
// clang-format on
#include "core/audio/jail_audio.hpp" // Para JA_FadeOutMusic, JA_Init, JA_PauseM...
#include "core/resources/resource_cache.hpp" // Para Resource
#include "game/options.hpp" // Para AudioOptions, audio, MusicOptions
#include "core/audio/audio_adapter.hpp" // Para AudioResource::getMusic/getSound
#include "core/audio/jail_audio.hpp" // Para JA_*
#include "game/options.hpp" // Para Options::audio
// Singleton
Audio* Audio::instance = nullptr;
@@ -22,7 +36,10 @@ Audio* Audio::instance = nullptr;
void Audio::init() { Audio::instance = new Audio(); }
// Libera la instancia
void Audio::destroy() { delete Audio::instance; }
void Audio::destroy() {
delete Audio::instance;
Audio::instance = nullptr;
}
// Obtiene la instancia
auto Audio::get() -> Audio* { return Audio::instance; }
@@ -32,16 +49,21 @@ 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 != nullptr && instance->music_.state == MusicState::PLAYING && Ja::getMusicState() != Ja::MusicState::PLAYING) {
instance->music_.state = MusicState::STOPPED;
}
}
// Reproduce la música
void Audio::playMusic(const std::string& name, const int loop) { // NOLINT(readability-convert-member-functions-to-static)
// Reproduce la música por nombre (con crossfade opcional)
void Audio::playMusic(const std::string& name, const int loop, const int crossfade_ms) {
bool new_loop = (loop != 0);
// Si ya está sonando exactamente la misma pista y mismo modo loop, no hacemos nada
@@ -49,47 +71,63 @@ void Audio::playMusic(const std::string& name, const int loop) { // NOLINT(read
return;
}
// Intentar obtener recurso; si falla, no tocar estado
auto* resource = Resource::Cache::get()->getMusic(name);
if (resource == nullptr) {
// manejo de error opcional
return;
if (!music_enabled_) { return; }
auto* resource = AudioResource::getMusic(name);
if (resource == nullptr) { return; }
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
Ja::crossfadeMusic(resource, crossfade_ms, loop);
} else {
if (music_.state == MusicState::PLAYING) {
Ja::stopMusic();
}
Ja::playMusic(resource, loop);
}
// Si hay algo reproduciéndose, detenerlo primero (si el backend lo requiere)
if (music_.state == MusicState::PLAYING) {
JA_StopMusic(); // sustituir por la función de stop real del API si tiene otro nombre
}
// Llamada al motor para reproducir la nueva pista
JA_PlayMusic(resource, loop);
// Actualizar estado y metadatos después de iniciar con éxito
music_.name = name;
music_.loop = new_loop;
music_.state = MusicState::PLAYING;
}
// Reproduce la música por puntero (con crossfade opcional)
void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms) {
if (!music_enabled_ || music == nullptr) { return; }
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
Ja::crossfadeMusic(music, crossfade_ms, loop);
} else {
if (music_.state == MusicState::PLAYING) {
Ja::stopMusic();
}
Ja::playMusic(music, loop);
}
music_.name.clear(); // nom desconegut quan es passa per punter
music_.loop = (loop != 0);
music_.state = MusicState::PLAYING;
}
// Pausa la música
void Audio::pauseMusic() { // NOLINT(readability-convert-member-functions-to-static)
void Audio::pauseMusic() {
if (music_enabled_ && music_.state == MusicState::PLAYING) {
JA_PauseMusic();
Ja::pauseMusic();
music_.state = MusicState::PAUSED;
}
}
// Continua la música pausada
void Audio::resumeMusic() { // NOLINT(readability-convert-member-functions-to-static)
void Audio::resumeMusic() {
if (music_enabled_ && music_.state == MusicState::PAUSED) {
JA_ResumeMusic();
Ja::resumeMusic();
music_.state = MusicState::PLAYING;
}
}
// Detiene la música
void Audio::stopMusic() { // NOLINT(readability-make-member-function-const)
void Audio::stopMusic() {
if (music_enabled_) {
JA_StopMusic();
Ja::stopMusic();
music_.state = MusicState::STOPPED;
}
}
@@ -97,62 +135,62 @@ void Audio::stopMusic() { // NOLINT(readability-make-member-function-const)
// Reproduce un sonido por nombre
void Audio::playSound(const std::string& name, Group group) const {
if (sound_enabled_) {
JA_PlaySound(Resource::Cache::get()->getSound(name), 0, static_cast<int>(group));
Ja::playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
}
}
// Reproduce un sonido por puntero directo
void Audio::playSound(JA_Sound_t* sound, Group group) const {
if (sound_enabled_) {
JA_PlaySound(sound, 0, static_cast<int>(group));
void Audio::playSound(Ja::Sound* sound, Group group) const {
if (sound_enabled_ && sound != nullptr) {
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;
}
}
// Establece el volumen de los sonidos
// Establece el volumen de los sonidos (float 0.0..1.0)
void Audio::setSoundVolume(float sound_volume, Group group) const {
if (sound_enabled_) {
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
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));
}
}
// Establece el volumen de la música
// Establece el volumen de la música (float 0.0..1.0)
void Audio::setMusicVolume(float music_volume) const {
if (music_enabled_) {
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
const float CONVERTED_VOLUME = music_volume * Options::audio.volume;
JA_SetMusicVolume(CONVERTED_VOLUME);
Ja::setMusicVolume(CONVERTED_VOLUME);
}
}
@@ -172,24 +210,9 @@ void Audio::enable(bool value) {
// Inicializa SDL Audio
void Audio::initSDLAudio() {
if (!SDL_Init(SDL_INIT_AUDIO)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL_AUDIO could not initialize! SDL Error: %s", SDL_GetError());
std::cout << "SDL_AUDIO could not initialize! SDL Error: " << SDL_GetError() << '\n';
} else {
JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2);
Ja::init(FREQUENCY, SDL_AUDIO_S16LE, 2);
enable(Options::audio.enabled);
// Aplicar estado de música y sonido guardado en las opciones.
// enable() ya aplica los volúmenes, pero no toca music_enabled_/sound_enabled_.
// Si alguno está desactivado, hay que forzar el volumen a 0 en el backend.
if (!Options::audio.music.enabled) {
setMusicVolume(0.0F); // music_enabled_=true aún → llega a JA
enableMusic(false);
}
if (!Options::audio.sound.enabled) {
setSoundVolume(0.0F); // sound_enabled_=true aún → llega a JA
enableSound(false);
}
std::cout << "\n** AUDIO SYSTEM **\n";
std::cout << "Audio system initialized successfully\n";
}
}
}
+38 -15
View File
@@ -1,28 +1,41 @@
#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
// presentació (menús, notificacions) usa les helpers toPercent/fromPercent
// per mostrar 0100 a l'usuari.
class Audio {
public:
// --- Enums ---
enum class Group : int {
enum class Group : std::int8_t {
ALL = -1, // Todos los grupos
GAME = 0, // Sonidos del juego
INTERFACE = 1 // Sonidos de la interfaz
};
enum class MusicState {
enum class MusicState : std::uint8_t {
PLAYING, // Reproduciendo música
PAUSED, // Música pausada
STOPPED, // Música detenida
};
// --- Constantes ---
static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo
static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo
static constexpr int FREQUENCY = 48000; // Frecuencia de audio
static constexpr float MAX_VOLUME = 1.0F; // Volumen máximo (float 0..1)
static constexpr float MIN_VOLUME = 0.0F; // Volumen mínimo (float 0..1)
static constexpr float VOLUME_STEP = 0.05F; // Pas estàndard per a UI (5%)
static constexpr int FREQUENCY = 48000; // Frecuencia de audio
static constexpr int DEFAULT_CROSSFADE_MS = 1500; // Duració del crossfade per defecte (ms)
// --- Singleton ---
static void init(); // Inicializa el objeto Audio
@@ -34,21 +47,31 @@ class Audio {
static void update(); // Actualización del sistema de audio
// --- Control de música ---
void playMusic(const std::string& name, int loop = -1); // Reproducir música en bucle
void pauseMusic(); // Pausar reproducción de música
void resumeMusic(); // Continua la música pausada
void stopMusic(); // Detener completamente la música
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional)
void playMusic(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 ---
// --- Control de volumen (API interna: float 0.0..1.0) ---
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
void setMusicVolume(float volume) const; // Ajustar volumen de música
// --- Helpers de conversió per a la capa de presentació ---
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
static 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;
}
// --- Configuración general ---
void enable(bool value); // Establecer estado general
void toggleEnabled() { enabled_ = !enabled_; } // Alternar estado general
@@ -94,4 +117,4 @@ class Audio {
bool enabled_{true}; // Estado general del audio
bool sound_enabled_{true}; // Estado de los efectos de sonido
bool music_enabled_{true}; // Estado de la música
};
};
+13
View File
@@ -0,0 +1,13 @@
#include "core/audio/audio_adapter.hpp"
#include "core/resources/resource_cache.hpp"
namespace AudioResource {
auto getMusic(const std::string& name) -> Ja::Music* {
return Resource::Cache::get()->getMusic(name);
}
auto getSound(const std::string& name) -> Ja::Sound* {
return Resource::Cache::get()->getSound(name);
}
} // namespace AudioResource
+19
View File
@@ -0,0 +1,19 @@
#pragma once
// --- Audio Resource Adapter ---
// 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
namespace Ja {
struct Music;
struct Sound;
} // namespace Ja
namespace AudioResource {
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
+67 -75
View File
@@ -2,13 +2,16 @@
#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
#include "core/rendering/render_info.hpp" // Para RenderInfo
#include "core/rendering/screen.hpp" // Para Screen
#include "core/system/global_events.hpp" // Para GlobalEvents::consumeGamepadButtonPressed
#include "game/options.hpp" // Para Options, options, OptionsVideo, Section
#include "game/scene_manager.hpp" // Para SceneManager
#include "game/ui/console.hpp" // Para Console
@@ -20,17 +23,14 @@ namespace GlobalInputs {
// Funciones internas
namespace {
void handleQuit() {
#ifdef __EMSCRIPTEN__
// A la versió web no es pot eixir del joc
return;
#else
// En la escena GAME el comportamiento es siempre el mismo (con o sin modo kiosko)
// En la escena GAME el comportamiento es siempre el mismo (con o sin modo kiosko):
// Escape torna al menu principal. Això també és vàlid en la versió web.
if (SceneManager::current == SceneManager::Scene::GAME) {
const std::string CODE = "PRESS AGAIN TO RETURN TO MENU";
if (stringInVector(Notifier::get()->getCodes(), CODE)) {
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;
}
@@ -45,12 +45,17 @@ namespace GlobalInputs {
return;
}
#ifdef __EMSCRIPTEN__
// A la versió web no es pot eixir del joc des de fora de l'escena GAME
// (el navegador gestiona la pestanya; Escape no tanca res).
return;
#else
// Comportamiento normal fuera del modo kiosko
const std::string CODE = "PRESS AGAIN TO EXIT";
if (stringInVector(Notifier::get()->getCodes(), CODE)) {
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__
}
@@ -75,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() {
@@ -105,104 +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)
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;
}
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 (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
@@ -274,7 +266,7 @@ namespace GlobalInputs {
handleNextShaderPreset();
break;
case InputAction::TOGGLE_SUPERSAMPLING:
case InputAction::NEXT_SHADER:
handleNextShader();
break;
+106 -84
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
@@ -10,11 +11,46 @@
#include "game/options.hpp" // Para Options::controls
// Emscripten-only: SDL 3.4+ ja no casa el GUID dels mandos de Chrome Android
// amb gamecontrollerdb (el gamepad.id d'Android no porta Vendor/Product, el
// parser extreu valors escombraries, el GUID resultant no està a la db i el
// gamepad queda obert amb un mapping incorrecte). Com el W3C Gamepad API
// garanteix el layout estàndard quan el navegador reporta mapping=="standard",
// injectem un mapping SDL amb eixe layout per al GUID del joystick abans
// d'obrir-lo com gamepad. Fora d'Emscripten és un no-op.
static void installWebStandardMapping(SDL_JoystickID jid) {
#ifdef __EMSCRIPTEN__
SDL_GUID guid = SDL_GetJoystickGUIDForID(jid);
char guidStr[33];
SDL_GUIDToString(guid, guidStr, sizeof(guidStr));
const char* name = SDL_GetJoystickNameForID(jid);
if (!name || !*name) name = "Standard Gamepad";
char mapping[512];
SDL_snprintf(mapping, sizeof(mapping),
"%s,%s,"
"a:b0,b:b1,x:b2,y:b3,"
"leftshoulder:b4,rightshoulder:b5,"
"lefttrigger:b6,righttrigger:b7,"
"back:b8,start:b9,"
"leftstick:b10,rightstick:b11,"
"dpup:b12,dpdown:b13,dpleft:b14,dpright:b15,"
"guide:b16,"
"leftx:a0,lefty:a1,rightx:a2,righty:a3,"
"platform:Emscripten",
guidStr,
name);
SDL_AddGamepadMapping(mapping);
#else
(void)jid;
#endif
}
// Singleton
Input* Input::instance = nullptr;
// 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);
}
@@ -70,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;
@@ -91,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;
@@ -143,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
@@ -164,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.
@@ -180,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
@@ -210,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;
}
@@ -220,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];
@@ -287,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
@@ -334,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
@@ -376,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);
@@ -390,27 +400,38 @@ void Input::update() { // NOLINT(readability-convert-member-functions-to-static
// --- MANDOS ---
for (const auto& gamepad : gamepads_) {
for (auto& binding : gamepad->bindings) {
bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0;
for (auto& [action, state] : gamepad->bindings) {
bool is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(state.button))) != 0;
// JUMP accepta qualsevol dels 4 botons frontals (South/East/North/West)
if (action == Action::JUMP) {
is_down_now = is_down_now ||
SDL_GetGamepadButton(gamepad->pad, SDL_GAMEPAD_BUTTON_SOUTH) ||
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
binding.second.just_pressed = button_is_down_now && !binding.second.is_held;
binding.second.is_held = button_is_down_now;
state.just_pressed = is_down_now && !state.is_held;
state.is_held = is_down_now;
}
}
}
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) {
std::cerr << "Error al abrir el gamepad: " << SDL_GetError() << '\n';
@@ -421,10 +442,16 @@ auto Input::addGamepad(int device_index) -> std::string { // NOLINT(readability
auto name = gamepad->name;
std::cout << "Gamepad connected (" << name << ")" << '\n';
gamepads_.push_back(std::move(gamepad));
return name + " CONNECTED";
// Aplica els bindings d'Options al nou gamepad (en hot-plug/wasm el ctor
// ja ha cridat applyGamepadBindingsFromOptions però llavors gamepads_
// estava buit i no s'ha fet res).
applyGamepadBindingsFromOptions();
return name;
}
auto Input::removeGamepad(SDL_JoystickID id) -> std::string { // NOLINT(readability-convert-member-functions-to-static)
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;
});
@@ -433,13 +460,13 @@ auto Input::removeGamepad(SDL_JoystickID id) -> std::string { // NOLINT(readabi
std::string name = (*it)->name;
std::cout << "Gamepad disconnected (" << name << ")" << '\n';
gamepads_.erase(it);
return name + " DISCONNECTED";
return name;
}
std::cerr << "No se encontró el gamepad con ID " << id << '\n';
return {};
}
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;
@@ -453,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;
}
+20 -2
View File
@@ -51,16 +51,34 @@ class Input {
std::string path; // Ruta del dispositivo
std::unordered_map<Action, ButtonState> bindings; // Mapa de acciones a estados de botón
// Recorta el nombre del mando hasta el primer '(' o '[' y elimina espacios finales.
// Evita nombres como "Retroid Controller (vendor: 1001) ..." en las notificaciones.
static auto trimName(const char* raw) -> std::string {
std::string s(raw != nullptr ? raw : "");
const auto POS = s.find_first_of("([");
if (POS != std::string::npos) { s.erase(POS); }
while (!s.empty() && s.back() == ' ') { s.pop_back(); }
return s;
}
explicit Gamepad(SDL_Gamepad* gamepad)
: pad(gamepad),
instance_id(SDL_GetJoystickID(SDL_GetGamepadJoystick(gamepad))),
name(std::string(SDL_GetGamepadName(gamepad))),
name(trimName(SDL_GetGamepadName(gamepad))),
path(std::string(SDL_GetGamepadPath(pad))),
bindings{
// Movimiento del jugador
{Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}},
{Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
{Action::JUMP, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}}} {}
{Action::JUMP, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}},
// Botó BACK del mando → sortir escena / tancar joc
{Action::EXIT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_BACK)}},
{Action::CANCEL, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_BACK)}},
// Botó START del mando → pausa
{Action::PAUSE, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_START)}},
// Shoulders → paleta següent / mode d'ordenació de paleta següent
{Action::NEXT_PALETTE, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)}},
{Action::NEXT_PALETTE_SORT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER)}}} {}
~Gamepad() {
if (pad != nullptr) {
+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
+6 -8
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
@@ -119,9 +118,10 @@ void RenderInfo::render() const {
// Fuente: preferir la de la consola si está disponible
auto text_obj = (Console::get() != nullptr) ? Console::get()->getText() : Screen::get()->getText();
// Posición Y: debajo de la consola + offset animado propio
// Posición Y: debajo de la consola + altura animada de la pila de notificaciones + offset animado propio
const int CONSOLE_Y = (Console::get() != nullptr) ? Console::get()->getVisibleHeight() : 0;
const int Y = CONSOLE_Y + static_cast<int>(y_);
const int NOTIFIER_Y = (Notifier::get() != nullptr) ? Notifier::get()->getVisibleHeight() : 0;
const int Y = CONSOLE_Y + NOTIFIER_Y + static_cast<int>(y_);
// Rectángulo de fondo: ancho completo, alto ajustado al texto
const SDL_FRect RECT = {
@@ -141,17 +141,15 @@ void RenderInfo::render() const {
MSG_COLOR);
}
// Activa o desactiva el overlay y notifica a Notifier del cambio de offset
// Activa o desactiva el overlay (la posición Y se calcula pull-side en render())
void RenderInfo::toggle() {
switch (status_) {
case Status::HIDDEN:
status_ = Status::RISING;
Screen::get()->updateZoomFactor();
if (Notifier::get() != nullptr) { Notifier::get()->addYOffset(HEIGHT); }
break;
case Status::ACTIVE:
status_ = Status::VANISHING;
if (Notifier::get() != nullptr) { Notifier::get()->removeYOffset(HEIGHT); }
break;
default:
break;
+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 };
+132 -62
View File
@@ -1,6 +1,10 @@
#include "core/rendering/screen.hpp"
#include <SDL3/SDL.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
#endif
#include <algorithm> // Para max, min, transform
#include <cctype> // Para toupper
@@ -11,23 +15,80 @@
#include <iterator> // Para istreambuf_iterator, operator==
#include <string> // Para char_traits, string, operator+, operator==
#include "core/input/mouse.hpp" // Para updateCursorVisibility
#include "core/rendering/render_info.hpp" // Para RenderInfo
#include "core/input/mouse.hpp" // Para updateCursorVisibility
#include "core/rendering/render_info.hpp" // Para RenderInfo
#ifndef __EMSCRIPTEN__
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // Para SDL3GPUShader (no suportat a WebGL2)
#endif
#include "core/rendering/surface.hpp" // Para Surface, readPalFile
#include "core/rendering/text.hpp" // Para Text
#include "core/resources/resource_cache.hpp" // Para Resource
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
#include "core/resources/resource_list.hpp" // Para Asset, AssetType
#include "game/options.hpp" // Para Options, options, OptionsVideo, Border
#include "game/ui/console.hpp" // Para Console
#include "game/ui/notifier.hpp" // Para Notifier
#include "core/rendering/surface.hpp" // Para Surface, readPalFile
#include "core/rendering/text.hpp" // Para Text
#include "core/resources/resource_cache.hpp" // Para Resource
#include "core/resources/resource_helper.hpp" // Para ResourceHelper
#include "core/resources/resource_list.hpp" // Para Asset, AssetType
#include "game/options.hpp" // Para Options, options, OptionsVideo, Border
#include "game/ui/console.hpp" // Para Console
#include "game/ui/notifier.hpp" // Para Notifier
// [SINGLETON]
Screen* Screen::screen = nullptr;
#ifdef __EMSCRIPTEN__
// ============================================================================
// Restauració del canvas en wasm/Emscripten
// ============================================================================
//
// Problema: SDL3 + Emscripten no notifica de manera fiable els canvis de mida
// del canvas HTML. En concret:
// - SDL_EVENT_WINDOW_LEAVE_FULLSCREEN no s'emet quan l'usuari surt de
// fullscreen amb Esc/F11 (libsdl-org/SDL#13300).
// - SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED i SDL_EVENT_DISPLAY_ORIENTATION
// tampoc es disparen de manera fiable (libsdl-org/SDL#11389).
// - Resultat: en sortir de fullscreen el canvas queda a la mida correcta
// però SDL encara creu que està en fullscreen amb la resolució anterior,
// i el joc es veu minúscul fins que l'usuari força un refresh manual
// (p. ex., canviant el mode d'escalat amb F7).
//
// Solució: registrem callbacks natius d'Emscripten (fullscreenchange,
// orientationchange) que re-sincronitzen SDL amb l'estat real del navegador.
// NO registrem resize callback: en mòbil el scroll fa que el navegador oculti/
// mostri la barra d'URL, disparant un resize del DOM per cada scroll.
// Tots delegen en Screen::handleCanvasResized(), que crida setVideoMode()
// amb l'estat de fullscreen actualitzat — això és el que realment restaura
// la finestra SDL perquè dins setVideoMode es crida SDL_SetWindowFullscreen,
// que és imprescindible per treure SDL del seu estat intern de fullscreen.
//
// Els callbacks diferixen la feina amb emscripten_async_call(0ms) perquè
// quan l'event es dispara el navegador encara no ha acabat de redimensionar
// el canvas: llegir la mida en aquest instant donaria un valor obsolet.
// Posposar al següent tick del event loop garanteix que el canvas ja està
// estable quan actuem.
//
// Referències:
// - https://github.com/libsdl-org/SDL/issues/13300
// - https://github.com/libsdl-org/SDL/issues/11389
// - https://discourse.libsdl.org/t/sdl-emscripten-allow-resize-events-on-fullscreen-windows/66279
// ============================================================================
namespace {
void deferredCanvasResize(void* /*user_data*/) {
if (Screen::get() != nullptr) { Screen::get()->handleCanvasResized(); }
}
auto onEmFullscreenChange(int /*event_type*/, const EmscriptenFullscreenChangeEvent* event, void* /*user_data*/) -> EM_BOOL {
// Actualitzem Options::video.fullscreen amb l'estat real del navegador
// abans de diferir la restauració: quan l'usuari surt amb Esc no passem
// per setVideoMode() i l'estat intern quedaria desincronitzat.
Options::video.fullscreen = (event != nullptr && event->isFullscreen != 0);
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
auto onEmOrientationChange(int /*event_type*/, const EmscriptenOrientationChangeEvent* /*event*/, void* /*user_data*/) -> EM_BOOL {
emscripten_async_call(deferredCanvasResize, nullptr, 0);
return EM_FALSE;
}
} // namespace
#endif
// [SINGLETON] Crearemos el objeto con esta función estática
void Screen::init() {
Screen::screen = new Screen();
@@ -168,6 +229,20 @@ void Screen::toggleVideoMode() {
setVideoMode(Options::video.fullscreen);
}
// Re-sincronitza SDL amb l'estat real del canvas del navegador. L'invoquen els
// callbacks natius d'Emscripten definits a dalt (vegeu el bloc de documentació
// just després dels includes) quan es detecta un fullscreenchange, resize o
// orientationchange. Delegar a setVideoMode() és el que realment restaura la
// finestra: per sota crida SDL_SetWindowFullscreen, imprescindible perquè
// SDL tregui el seu estat intern de fullscreen quan l'usuari ha sortit amb
// Esc sense passar pel nostre toggleVideoMode(). No fem res fora d'emscripten
// perquè en desktop SDL ja emet SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED correctament.
void Screen::handleCanvasResized() {
#ifdef __EMSCRIPTEN__
setVideoMode(Options::video.fullscreen);
#endif
}
// Reduce el tamaño de la ventana
auto Screen::decWindowZoom() -> bool {
if (static_cast<int>(Options::video.fullscreen) == 0) {
@@ -295,16 +370,16 @@ void Screen::adjustWindowSize() {
window_width_ = Options::game.width + (Options::video.border.enabled ? Options::video.border.width * 2 : 0);
window_height_ = Options::game.height + (Options::video.border.enabled ? Options::video.border.height * 2 : 0);
// Reservamos memoria una sola vez.
// Si el buffer es más pequeño que la superficie, crash asegurado.
border_pixel_buffer_.resize(static_cast<size_t>(window_width_ * window_height_));
// border_surface_ sempre té el tamany complet del borde (game + 2*border_w/h),
// independentment de si el borde està visible o no. El buffer ARGB que l'ombra
// ha de ser ALMENYS tan gran com la surface; si no, toARGBBuffer() escriu fora
// de bounds i corromp el heap (bug latent a desktop fins a disparar-lo un toggleBorder).
const size_t FULL_BORDER_BUFFER_SIZE =
static_cast<size_t>(Options::game.width + (Options::video.border.width * 2)) *
static_cast<size_t>(Options::game.height + (Options::video.border.height * 2));
border_pixel_buffer_.resize(FULL_BORDER_BUFFER_SIZE);
game_pixel_buffer_.resize(static_cast<size_t>(Options::game.width * Options::game.height));
// border_pixel_buffer_ es el buffer que se sube a la GPU (tamaño total ventana).
if (Options::video.border.enabled) {
border_pixel_buffer_.resize(static_cast<size_t>(window_width_ * window_height_));
}
// Lógica de centrado y redimensionado de ventana SDL
if (static_cast<int>(Options::video.fullscreen) == 0) {
int old_w;
@@ -372,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_);
@@ -400,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_);
}
@@ -419,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));
}
@@ -513,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{
@@ -621,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();
@@ -643,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;
@@ -743,12 +798,27 @@ auto Screen::initSDLVideo() -> bool {
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
SDL_SetRenderVSync(renderer_, Options::video.vertical_sync ? 1 : SDL_RENDERER_VSYNC_DISABLED);
registerEmscriptenEventCallbacks();
std::cout << "Video system initialized successfully\n";
return true;
}
// Registra els callbacks natius d'Emscripten que restauren el canvas quan
// SDL3 no emet els events equivalents. Fora d'Emscripten és un no-op.
// Vegeu el bloc de documentació a dalt del fitxer per al context complet.
void Screen::registerEmscriptenEventCallbacks() {
#ifdef __EMSCRIPTEN__
// NO registrem resize callback. En mòbil, fer scroll fa que el navegador
// oculti/mostri la barra d'URL disparant un resize del DOM per cada scroll,
// i això portaria a cridar setVideoMode innecessàriament. Alineat amb CC i CCAE.
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
#endif
}
// Crea el objeto de texto
void Screen::createText() { // NOLINT(readability-convert-member-functions-to-static)
void Screen::createText() {
// Carga la surface de la fuente directamente del archivo
auto surface = std::make_shared<Surface>(Resource::List::get()->get("aseprite.gif"));
+16 -18
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,
};
@@ -38,6 +38,7 @@ class Screen {
// Video y ventana
void setVideoMode(bool mode); // Establece el modo de video
void toggleVideoMode(); // Cambia entre pantalla completa y ventana
void handleCanvasResized(); // Restaura el canvas quan SDL3 no reporta el canvi (emscripten only: sortida de fullscreen amb Esc, rotació, resize); no-op fora d'emscripten
void toggleIntegerScale(); // Alterna entre activar y desactivar el escalado entero
void toggleVSync(); // Alterna entre activar y desactivar el V-Sync
auto decWindowZoom() -> bool; // Reduce el tamaño de la ventana
@@ -63,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)
@@ -88,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
@@ -125,18 +122,19 @@ class Screen {
static Screen* screen;
// Métodos privados
void renderNotifications() const; // Dibuja las notificaciones
void adjustWindowSize(); // Calcula el tamaño de la ventana
void adjustRenderLogicalSize(); // Ajusta el tamaño lógico del renderizador
void surfaceToTexture(); // Copia la surface a la textura
void textureToRenderer(); // Copia la textura al renderizador
void renderOverlays(); // Renderiza todos los overlays
void initShaders(); // Inicializa los shaders
void applyCurrentPostFXPreset(); // Aplica los parámetros del preset PostFX actual al backend
void applyCurrentCrtPiPreset(); // Aplica los parámetros del preset CrtPi actual al backend
void getDisplayInfo(); // Obtiene información sobre la pantalla
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
void createText(); // Crea el objeto de texto
void renderNotifications() const; // Dibuja las notificaciones
void adjustWindowSize(); // Calcula el tamaño de la ventana
void adjustRenderLogicalSize(); // Ajusta el tamaño lógico del renderizador
void surfaceToTexture(); // Copia la surface a la textura
void textureToRenderer(); // Copia la textura al renderizador
void renderOverlays(); // Renderiza todos los overlays
void initShaders(); // Inicializa los shaders
void applyCurrentPostFXPreset(); // Aplica los parámetros del preset PostFX actual al backend
void applyCurrentCrtPiPreset(); // Aplica los parámetros del preset CrtPi actual al backend
void getDisplayInfo(); // Obtiene información sobre la pantalla
auto initSDLVideo() -> bool; // Arranca SDL VIDEO y crea la ventana
void registerEmscriptenEventCallbacks(); // Registra els callbacks natius per restaurar el canvas en wasm (no-op fora d'emscripten)
void createText(); // Crea el objeto de texto
// Constructor y destructor
Screen();
File diff suppressed because it is too large Load Diff
@@ -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
@@ -0,0 +1,2 @@
DisableFormat: true
SortIncludes: Never
@@ -0,0 +1,4 @@
# source/core/rendering/sdl3gpu/spv/.clang-tidy
Checks: '-*'
WarningsAsErrors: ''
HeaderFilterRegex: ''
@@ -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
@@ -219,12 +219,12 @@ AnimatedSprite::AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
: MovingSprite(std::move(surface), pos) {
// animations_ queda buit (protegit per el guard de animate())
if (surface_) {
clip_ = {.x = 0, .y = 0, .w = surface_->getWidth(), .h = surface_->getHeight()};
clip_ = {.x = 0, .y = 0, .w = static_cast<float>(surface_->getWidth()), .h = static_cast<float>(surface_->getHeight())};
}
}
// 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;
@@ -35,8 +35,8 @@ auto DissolveSprite::computePixelRank(int col, int row, int frame_h, DissolveDir
DissolveSprite::DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
: AnimatedSprite(std::move(surface), pos) {
if (surface_) {
const int W = static_cast<int>(surface_->getWidth());
const int H = static_cast<int>(surface_->getHeight());
const int W = surface_->getWidth();
const int H = surface_->getHeight();
surface_display_ = std::make_shared<Surface>(W, H);
surface_display_->setTransparentColor(surface_->getTransparentColor());
surface_display_->clear(surface_->getTransparentColor());
@@ -47,8 +47,8 @@ DissolveSprite::DissolveSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
DissolveSprite::DissolveSprite(const AnimationResource& data)
: AnimatedSprite(data) {
if (surface_) {
const int W = static_cast<int>(surface_->getWidth());
const int H = static_cast<int>(surface_->getHeight());
const int W = surface_->getWidth();
const int H = surface_->getHeight();
surface_display_ = std::make_shared<Surface>(W, H);
surface_display_->setTransparentColor(surface_->getTransparentColor());
// Inicialitza tots els píxels com a transparents
@@ -75,8 +75,8 @@ void DissolveSprite::rebuildDisplaySurface() {
auto src_data = surface_->getSurfaceData();
auto dst_data = surface_display_->getSurfaceData();
const int SRC_W = static_cast<int>(src_data->width);
const int DST_W = static_cast<int>(dst_data->width);
const int SRC_W = src_data->width;
const int DST_W = dst_data->width;
const Uint8 TRANSPARENT = surface_->getTransparentColor();
// Esborra frame anterior si ha canviat
@@ -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 };
+1 -1
View File
@@ -19,7 +19,7 @@ Sprite::Sprite() = default;
Sprite::Sprite(std::shared_ptr<Surface> surface)
: surface_(std::move(surface)),
pos_{0.0F, 0.0F, surface_->getWidth(), surface_->getHeight()},
pos_{0.0F, 0.0F, static_cast<float>(surface_->getWidth()), static_cast<float>(surface_->getHeight())},
clip_(pos_) {}
// Muestra el sprite por pantalla
+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;
+78 -118
View File
@@ -104,7 +104,7 @@ Surface::Surface(const std::string& file_path)
}
// Carga una superficie desde un archivo
auto Surface::loadSurface(const std::string& file_path) -> SurfaceData { // NOLINT(readability-convert-member-functions-to-static)
auto Surface::loadSurface(const std::string& file_path) -> SurfaceData {
// Load file using ResourceHelper (supports both filesystem and pack)
std::vector<Uint8> buffer = Resource::Helper::loadFile(file_path);
if (buffer.empty()) {
@@ -129,7 +129,7 @@ auto Surface::loadSurface(const std::string& file_path) -> SurfaceData { // NOL
// Crear y devolver directamente el objeto SurfaceData
printWithDots("Surface : ", file_path.substr(file_path.find_last_of("\\/") + 1), "[ LOADED ]");
return {static_cast<float>(w), static_cast<float>(h), pixels};
return {static_cast<int>(w), static_cast<int>(h), pixels};
}
// Carga una paleta desde un archivo
@@ -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)
const size_t TOTAL_PIXELS = surface_data_->width * surface_data_->height;
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
}
@@ -165,39 +165,39 @@ void Surface::putPixel(int x, int y, Uint8 color) { // NOLINT(readability-conve
}
// Obtiene el color de un pixel de la surface_data
auto Surface::getPixel(int x, int y) -> Uint8 { return surface_data_->data.get()[x + (y * static_cast<int>(surface_data_->width))]; }
auto Surface::getPixel(int x, int y) -> Uint8 { return surface_data_->data.get()[x + (y * surface_data_->width)]; }
// Dibuja un rectangulo relleno
void Surface::fillRect(const SDL_FRect* rect, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
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);
float x_end = std::min(rect->x + rect->w, surface_data_->width);
float y_end = std::min(rect->y + rect->h, surface_data_->height);
float x_end = std::min(rect->x + rect->w, static_cast<float>(surface_data_->width));
float y_end = std::min(rect->y + rect->h, static_cast<float>(surface_data_->height));
// Rellenar fila a fila con memset (memoria contigua por fila)
Uint8* data_ptr = surface_data_->data.get();
const int SURF_WIDTH = static_cast<int>(surface_data_->width);
const int SURF_WIDTH = surface_data_->width;
const int ROW_WIDTH = static_cast<int>(x_end) - static_cast<int>(x_start);
for (int y = static_cast<int>(y_start); y < static_cast<int>(y_end); ++y) {
std::memset(data_ptr + (y * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
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);
float x_end = std::min(rect->x + rect->w, surface_data_->width);
float y_end = std::min(rect->y + rect->h, surface_data_->height);
float x_end = std::min(rect->x + rect->w, static_cast<float>(surface_data_->width));
float y_end = std::min(rect->y + rect->h, static_cast<float>(surface_data_->height));
// Dibujar bordes horizontales con memset (líneas contiguas en memoria)
Uint8* data_ptr = surface_data_->data.get();
const int SURF_WIDTH = static_cast<int>(surface_data_->width);
const int SURF_WIDTH = surface_data_->width;
const int ROW_WIDTH = static_cast<int>(x_end) - static_cast<int>(x_start);
std::memset(data_ptr + (static_cast<int>(y_start) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
std::memset(data_ptr + ((static_cast<int>(y_end) - 1) * SURF_WIDTH) + static_cast<int>(x_start), color, ROW_WIDTH);
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) {
@@ -211,90 +211,56 @@ void Surface::drawRectBorder(const SDL_FRect* rect, Uint8 color) { // NOLINT(re
}
}
// Dibuja una linea
void Surface::drawLine(float x1, float y1, float x2, float y2, Uint8 color) { // NOLINT(readability-convert-member-functions-to-static)
// Calcula las diferencias
float dx = std::abs(x2 - x1);
float dy = std::abs(y2 - y1);
// Dibuja una linea (Bresenham en enteros)
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));
const int IY2 = static_cast<int>(std::lround(y2));
// Determina la dirección del incremento
float sx = (x1 < x2) ? 1 : -1;
float sy = (y1 < y2) ? 1 : -1;
const int DX = std::abs(IX2 - ix1);
const int DY = std::abs(IY2 - iy1);
const int SX = (ix1 < IX2) ? 1 : -1;
const int SY = (iy1 < IY2) ? 1 : -1;
float err = dx - dy;
const int SURF_W = surface_data_->width;
const int SURF_H = surface_data_->height;
Uint8* data_ptr = surface_data_->data.get();
int err = DX - DY;
while (true) {
// Asegúrate de no dibujar fuera de los límites de la superficie
if (x1 >= 0 && x1 < surface_data_->width && y1 >= 0 && y1 < surface_data_->height) {
surface_data_->data.get()[static_cast<size_t>(x1 + (y1 * surface_data_->width))] = color;
if (ix1 >= 0 && ix1 < SURF_W && iy1 >= 0 && iy1 < SURF_H) {
data_ptr[ix1 + (iy1 * SURF_W)] = color;
}
// Si alcanzamos el punto final, salimos
if (x1 == x2 && y1 == y2) {
if (ix1 == IX2 && iy1 == IY2) {
break;
}
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x1 += sx;
if (e2 > -DY) {
err -= DY;
ix1 += SX;
}
if (e2 < dx) {
err += dx;
y1 += sy;
if (e2 < DX) {
err += DX;
iy1 += SY;
}
}
}
void Surface::render(float dx, float dy, float sx, float sy, float w, float h) { // NOLINT(readability-make-member-function-const)
auto surface_data = Screen::get()->getRendererSurface()->getSurfaceData();
// Limitar la región para evitar accesos fuera de rango en origen
w = std::min(w, surface_data_->width - sx);
h = std::min(h, surface_data_->height - sy);
// Limitar la región para evitar accesos fuera de rango en destino
w = std::min(w, surface_data->width - dx);
h = std::min(h, surface_data->height - dy);
const Uint8* src_ptr = surface_data_->data.get();
Uint8* dst_ptr = surface_data->data.get();
for (int iy = 0; iy < h; ++iy) {
for (int ix = 0; ix < w; ++ix) {
// Verificar que las coordenadas de destino están dentro de los límites
if (int dest_x = dx + ix; dest_x >= 0 && dest_x < surface_data->width) {
if (int dest_y = dy + iy; dest_y >= 0 && dest_y < surface_data->height) {
int src_x = sx + ix;
int src_y = sy + iy;
Uint8 color = src_ptr[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
if (color != static_cast<Uint8>(transparent_color_)) {
dst_ptr[static_cast<size_t>(dest_x + (dest_y * surface_data->width))] = sub_palette_[color];
}
}
}
}
}
}
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
float sx = (src_rect != nullptr) ? src_rect->x : 0;
float sy = (src_rect != nullptr) ? src_rect->y : 0;
float w = (src_rect != nullptr) ? src_rect->w : surface_data_->width;
float h = (src_rect != nullptr) ? src_rect->h : surface_data_->height;
float w = (src_rect != nullptr) ? src_rect->w : static_cast<float>(surface_data_->width);
float h = (src_rect != nullptr) ? src_rect->h : static_cast<float>(surface_data_->height);
// Limitar la región para evitar accesos fuera de rango en origen
w = std::min(w, surface_data_->width - sx);
h = std::min(h, surface_data_->height - sy);
w = std::min(w, surface_data_dest->width - x);
h = std::min(h, surface_data_dest->height - y);
// Limitar la región para evitar accesos fuera de rango en destino
w = std::min(w, surface_data_dest->width - x);
h = std::min(h, surface_data_dest->height - y);
// Limitar la región para evitar accesos fuera de rango (origen y destino)
w = std::min(w, static_cast<float>(surface_data_->width) - sx);
h = std::min(h, static_cast<float>(surface_data_->height) - sy);
w = std::min(w, static_cast<float>(surface_data_dest->width - x));
h = std::min(h, static_cast<float>(surface_data_dest->height - y));
// Renderiza píxel por píxel aplicando el flip si es necesario
const Uint8* src_ptr = surface_data_->data.get();
@@ -322,7 +288,7 @@ void Surface::render(int x, int y, SDL_FRect* src_rect, SDL_FlipMode flip) { //
}
// Helper para calcular coordenadas con flip
void Surface::calculateFlippedCoords(int ix, int iy, float sx, float sy, float w, float h, SDL_FlipMode flip, int& src_x, int& src_y) {
void Surface::calculateFlippedCoords(int ix, int iy, int sx, int sy, int w, int h, SDL_FlipMode flip, int& src_x, int& src_y) {
src_x = (flip == SDL_FLIP_HORIZONTAL) ? (sx + w - 1 - ix) : (sx + ix);
src_y = (flip == SDL_FLIP_VERTICAL) ? (sy + h - 1 - iy) : (sy + iy);
}
@@ -418,11 +384,11 @@ void Surface::renderWithColorReplace(int x, int y, Uint8 source_color, Uint8 tar
continue; // Saltar píxeles fuera del rango del destino
}
// Copia el píxel si no es transparente
// Copia el píxel si no es transparente; aplica sub_palette_ como el resto de render*
Uint8 color = surface_data_->data.get()[static_cast<size_t>(src_x + (src_y * surface_data_->width))];
if (color != static_cast<Uint8>(transparent_color_)) {
surface_data->data[dest_x + (dest_y * surface_data->width)] =
(color == source_color) ? target_color : color;
(color == source_color) ? target_color : sub_palette_[color];
}
}
}
@@ -449,17 +415,17 @@ 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) : static_cast<int>(surface_data_->width);
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : static_cast<int>(surface_data_->height);
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : surface_data_->width;
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : surface_data_->height;
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
for (int row = 0; row < SH; row++) {
const int SCREEN_Y = y + row;
if (SCREEN_Y < 0 || SCREEN_Y >= static_cast<int>(surface_data_dest->height)) {
if (SCREEN_Y < 0 || SCREEN_Y >= surface_data_dest->height) {
continue;
}
@@ -467,11 +433,11 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
for (int col = 0; col < SW; col++) {
const int SCREEN_X = x + col;
if (SCREEN_X < 0 || SCREEN_X >= static_cast<int>(surface_data_dest->width)) {
if (SCREEN_X < 0 || SCREEN_X >= surface_data_dest->width) {
continue;
}
const Uint8 COLOR = surface_data_->data[((SY + row) * static_cast<int>(surface_data_->width)) + (SX + col)];
const Uint8 COLOR = surface_data_->data[((SY + row) * surface_data_->width) + (SX + col)];
if (COLOR == static_cast<Uint8>(transparent_color_)) {
continue;
}
@@ -480,23 +446,23 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
continue; // Pixel tapat per la zona de fade
}
surface_data_dest->data[SCREEN_X + (SCREEN_Y * static_cast<int>(surface_data_dest->width))] = sub_palette_[COLOR];
surface_data_dest->data[SCREEN_X + (SCREEN_Y * surface_data_dest->width)] = sub_palette_[COLOR];
}
}
}
// 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) : static_cast<int>(surface_data_->width);
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : static_cast<int>(surface_data_->height);
const int SW = (src_rect != nullptr) ? static_cast<int>(src_rect->w) : surface_data_->width;
const int SH = (src_rect != nullptr) ? static_cast<int>(src_rect->h) : surface_data_->height;
auto surface_data_dest = Screen::get()->getRendererSurface()->getSurfaceData();
for (int row = 0; row < SH; row++) {
const int SCREEN_Y = y + row;
if (SCREEN_Y < 0 || SCREEN_Y >= static_cast<int>(surface_data_dest->height)) {
if (SCREEN_Y < 0 || SCREEN_Y >= surface_data_dest->height) {
continue;
}
@@ -504,11 +470,11 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
for (int col = 0; col < SW; col++) {
const int SCREEN_X = x + col;
if (SCREEN_X < 0 || SCREEN_X >= static_cast<int>(surface_data_dest->width)) {
if (SCREEN_X < 0 || SCREEN_X >= surface_data_dest->width) {
continue;
}
const Uint8 COLOR = surface_data_->data[((SY + row) * static_cast<int>(surface_data_->width)) + (SX + col)];
const Uint8 COLOR = surface_data_->data[((SY + row) * surface_data_->width) + (SX + col)];
if (COLOR == static_cast<Uint8>(transparent_color_)) {
continue;
}
@@ -518,7 +484,7 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
}
const Uint8 OUT_COLOR = (COLOR == source_color) ? target_color : sub_palette_[COLOR];
surface_data_dest->data[SCREEN_X + (SCREEN_Y * static_cast<int>(surface_data_dest->width))] = OUT_COLOR;
surface_data_dest->data[SCREEN_X + (SCREEN_Y * surface_data_dest->width)] = OUT_COLOR;
}
}
}
@@ -527,8 +493,8 @@ void Surface::renderWithVerticalFade(int x, int y, int fade_h, int canvas_height
void Surface::toARGBBuffer(Uint32* buffer) const {
if (!surface_data_ || !surface_data_->data || (buffer == nullptr)) { return; }
const int WIDTH = static_cast<int>(surface_data_->width);
const int HEIGHT = static_cast<int>(surface_data_->height);
const int WIDTH = surface_data_->width;
const int HEIGHT = surface_data_->height;
const Uint8* src = surface_data_->data.get();
// Obtenemos el tamaño de la paleta para evitar accesos fuera de rango
@@ -547,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.");
}
@@ -573,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]];
}
@@ -589,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.");
}
@@ -622,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]];
}
@@ -638,12 +604,9 @@ void Surface::copyToTexture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_FR
}
// Realiza un efecto de fundido en la paleta principal
auto Surface::fadePalette() -> bool { // NOLINT(readability-convert-member-functions-to-static)
// Verificar que el tamaño mínimo de palette_ sea adecuado
auto Surface::fadePalette() -> bool {
static constexpr int PALETTE_SIZE = 19;
if (sizeof(palette_) / sizeof(palette_[0]) < PALETTE_SIZE) {
throw std::runtime_error("Palette size is insufficient for fadePalette operation.");
}
static_assert(std::tuple_size_v<Palette> >= PALETTE_SIZE, "Palette size is insufficient for fadePalette operation.");
// Desplazar colores (pares e impares)
for (int i = 18; i > 1; --i) {
@@ -658,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;
@@ -673,11 +636,8 @@ auto Surface::fadeSubPalette(Uint32 delay) -> bool { // NOLINT(readability-conv
// Actualizar el último tick
last_tick_ = current_tick;
// Verificar que el tamaño mínimo de sub_palette_ sea adecuado
static constexpr int SUB_PALETTE_SIZE = 19;
if (sizeof(sub_palette_) / sizeof(sub_palette_[0]) < SUB_PALETTE_SIZE) {
throw std::runtime_error("Palette size is insufficient for fadePalette operation.");
}
static_assert(std::tuple_size_v<SubPalette> >= SUB_PALETTE_SIZE, "Sub-palette size is insufficient for fadeSubPalette operation.");
// Desplazar colores (pares e impares)
for (int i = 18; i > 1; --i) {
@@ -692,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_); }
+13 -12
View File
@@ -22,8 +22,8 @@ auto readPalFile(const std::string& file_path) -> Palette;
struct SurfaceData {
std::shared_ptr<Uint8[]> data; // Usa std::shared_ptr para gestión automática
float width; // Ancho de la imagen
float height; // Alto de la imagen
int width; // Ancho de la imagen
int height; // Alto de la imagen
// Constructor por defecto
SurfaceData()
@@ -32,13 +32,13 @@ struct SurfaceData {
height(0) {}
// Constructor que inicializa dimensiones y asigna memoria
SurfaceData(float w, float h)
: data(std::shared_ptr<Uint8[]>(new Uint8[static_cast<size_t>(w * h)](), std::default_delete<Uint8[]>())),
SurfaceData(int w, int h)
: data(std::shared_ptr<Uint8[]>(new Uint8[static_cast<size_t>(w) * static_cast<size_t>(h)](), std::default_delete<Uint8[]>())),
width(w),
height(h) {}
// Constructor para inicializar directamente con datos
SurfaceData(float w, float h, std::shared_ptr<Uint8[]> pixels)
SurfaceData(int w, int h, std::shared_ptr<Uint8[]> pixels)
: data(std::move(pixels)),
width(w),
height(h) {}
@@ -56,6 +56,9 @@ struct SurfaceData {
class Surface {
private:
// shared_ptr porque render() accede al SurfaceData propio y al del renderer
// surface (ver getRendererSurface()) de forma efímera; con self-blit ambos
// pueden alias y el refcount evita free accidental durante el recorrido.
std::shared_ptr<SurfaceData> surface_data_; // Datos a dibujar
Palette palette_; // Paleta para volcar la SurfaceData a una Textura
SubPalette sub_palette_; // Paleta para reindexar colores
@@ -77,7 +80,6 @@ class Surface {
void loadPalette(const Palette& palette);
// Copia una región de la SurfaceData de origen a la SurfaceData de destino
void render(float dx, float dy, float sx, float sy, float w, float h);
void render(int x, int y, SDL_FRect* src_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
void render(SDL_FRect* src_rect = nullptr, SDL_FRect* dst_rect = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE);
@@ -85,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);
@@ -127,11 +129,10 @@ class Surface {
// Metodos para gestionar surface_data_
[[nodiscard]] auto getSurfaceData() const -> std::shared_ptr<SurfaceData> { return surface_data_; }
void setSurfaceData(std::shared_ptr<SurfaceData> new_data) { surface_data_ = std::move(new_data); }
// Obtien ancho y alto
[[nodiscard]] auto getWidth() const -> float { return surface_data_->width; }
[[nodiscard]] auto getHeight() const -> float { return surface_data_->height; }
[[nodiscard]] auto getWidth() const -> int { return surface_data_->width; }
[[nodiscard]] auto getHeight() const -> int { return surface_data_->height; }
// Color transparente
[[nodiscard]] auto getTransparentColor() const -> Uint8 { return transparent_color_; }
@@ -146,7 +147,7 @@ class Surface {
private:
// Helper para calcular coordenadas con flip
static void calculateFlippedCoords(int ix, int iy, float sx, float sy, float w, float h, SDL_FlipMode flip, int& src_x, int& src_y);
static void calculateFlippedCoords(int ix, int iy, int sx, int sy, int w, int h, SDL_FlipMode flip, int& src_x, int& src_y);
// Helper para copiar un pixel si no es transparente
void copyPixelIfNotTransparent(Uint8* dest_data, int dest_x, int dest_y, int dest_width, int src_x, int src_y) const;
+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; }
+334 -197
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 {
@@ -38,10 +40,10 @@ namespace Resource {
// [SINGLETON] Con este método obtenemos el objeto cache y podemos trabajar con él
auto Cache::get() -> Cache* { return Cache::cache; }
// Constructor
// Constructor — no dispara la carga. Director llama a beginLoad() + loadStep()
// desde iterate() para que el bucle SDL3 esté vivo durante la carga.
Cache::Cache()
: loading_text_(Screen::get()->getText()) {
load();
}
// Vacia todos los vectores de recursos
@@ -53,12 +55,11 @@ namespace Resource {
text_files_.clear();
texts_.clear();
animations_.clear();
rooms_.clear();
}
// Carga todos los recursos
// Carga todos los recursos de golpe (usado solo por reload() en hot-reload de debug)
void Cache::load() {
// Nota: el overlay de debug (RenderInfo) se inicializa después de esta carga,
// por lo que updateZoomFactor() se llamará correctamente en RenderInfo::init().
calculateTotal();
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK));
std::cout << "\n** LOADING RESOURCES" << '\n';
@@ -73,14 +74,109 @@ namespace Resource {
std::cout << "\n** RESOURCES LOADED" << '\n';
}
// Recarga todos los recursos
// Prepara el loader incremental. Director lo llama una vez tras Cache::init().
void Cache::beginLoad() {
calculateTotal();
Screen::get()->setBorderColor(static_cast<Uint8>(PaletteColor::BLACK));
std::cout << "\n** LOADING RESOURCES (incremental)" << '\n';
stage_ = LoadStage::SOUNDS;
stage_index_ = 0;
}
auto Cache::isLoadDone() const -> bool {
return stage_ == LoadStage::DONE;
}
// 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; }
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:
stepEachInList(List::Type::SOUND, "SOUND FILES", [this] { sounds_.clear(); }, LoadStage::MUSICS, [this](size_t i) { loadOneSound(i); });
break;
case LoadStage::MUSICS:
stepEachInList(List::Type::MUSIC, "MUSIC FILES", [this] { musics_.clear(); }, LoadStage::SURFACES, [this](size_t i) { loadOneMusic(i); });
break;
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:
finalizeSurfaces();
stage_ = LoadStage::PALETTES;
stage_index_ = 0;
break;
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:
stepEachInList(List::Type::FONT, "TEXT FILES", [this] { text_files_.clear(); }, LoadStage::ANIMATIONS, [this](size_t i) { loadOneTextFile(i); });
break;
case LoadStage::ANIMATIONS:
stepEachInList(List::Type::ANIMATION, "ANIMATIONS", [this] { animations_.clear(); }, LoadStage::ROOMS, [this](size_t i) { loadOneAnimation(i); });
break;
case LoadStage::ROOMS:
stepEachInList(List::Type::ROOM, "ROOMS", [this] { rooms_.clear(); }, LoadStage::TEXTS, [this](size_t i) { loadOneRoom(i); });
break;
case LoadStage::TEXTS:
stepTexts();
break;
case LoadStage::DONE:
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();
load();
}
// 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()) {
@@ -92,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()) {
@@ -104,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()) {
@@ -116,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()) {
@@ -128,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()) {
@@ -140,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()) {
@@ -152,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()) {
@@ -164,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()) {
@@ -208,102 +304,108 @@ 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;
}
// Carga los sonidos
void Cache::loadSounds() { // NOLINT(readability-convert-member-functions-to-static)
std::cout << "\n>> SOUND FILES" << '\n';
// Lista fija de text objects. Compartida entre createText() y createOneText(i).
namespace {
struct TextObjectInfo {
std::string key; // Identificador del recurso
std::string texture_file; // Nombre del archivo de textura
std::string text_file; // Nombre del archivo de texto
};
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;
}
} // namespace
// --- Helpers incrementales (un asset por llamada) ---
void Cache::loadOneSound(size_t index) {
auto list = List::get()->getListByType(List::Type::SOUND);
sounds_.clear();
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
Ja::Sound* sound = nullptr;
for (const auto& l : list) {
try {
auto name = getFileName(l);
JA_Sound_t* sound = nullptr;
// Try loading from resource pack first
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
sound = JA_LoadSound(audio_data.data(), static_cast<Uint32>(audio_data.size()));
}
// Fallback to file path if memory loading failed
if (sound == nullptr) {
sound = JA_LoadSound(l.c_str());
}
if (sound == nullptr) {
throw std::runtime_error("Failed to decode audio file");
}
sounds_.emplace_back(SoundResource{.name = name, .sound = sound});
printWithDots("Sound : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("SOUND", l, e);
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
sound = Ja::loadSound(audio_data.data(), static_cast<Uint32>(audio_data.size()));
}
if (sound == nullptr) {
sound = Ja::loadSound(l.c_str());
}
if (sound == nullptr) {
throw std::runtime_error("Failed to decode audio file");
}
sounds_.emplace_back(SoundResource{.name = name, .sound = sound});
printWithDots("Sound : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
logLoadError("SOUND", l, e);
throw;
}
}
// Carga las musicas
void Cache::loadMusics() { // NOLINT(readability-convert-member-functions-to-static)
std::cout << "\n>> MUSIC FILES" << '\n';
void Cache::loadOneMusic(size_t index) {
auto list = List::get()->getListByType(List::Type::MUSIC);
musics_.clear();
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
Ja::Music* music = nullptr;
for (const auto& l : list) {
try {
auto name = getFileName(l);
JA_Music_t* music = nullptr;
// Try loading from resource pack first
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
music = JA_LoadMusic(audio_data.data(), static_cast<Uint32>(audio_data.size()));
}
// Fallback to file path if memory loading failed
if (music == nullptr) {
music = JA_LoadMusic(l.c_str());
}
if (music == nullptr) {
throw std::runtime_error("Failed to decode music file");
}
musics_.emplace_back(MusicResource{.name = name, .music = music});
printWithDots("Music : ", name, "[ LOADED ]");
updateLoadingProgress(1);
} catch (const std::exception& e) {
throwLoadError("MUSIC", l, e);
auto audio_data = Helper::loadFile(l);
if (!audio_data.empty()) {
music = Ja::loadMusic(audio_data.data(), static_cast<Uint32>(audio_data.size()));
}
if (music == nullptr) {
music = Ja::loadMusic(l.c_str());
}
if (music == nullptr) {
throw std::runtime_error("Failed to decode music file");
}
musics_.emplace_back(MusicResource{.name = name, .music = music});
printWithDots("Music : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
logLoadError("MUSIC", l, e);
throw;
}
}
// Carga las texturas
void Cache::loadSurfaces() { // NOLINT(readability-convert-member-functions-to-static)
std::cout << "\n>> SURFACES" << '\n';
void Cache::loadOneSurface(size_t index) {
auto list = List::get()->getListByType(List::Type::BITMAP);
surfaces_.clear();
for (const auto& l : list) {
try {
auto name = getFileName(l);
surfaces_.emplace_back(SurfaceResource{.name = name, .surface = std::make_shared<Surface>(l)});
surfaces_.back().surface->setTransparentColor(0);
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("BITMAP", l, e);
}
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
surfaces_.emplace_back(SurfaceResource{.name = name, .surface = std::make_shared<Surface>(l)});
surfaces_.back().surface->setTransparentColor(0);
updateLoadingProgress();
} catch (const std::exception& e) {
logLoadError("BITMAP", l, e);
throw;
}
}
void Cache::finalizeSurfaces() {
// Reconfigura el color transparente de algunas surfaces
getSurface("loading_screen_color.gif")->setTransparentColor();
getSurface("ending1.gif")->setTransparentColor();
@@ -314,112 +416,144 @@ namespace Resource {
getSurface("standard.gif")->setTransparentColor(16);
}
// Carga las paletas
void Cache::loadPalettes() { // NOLINT(readability-convert-member-functions-to-static)
void Cache::loadOnePalette(size_t index) {
auto list = List::get()->getListByType(List::Type::PALETTE);
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
logLoadError("PALETTE", l, e);
throw;
}
}
void Cache::loadOneTextFile(size_t index) {
auto list = List::get()->getListByType(List::Type::FONT);
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
logLoadError("FONT", l, e);
throw;
}
}
void Cache::loadOneAnimation(size_t index) {
auto list = List::get()->getListByType(List::Type::ANIMATION);
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
auto yaml_bytes = Helper::loadFile(l);
if (yaml_bytes.empty()) {
throw std::runtime_error("File is empty or could not be loaded");
}
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = yaml_bytes});
printWithDots("Animation : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
logLoadError("ANIMATION", l, e);
throw;
}
}
void Cache::loadOneRoom(size_t index) {
auto list = List::get()->getListByType(List::Type::ROOM);
const auto& l = list[index];
try {
auto name = getFileName(l);
setCurrentLoading(name);
rooms_.emplace_back(RoomResource{.name = name, .room = std::make_shared<Room::Data>(Room::loadYAML(l))});
printWithDots("Room : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
logLoadError("ROOM", l, e);
throw;
}
}
void Cache::createOneText(size_t index) {
const auto& infos = getTextObjectInfos();
const auto& res_info = infos[index];
texts_.emplace_back(TextResource{
.name = res_info.key,
.text = std::make_shared<Text>(getSurface(res_info.texture_file), getTextFile(res_info.text_file))});
printWithDots("Text : ", res_info.key, "[ DONE ]");
}
// --- Bucles completos (solo usados por reload() síncrono) ---
void Cache::loadSounds() {
std::cout << "\n>> SOUND FILES" << '\n';
auto list = List::get()->getListByType(List::Type::SOUND);
sounds_.clear();
for (size_t i = 0; i < list.size(); ++i) { loadOneSound(i); }
}
void Cache::loadMusics() {
std::cout << "\n>> MUSIC FILES" << '\n';
auto list = List::get()->getListByType(List::Type::MUSIC);
musics_.clear();
for (size_t i = 0; i < list.size(); ++i) { loadOneMusic(i); }
}
void Cache::loadSurfaces() {
std::cout << "\n>> SURFACES" << '\n';
auto list = List::get()->getListByType(List::Type::BITMAP);
surfaces_.clear();
for (size_t i = 0; i < list.size(); ++i) { loadOneSurface(i); }
finalizeSurfaces();
}
void Cache::loadPalettes() {
std::cout << "\n>> PALETTES" << '\n';
auto list = List::get()->getListByType(List::Type::PALETTE);
palettes_.clear();
for (const auto& l : list) {
try {
auto name = getFileName(l);
palettes_.emplace_back(ResourcePalette{.name = name, .palette = readPalFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("PALETTE", l, e);
}
}
for (size_t i = 0; i < list.size(); ++i) { loadOnePalette(i); }
}
// Carga los ficheros de texto
void Cache::loadTextFiles() { // NOLINT(readability-convert-member-functions-to-static)
void Cache::loadTextFiles() {
std::cout << "\n>> TEXT FILES" << '\n';
auto list = List::get()->getListByType(List::Type::FONT);
text_files_.clear();
for (const auto& l : list) {
try {
auto name = getFileName(l);
text_files_.emplace_back(TextFileResource{.name = name, .text_file = Text::loadTextFile(l)});
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("FONT", l, e);
}
}
for (size_t i = 0; i < list.size(); ++i) { loadOneTextFile(i); }
}
// Carga las animaciones
void Cache::loadAnimations() { // NOLINT(readability-convert-member-functions-to-static)
void Cache::loadAnimations() {
std::cout << "\n>> ANIMATIONS" << '\n';
auto list = List::get()->getListByType(List::Type::ANIMATION);
animations_.clear();
for (const auto& l : list) {
try {
auto name = getFileName(l);
// Cargar bytes del archivo YAML sin parsear (carga lazy)
auto yaml_bytes = Helper::loadFile(l);
if (yaml_bytes.empty()) {
throw std::runtime_error("File is empty or could not be loaded");
}
animations_.emplace_back(AnimationResource{.name = name, .yaml_data = yaml_bytes});
printWithDots("Animation : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("ANIMATION", l, e);
}
}
for (size_t i = 0; i < list.size(); ++i) { loadOneAnimation(i); }
}
// Carga las habitaciones desde archivos YAML
void Cache::loadRooms() { // NOLINT(readability-convert-member-functions-to-static)
void Cache::loadRooms() {
std::cout << "\n>> ROOMS" << '\n';
auto list = List::get()->getListByType(List::Type::ROOM);
rooms_.clear();
for (const auto& l : list) {
try {
auto name = getFileName(l);
rooms_.emplace_back(RoomResource{.name = name, .room = std::make_shared<Room::Data>(Room::loadYAML(l))});
printWithDots("Room : ", name, "[ LOADED ]");
updateLoadingProgress();
} catch (const std::exception& e) {
throwLoadError("ROOM", l, e);
}
}
for (size_t i = 0; i < list.size(); ++i) { loadOneRoom(i); }
}
void Cache::createText() { // NOLINT(readability-convert-member-functions-to-static)
struct ResourceInfo {
std::string key; // Identificador del recurso
std::string texture_file; // Nombre del archivo de textura
std::string text_file; // Nombre del archivo de texto
};
void Cache::createText() {
std::cout << "\n>> CREATING TEXT_OBJECTS" << '\n';
std::vector<ResourceInfo> resources = {
{.key = "aseprite", .texture_file = "aseprite.gif", .text_file = "aseprite.fnt"},
{.key = "gauntlet", .texture_file = "gauntlet.gif", .text_file = "gauntlet.fnt"},
{.key = "smb2", .texture_file = "smb2.gif", .text_file = "smb2.fnt"},
{.key = "subatomic", .texture_file = "subatomic.gif", .text_file = "subatomic.fnt"},
{.key = "8bithud", .texture_file = "8bithud.gif", .text_file = "8bithud.fnt"}};
for (const auto& res_info : resources) {
texts_.emplace_back(TextResource{.name = res_info.key, .text = std::make_shared<Text>(getSurface(res_info.texture_file), getTextFile(res_info.text_file))});
printWithDots("Text : ", res_info.key, "[ DONE ]");
}
texts_.clear();
const auto& infos = getTextObjectInfos();
for (size_t i = 0; i < infos.size(); ++i) { createOneText(i); }
}
// Vacía el vector de sonidos
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;
}
}
@@ -428,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;
}
}
@@ -460,14 +594,20 @@ namespace Resource {
// Muestra el progreso de carga
void Cache::renderProgress() {
Screen::get()->start();
Screen::get()->clearSurface(static_cast<Uint8>(PaletteColor::BLACK));
// Si show=false: pantalla negra y salir
if (!Options::loading.show) {
Screen::get()->render();
return;
}
constexpr float X_PADDING = 60.0F;
constexpr float Y_PADDING = 10.0F;
constexpr float BAR_HEIGHT = 5.0F;
const float BAR_POSITION = Options::game.height - BAR_HEIGHT - Y_PADDING;
Screen::get()->start();
Screen::get()->clearSurface(static_cast<Uint8>(PaletteColor::BLACK));
auto surface = Screen::get()->getRendererSurface();
const auto LOADING_TEXT_COLOR = static_cast<Uint8>(PaletteColor::BRIGHT_WHITE);
const auto BAR_COLOR = static_cast<Uint8>(PaletteColor::WHITE);
@@ -501,33 +641,30 @@ namespace Resource {
SDL_FRect rect_full = {.x = X_PADDING, .y = BAR_POSITION, .w = FULL_BAR_WIDTH, .h = BAR_HEIGHT};
surface->fillRect(&rect_full, BAR_COLOR);
// Mostra el nom del recurs (o missatge d'espera si ja ha acabat i wait_for_input=true)
const bool WAITING_FOR_INPUT = isLoadDone() && Options::loading.wait_for_input;
const std::string OVER_BAR_TEXT = WAITING_FOR_INPUT ? "PRESS ANY KEY TO CONTINUE" : current_loading_name_;
if ((Options::loading.show_resource_name || WAITING_FOR_INPUT) && !OVER_BAR_TEXT.empty()) {
const float TEXT_Y = BAR_POSITION - static_cast<float>(TEXT_HEIGHT) - 2.0F;
loading_text_->writeColored(
CENTER_X - (loading_text_->length(OVER_BAR_TEXT) / 2),
static_cast<int>(TEXT_Y),
OVER_BAR_TEXT,
LOADING_TEXT_COLOR);
}
Screen::get()->render();
}
// Comprueba los eventos de la pantalla de carga
void Cache::checkEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_EVENT_QUIT:
exit(0);
break;
case SDL_EVENT_KEY_DOWN:
if (event.key.key == SDLK_ESCAPE) {
exit(0);
}
break;
}
}
// Guarda el nombre del recurso que se está a punto de cargar. El repintado
// lo hace el BootLoader (una vez por frame) — aquí solo se actualiza el estado.
void Cache::setCurrentLoading(const std::string& name) {
current_loading_name_ = name;
}
// Actualiza el progreso de carga
void Cache::updateLoadingProgress(int steps) {
// Incrementa el contador de recursos cargados
void Cache::updateLoadingProgress() {
count_.add(1);
if (count_.loaded % steps == 0 || count_.loaded == count_.total) {
renderProgress();
}
checkEvents();
}
} // namespace Resource
+54 -11
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>;
@@ -25,7 +28,13 @@ namespace Resource {
auto getRoom(const std::string& name) -> std::shared_ptr<Room::Data>;
auto getRooms() -> std::vector<RoomResource>&;
void reload(); // Recarga todos los recursos
// --- Incremental loading (Director drives this from iterate()) ---
void beginLoad(); // Prepara el estado del loader incremental
auto loadStep(int budget_ms) -> bool; // Carga assets durante budget_ms; devuelve true si ha terminado
void renderProgress(); // Dibuja la barra de progreso (usada por BootLoader)
[[nodiscard]] auto isLoadDone() const -> bool;
void reload(); // Recarga todos los recursos (síncrono, usado en hot-reload de debug)
#ifdef _DEBUG
void reloadRoom(const std::string& name); // Recarga una habitación desde disco
#endif
@@ -47,7 +56,21 @@ namespace Resource {
}
};
// Métodos de carga de recursos
// Etapas del loader incremental
enum class LoadStage : std::uint8_t {
SOUNDS,
MUSICS,
SURFACES,
SURFACES_POST, // Ajuste de transparent colors tras cargar todas las surfaces
PALETTES,
TEXT_FILES,
ANIMATIONS,
ROOMS,
TEXTS,
DONE
};
// Métodos de carga de recursos (bucle completo, usados por reload() síncrono)
void loadSounds();
void loadMusics();
void loadSurfaces();
@@ -57,20 +80,35 @@ namespace Resource {
void loadRooms();
void createText();
// Helpers incrementales: cargan un único asset de la categoría correspondiente
void loadOneSound(size_t index);
void loadOneMusic(size_t index);
void loadOneSurface(size_t index);
void finalizeSurfaces(); // Ajuste de transparent colors tras cargar surfaces
void loadOnePalette(size_t index);
void loadOneTextFile(size_t index);
void loadOneAnimation(size_t index);
void loadOneRoom(size_t index);
void createOneText(size_t index);
// Métodos de limpieza
void clear();
void clearSounds();
void clearMusics();
// Métodos de gestión de carga
void load();
void load(); // Carga completa síncrona (usado solo por reload())
void calculateTotal();
void renderProgress();
static void checkEvents();
void updateLoadingProgress(int steps = 5);
void updateLoadingProgress();
void setCurrentLoading(const std::string& name); // Desa el nom del recurs en curs
// Helper para mensajes de error de carga
[[noreturn]] static void throwLoadError(const std::string& asset_type, const std::string& file_path, const std::exception& e);
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();
@@ -91,6 +129,11 @@ namespace Resource {
ResourceCount count_{}; // Contador de recursos
std::shared_ptr<Text> loading_text_; // Texto para la pantalla de carga
std::string current_loading_name_; // Nom del recurs que s'està a punt de carregar
// Estado del loader incremental
LoadStage stage_{LoadStage::DONE}; // Arranca en DONE hasta que beginLoad() lo cambie
size_t stage_index_{0}; // Cursor dentro de la categoría actual
};
} // namespace Resource
+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;
+110 -35
View File
@@ -23,6 +23,7 @@
#include "game/gameplay/cheevos.hpp" // Para Cheevos
#include "game/options.hpp" // Para Options, options, OptionsVideo
#include "game/scene_manager.hpp" // Para SceneManager
#include "game/scenes/boot_loader.hpp" // Para BootLoader
#include "game/scenes/credits.hpp" // Para Credits
#include "game/scenes/ending.hpp" // Para Ending
#include "game/scenes/ending2.hpp" // Para Ending2
@@ -44,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");
@@ -67,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
@@ -143,21 +148,27 @@ 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__
// A la versió web el navegador gestiona la finestra: res de fullscreen ni zoom.
// A la versió web el navegador gestiona la finestra: forcem zoom x3
// perquè la textura 256x192 no es vegi minúscula al canvas HTML,
// i desactivem el borde per aprofitar al màxim l'espai del canvas.
Options::video.fullscreen = false;
Options::window.zoom = 1;
Options::video.integer_scale = true;
Options::window.zoom = 4;
Options::video.border.enabled = true;
Options::video.border.height = 8;
Options::video.border.width = 8;
#endif
// Configura la ruta y carga los presets de PostFX
Options::setPostFXFile(Resource::List::get()->get("postfx.yaml")); // NOLINT(readability-static-accessed-through-instance)
Options::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ó
@@ -171,12 +182,12 @@ Director::Director() {
// Crea los objetos
Screen::init();
// Initialize resources (works for both release and development)
// Inicializa el singleton del cache sin disparar la carga. La carga real
// la hace Director::iterate() llamando a Cache::loadStep() en cada frame,
// de forma que la ventana, los eventos y la barra de progreso están vivos
// desde el primer tick.
Resource::Cache::init();
Notifier::init("", "8bithud");
RenderInfo::init();
Console::init("8bithud");
Screen::get()->setNotificationsEnabled(true);
Resource::Cache::get()->beginLoad();
// Special handling for gamecontrollerdb.txt - SDL needs filesystem path
#if defined(RELEASE_BUILD) && !defined(__EMSCRIPTEN__)
@@ -185,34 +196,57 @@ 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
Input::get()->applyKeyboardBindingsFromOptions();
Input::get()->applyGamepadBindingsFromOptions();
std::cout << "\n"; // Fin de inicialización mínima de sistemas
// Construeix l'escena inicial (BootLoader). finishBoot() la canviarà a
// LOGO (o la que digui Debug) quan Cache::loadStep() complete la càrrega.
SceneManager::current = SceneManager::Scene::BOOT_LOADER;
switchToActiveScene();
}
// Inicialitzacions que depenen del cache poblat. Es crida des d'iterate()
// just quan Cache::loadStep() retorna true, amb la finestra i el bucle ja vius.
void Director::finishBoot() {
Notifier::init("", "8bithud");
RenderInfo::init();
Console::init("8bithud");
Screen::get()->setNotificationsEnabled(true);
#ifdef _DEBUG
Debug::init();
#ifdef __EMSCRIPTEN__
// A wasm el debug.yaml viu a SYSTEM_FOLDER (MEMFS no persistent) i no està
// disponible. Saltem el loadFromFile i entrem directament a la GAME.
SceneManager::current = SceneManager::Scene::GAME;
#else
Debug::get()->setDebugFile(Resource::List::get()->get("debug.yaml"));
Debug::get()->loadFromFile();
SceneManager::current = Debug::get()->getInitialScene();
MapEditor::init();
#endif
std::cout << "\n"; // Fin de inicialización de sistemas
MapEditor::init();
#else
// En release, pasamos a LOGO siempre tras la carga
SceneManager::current = SceneManager::Scene::LOGO;
#endif
// Inicializa el sistema de localización (antes de Cheevos que usa textos traducidos)
#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
@@ -222,15 +256,17 @@ Director::Director() {
#else
Cheevos::init(Resource::List::get()->get("cheevos.bin"));
#endif
// Construeix la primera escena (LOGO per defecte, o la que digui Debug)
switchToActiveScene();
}
Director::~Director() {
// Guarda las opciones a un fichero
Options::saveToFile();
// Destruir l'escena activa ABANS dels singletons. Si no, el unique_ptr membre
// destrueix l'escena al final del destructor — quan Audio, Screen, Resource...
// ja són morts — i qualsevol accés en els seus destructors és un UAF.
active_scene_.reset();
// Destruye los singletones
Cheevos::destroy();
Locale::destroy();
@@ -254,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.
@@ -283,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
@@ -316,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";
@@ -344,6 +380,10 @@ void Director::switchToActiveScene() {
active_scene_.reset();
switch (SceneManager::current) {
case SceneManager::Scene::BOOT_LOADER:
active_scene_ = std::make_unique<BootLoader>();
break;
case SceneManager::Scene::LOGO:
active_scene_ = std::make_unique<Logo>();
break;
@@ -394,6 +434,30 @@ auto Director::iterate() -> SDL_AppResult {
return SDL_APP_SUCCESS;
}
// Fase de boot: anem cridant loadStep() fins que el cache estiga ple.
// Durant aquesta fase l'escena activa és BootLoader (una barra de progrés).
if (boot_loading_) {
try {
// Budget de 50ms: durant el boot el joc va a ~15-20 FPS, suficient
// per veure la barra avançar suau i processar events del WM/ESC,
// i evita el 50% d'ineficiència que provocaria un budget < vsync.
if (Resource::Cache::get()->loadStep(50 /*ms*/)) {
if (Options::loading.show && Options::loading.wait_for_input) {
boot_waiting_for_input_ = true; // Esperar tecla antes de continuar
} else {
finishBoot();
}
boot_loading_ = false;
// finishBoot() ja ha fixat SceneManager::current a LOGO (o la que
// digui Debug). El canvi d'escena es fa just a sota.
}
} catch (const std::exception& e) {
std::cerr << "Fatal error during resource load: " << e.what() << '\n';
SceneManager::current = SceneManager::Scene::QUIT;
return SDL_APP_FAILURE;
}
}
// Si l'escena ha canviat (o s'ha demanat RESTART_CURRENT), canviar-la abans del frame
if (SceneManager::current != current_scene_ || SceneManager::current == SceneManager::Scene::RESTART_CURRENT) {
switchToActiveScene();
@@ -416,6 +480,17 @@ auto Director::handleEvent(const SDL_Event& event) -> SDL_AppResult {
}
#endif
// Si estamos esperando input tras la carga: consumir tecla/botón y arrancar
if (boot_waiting_for_input_) {
const bool IS_KEY = event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat;
const bool IS_BUTTON = event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN;
if (IS_KEY || IS_BUTTON) {
boot_waiting_for_input_ = false;
finishBoot();
}
return SDL_APP_CONTINUE;
}
if (active_scene_) {
active_scene_->handleEvent(event);
}
+5 -2
View File
@@ -22,11 +22,14 @@ class Director {
std::string executable_path_; // Path del ejecutable
std::string system_folder_; // Carpeta del sistema donde guardar datos
std::unique_ptr<Scene> active_scene_; // Escena activa
SceneManager::Scene current_scene_{SceneManager::Scene::LOGO}; // Tipus d'escena activa
std::unique_ptr<Scene> active_scene_; // Escena activa
SceneManager::Scene current_scene_{SceneManager::Scene::BOOT_LOADER}; // Tipus d'escena activa
bool boot_loading_{true}; // True mientras Cache::loadStep() no haya acabado
bool boot_waiting_for_input_{false}; // True si la carga acabó y Options::loading.wait_for_input está activo
// --- Funciones ---
void createSystemFolder(const std::string& folder); // Crea la carpeta del sistema donde guardar datos
void setFileList(); // Carga la configuración de assets desde assets.yaml
void switchToActiveScene(); // Construeix l'escena segons SceneManager::current
void finishBoot(); // Inits que dependen del cache, ejecutado tras loadStep==done
};
+56 -2
View File
@@ -1,10 +1,21 @@
#include "core/system/global_events.hpp"
#include "core/input/input.hpp" // Para Input (gamepad add/remove)
#include "core/input/mouse.hpp"
#include "game/options.hpp" // Para Options, options, OptionsGame, OptionsAudio
#include "game/ui/console.hpp" // Para Console
#include "core/locale/locale.hpp" // Para Locale
#include "game/options.hpp" // Para Options, options, OptionsGame, OptionsAudio
#include "game/scene_manager.hpp" // Para SceneManager::current (filtrar BACK a GAME)
#include "game/ui/console.hpp" // Para Console
#include "game/ui/notifier.hpp" // Para Notifier
namespace GlobalEvents {
namespace {
// Flag per saber si en aquest frame s'ha rebut un button down del gamepad.
// El consumeix GlobalInputs perquè un botó del comandament salti escenes.
bool gamepad_button_pressed = false;
} // namespace
// Comprueba los eventos que se pueden producir en cualquier sección del juego.
// Nota: SDL_EVENT_QUIT el gestiona Director::handleEvent() directament.
void handle(const SDL_Event& event) {
@@ -12,6 +23,43 @@ namespace GlobalEvents {
// reLoadTextures();
}
// Connexió/desconnexió de gamepads: cal enrutar-los a Input perquè
// afegisca el dispositiu a gamepads_. Sense això, en wasm els gamepads
// mai es detecten (la Gamepad API del navegador només els exposa
// després que l'usuari els active, més tard que el discoverGamepads
// inicial). En desktop també arregla la connexió en calent.
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED) {
if (Input::get() != nullptr) {
std::string name = Input::get()->handleEvent(event);
if (!name.empty() && Notifier::get() != nullptr && Locale::get() != nullptr) {
const std::string KEY = (event.type == SDL_EVENT_GAMEPAD_ADDED)
? "ui.gamepad_connected"
: "ui.gamepad_disconnected";
Notifier::get()->show({name + " " + Locale::get()->get(KEY)});
}
}
}
// Marcar polsació de qualsevol botó del comandament (els consumirà GlobalInputs
// per saltar escenes d'attract mode). Queden exclosos els botons reservats a
// accions globals perquè el "any button → ACCEPT" no se'ls mengi abans que
// checkAction() els pugui enrutar:
// - BACK → EXIT (a emscripten només a l'escena GAME, ja que no pot tancar).
// - LEFT_SHOULDER / RIGHT_SHOULDER → NEXT_PALETTE / NEXT_PALETTE_SORT.
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
const auto BUTTON = event.gbutton.button;
const bool IS_BACK = (BUTTON == SDL_GAMEPAD_BUTTON_BACK);
const bool IS_SHOULDER = (BUTTON == SDL_GAMEPAD_BUTTON_LEFT_SHOULDER || BUTTON == SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER);
#ifdef __EMSCRIPTEN__
const bool RESERVE_BACK = IS_BACK && SceneManager::current == SceneManager::Scene::GAME;
#else
const bool RESERVE_BACK = IS_BACK;
#endif
if (!RESERVE_BACK && !IS_SHOULDER) {
gamepad_button_pressed = true;
}
}
// Enrutar eventos de texto a la consola cuando está activa
if (Console::get() != nullptr && Console::get()->isActive()) {
if (event.type == SDL_EVENT_TEXT_INPUT || event.type == SDL_EVENT_KEY_DOWN) {
@@ -22,4 +70,10 @@ namespace GlobalEvents {
Mouse::handleEvent(event);
}
auto consumeGamepadButtonPressed() -> bool {
const bool RESULT = gamepad_button_pressed;
gamepad_button_pressed = false;
return RESULT;
}
} // namespace GlobalEvents
+5
View File
@@ -5,4 +5,9 @@
namespace GlobalEvents {
// Comprueba los eventos que se pueden producir en cualquier sección del juego
void handle(const SDL_Event& event);
// True si en aquest frame s'ha rebut un SDL_EVENT_GAMEPAD_BUTTON_DOWN.
// Es consumeix (i es reseteja) per GlobalInputs::getPressedAction perquè
// qualsevol botó del comandament actuï com a "ACCEPT" (saltar escena).
auto consumeGamepadButtonPressed() -> bool;
} // namespace GlobalEvents
+68 -49
View File
@@ -1,4 +1,4 @@
// Ogg Vorbis audio decoder - v1.20 - public domain
// Ogg Vorbis audio decoder - v1.22 - public domain
// http://nothings.org/stb_vorbis/
//
// Original version written by Sean Barrett in 2007.
@@ -29,12 +29,15 @@
// Bernhard Wodo Evan Balster github:alxprd
// Tom Beaumont Ingo Leitgeb Nicolas Guillemot
// Phillip Bennefall Rohit Thiago Goulart
// github:manxorist saga musix github:infatum
// github:manxorist Saga Musix github:infatum
// Timur Gagiev Maxwell Koo Peter Waller
// github:audinowho Dougall Johnson David Reid
// github:Clownacy Pedro J. Estebanez Remi Verschelde
// AnthoFoxo github:morlat Gabriel Ravier
//
// Partial history:
// 1.22 - 2021-07-11 - various small fixes
// 1.21 - 2021-07-02 - fix bug for files with no comments
// 1.20 - 2020-07-11 - several small fixes
// 1.19 - 2020-02-05 - warnings
// 1.18 - 2020-02-02 - fix seek bugs; parse header comments; misc warnings etc.
@@ -220,6 +223,12 @@ extern int stb_vorbis_decode_frame_pushdata(
// channel. In other words, (*output)[0][0] contains the first sample from
// the first channel, and (*output)[1][0] contains the first sample from
// the second channel.
//
// *output points into stb_vorbis's internal output buffer storage; these
// buffers are owned by stb_vorbis and application code should not free
// them or modify their contents. They are transient and will be overwritten
// once you ask for more data to get decoded, so be sure to grab any data
// you need before then.
extern void stb_vorbis_flush_pushdata(stb_vorbis *f);
// inform stb_vorbis that your next datablock will not be contiguous with
@@ -579,7 +588,7 @@ enum STBVorbisError
#if defined(_MSC_VER) || defined(__MINGW32__)
#include <malloc.h>
#endif
#if defined(__linux__) || defined(__linux) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__)
#if defined(__linux__) || defined(__linux) || defined(__sun__) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__)
#include <alloca.h>
#endif
#else // STB_VORBIS_NO_CRT
@@ -646,6 +655,12 @@ typedef signed int int32;
typedef float codetype;
#ifdef _MSC_VER
#define STBV_NOTUSED(v) (void)(v)
#else
#define STBV_NOTUSED(v) (void)sizeof(v)
#endif
// @NOTE
//
// Some arrays below are tagged "//varies", which means it's actually
@@ -1046,7 +1061,7 @@ static float float32_unpack(uint32 x)
uint32 sign = x & 0x80000000;
uint32 exp = (x & 0x7fe00000) >> 21;
double res = sign ? -(double)mantissa : (double)mantissa;
return (float) ldexp((float)res, exp-788);
return (float) ldexp((float)res, (int)exp-788);
}
@@ -1077,6 +1092,7 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
// find the first entry
for (k=0; k < n; ++k) if (len[k] < NO_CODE) break;
if (k == n) { assert(c->sorted_entries == 0); return TRUE; }
assert(len[k] < 32); // no error return required, code reading lens checks this
// add to the list
add_entry(c, 0, k, m++, len[k], values);
// add all available leaves
@@ -1090,6 +1106,7 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
uint32 res;
int z = len[i], y;
if (z == NO_CODE) continue;
assert(z < 32); // no error return required, code reading lens checks this
// find lowest available leaf (should always be earliest,
// which is what the specification calls for)
// note that this property, and the fact we can never have
@@ -1099,12 +1116,10 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
while (z > 0 && !available[z]) --z;
if (z == 0) { return FALSE; }
res = available[z];
assert(z >= 0 && z < 32);
available[z] = 0;
add_entry(c, bit_reverse(res), i, m++, len[i], values);
// propagate availability up the tree
if (z != len[i]) {
assert(len[i] >= 0 && len[i] < 32);
for (y=len[i]; y > z; --y) {
assert(available[y] == 0);
available[y] = res + (1 << (32-y));
@@ -2577,34 +2592,33 @@ static void imdct_step3_inner_s_loop_ld654(int n, float *e, int i_off, float *A,
while (z > base) {
float k00,k11;
float l00,l11;
k00 = z[-0] - z[-8];
k11 = z[-1] - z[-9];
z[-0] = z[-0] + z[-8];
z[-1] = z[-1] + z[-9];
z[-8] = k00;
z[-9] = k11 ;
k00 = z[-0] - z[ -8];
k11 = z[-1] - z[ -9];
l00 = z[-2] - z[-10];
l11 = z[-3] - z[-11];
z[ -0] = z[-0] + z[ -8];
z[ -1] = z[-1] + z[ -9];
z[ -2] = z[-2] + z[-10];
z[ -3] = z[-3] + z[-11];
z[ -8] = k00;
z[ -9] = k11;
z[-10] = (l00+l11) * A2;
z[-11] = (l11-l00) * A2;
k00 = z[ -2] - z[-10];
k11 = z[ -3] - z[-11];
z[ -2] = z[ -2] + z[-10];
z[ -3] = z[ -3] + z[-11];
z[-10] = (k00+k11) * A2;
z[-11] = (k11-k00) * A2;
k00 = z[-12] - z[ -4]; // reverse to avoid a unary negation
k00 = z[ -4] - z[-12];
k11 = z[ -5] - z[-13];
l00 = z[ -6] - z[-14];
l11 = z[ -7] - z[-15];
z[ -4] = z[ -4] + z[-12];
z[ -5] = z[ -5] + z[-13];
z[-12] = k11;
z[-13] = k00;
k00 = z[-14] - z[ -6]; // reverse to avoid a unary negation
k11 = z[ -7] - z[-15];
z[ -6] = z[ -6] + z[-14];
z[ -7] = z[ -7] + z[-15];
z[-14] = (k00+k11) * A2;
z[-15] = (k00-k11) * A2;
z[-12] = k11;
z[-13] = -k00;
z[-14] = (l11-l00) * A2;
z[-15] = (l00+l11) * -A2;
iter_54(z);
iter_54(z-8);
@@ -3069,6 +3083,7 @@ static int do_floor(vorb *f, Mapping *map, int i, int n, float *target, YTYPE *f
for (q=1; q < g->values; ++q) {
j = g->sorted_order[q];
#ifndef STB_VORBIS_NO_DEFER_FLOOR
STBV_NOTUSED(step2_flag);
if (finalY[j] >= 0)
#else
if (step2_flag[j])
@@ -3171,6 +3186,7 @@ static int vorbis_decode_packet_rest(vorb *f, int *len, Mode *m, int left_start,
// WINDOWING
STBV_NOTUSED(left_end);
n = f->blocksize[m->blockflag];
map = &f->mapping[m->mapping];
@@ -3368,7 +3384,7 @@ static int vorbis_decode_packet_rest(vorb *f, int *len, Mode *m, int left_start,
// this isn't to spec, but spec would require us to read ahead
// and decode the size of all current frames--could be done,
// but presumably it's not a commonly used feature
f->current_loc = -n2; // start of first frame is positioned for discard
f->current_loc = 0u - n2; // start of first frame is positioned for discard (NB this is an intentional unsigned overflow/wrap-around)
// we might have to discard samples "from" the next frame too,
// if we're lapping a large block then a small at the start?
f->discard_samples_deferred = n - right_end;
@@ -3642,9 +3658,11 @@ static int start_decoder(vorb *f)
f->vendor[len] = (char)'\0';
//user comments
f->comment_list_length = get32_packet(f);
if (f->comment_list_length > 0) {
f->comment_list = (char**)setup_malloc(f, sizeof(char*) * (f->comment_list_length));
if (f->comment_list == NULL) return error(f, VORBIS_outofmem);
f->comment_list = NULL;
if (f->comment_list_length > 0)
{
f->comment_list = (char**) setup_malloc(f, sizeof(char*) * (f->comment_list_length));
if (f->comment_list == NULL) return error(f, VORBIS_outofmem);
}
for(i=0; i < f->comment_list_length; ++i) {
@@ -3867,8 +3885,7 @@ static int start_decoder(vorb *f)
unsigned int div=1;
for (k=0; k < c->dimensions; ++k) {
int off = (z / div) % c->lookup_values;
float val = mults[off];
val = mults[off]*c->delta_value + c->minimum_value + last;
float val = mults[off]*c->delta_value + c->minimum_value + last;
c->multiplicands[j*c->dimensions + k] = val;
if (c->sequence_p)
last = val;
@@ -3951,7 +3968,7 @@ static int start_decoder(vorb *f)
if (g->class_masterbooks[j] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
}
for (k=0; k < 1 << g->class_subclasses[j]; ++k) {
g->subclass_books[j][k] = get_bits(f,8)-1;
g->subclass_books[j][k] = (int16)get_bits(f,8)-1;
if (g->subclass_books[j][k] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
}
}
@@ -4509,6 +4526,7 @@ stb_vorbis *stb_vorbis_open_pushdata(
*error = VORBIS_need_more_data;
else
*error = p.error;
vorbis_deinit(&p);
return NULL;
}
f = vorbis_alloc(&p);
@@ -4566,7 +4584,7 @@ static uint32 vorbis_find_page(stb_vorbis *f, uint32 *end, uint32 *last)
header[i] = get8(f);
if (f->eof) return 0;
if (header[4] != 0) goto invalid;
goal = header[22] + (header[23] << 8) + (header[24]<<16) + (header[25]<<24);
goal = header[22] + (header[23] << 8) + (header[24]<<16) + ((uint32)header[25]<<24);
for (i=22; i < 26; ++i)
header[i] = 0;
crc = 0;
@@ -4970,7 +4988,7 @@ unsigned int stb_vorbis_stream_length_in_samples(stb_vorbis *f)
// set. whoops!
break;
}
previous_safe = last_page_loc+1;
//previous_safe = last_page_loc+1; // NOTE: not used after this point, but note for debugging
last_page_loc = stb_vorbis_get_file_offset(f);
}
@@ -5081,7 +5099,10 @@ stb_vorbis * stb_vorbis_open_filename(const char *filename, int *error, const st
stb_vorbis * stb_vorbis_open_memory(const unsigned char *data, int len, int *error, const stb_vorbis_alloc *alloc)
{
stb_vorbis *f, p;
if (data == NULL) return NULL;
if (!data) {
if (error) *error = VORBIS_unexpected_eof;
return NULL;
}
vorbis_init(&p, alloc);
p.stream = (uint8 *) data;
p.stream_end = (uint8 *) data + len;
@@ -5156,11 +5177,11 @@ static void copy_samples(short *dest, float *src, int len)
static void compute_samples(int mask, short *output, int num_c, float **data, int d_offset, int len)
{
#define BUFFER_SIZE 32
float buffer[BUFFER_SIZE];
int i,j,o,n = BUFFER_SIZE;
#define STB_BUFFER_SIZE 32
float buffer[STB_BUFFER_SIZE];
int i,j,o,n = STB_BUFFER_SIZE;
check_endianness();
for (o = 0; o < len; o += BUFFER_SIZE) {
for (o = 0; o < len; o += STB_BUFFER_SIZE) {
memset(buffer, 0, sizeof(buffer));
if (o + n > len) n = len - o;
for (j=0; j < num_c; ++j) {
@@ -5177,16 +5198,17 @@ static void compute_samples(int mask, short *output, int num_c, float **data, in
output[o+i] = v;
}
}
#undef STB_BUFFER_SIZE
}
static void compute_stereo_samples(short *output, int num_c, float **data, int d_offset, int len)
{
#define BUFFER_SIZE 32
float buffer[BUFFER_SIZE];
int i,j,o,n = BUFFER_SIZE >> 1;
#define STB_BUFFER_SIZE 32
float buffer[STB_BUFFER_SIZE];
int i,j,o,n = STB_BUFFER_SIZE >> 1;
// o is the offset in the source data
check_endianness();
for (o = 0; o < len; o += BUFFER_SIZE >> 1) {
for (o = 0; o < len; o += STB_BUFFER_SIZE >> 1) {
// o2 is the offset in the output data
int o2 = o << 1;
memset(buffer, 0, sizeof(buffer));
@@ -5216,6 +5238,7 @@ static void compute_stereo_samples(short *output, int num_c, float **data, int d
output[o2+i] = v;
}
}
#undef STB_BUFFER_SIZE
}
static void convert_samples_short(int buf_c, short **buffer, int b_offset, int data_c, float **data, int d_offset, int samples)
@@ -5288,8 +5311,6 @@ int stb_vorbis_get_samples_short_interleaved(stb_vorbis *f, int channels, short
float **outputs;
int len = num_shorts / channels;
int n=0;
int z = f->channels;
if (z > channels) z = channels;
while (n < len) {
int k = f->channel_buffer_end - f->channel_buffer_start;
if (n+k >= len) k = len - n;
@@ -5308,8 +5329,6 @@ int stb_vorbis_get_samples_short(stb_vorbis *f, int channels, short **buffer, in
{
float **outputs;
int n=0;
int z = f->channels;
if (z > channels) z = channels;
while (n < len) {
int k = f->channel_buffer_end - f->channel_buffer_start;
if (n+k >= len) k = len - n;
+6 -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
@@ -89,6 +86,12 @@ namespace Defaults::Localization {
constexpr const char* LANGUAGE = "ca"; // Idioma por defecto (en = inglés, ca = catalán)
} // namespace Defaults::Localization
namespace Defaults::Loading {
constexpr bool SHOW = false; // No mostrar la pantalla de carga por defecto
constexpr bool SHOW_RESOURCE_NAME = true; // Mostrar el nombre del recurso por defecto
constexpr bool WAIT_FOR_INPUT = false; // No esperar tecla al terminar por defecto
} // namespace Defaults::Loading
namespace Defaults::Game::Items {
constexpr const float PERCENT_TO_OPEN_THE_JAIL = 0.9F; // Porcentaje de items necesarios para abrir la jail
} // namespace Defaults::Game::Items
+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
+3 -3
View File
@@ -39,9 +39,9 @@ void MiniMap::buildTileColorTable(const std::string& tileset_name) {
auto tileset = Resource::Cache::get()->getSurface(tileset_name);
if (!tileset) { return; }
tileset_width_ = static_cast<int>(tileset->getWidth()) / Tile::SIZE;
tileset_width_ = tileset->getWidth() / Tile::SIZE;
tileset_transparent_ = tileset->getTransparentColor();
int tileset_height = static_cast<int>(tileset->getHeight()) / Tile::SIZE;
int tileset_height = tileset->getHeight() / Tile::SIZE;
int total_tiles = tileset_width_ * tileset_height;
tile_colors_.resize(total_tiles, 0);
@@ -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 ---
+2 -2
View File
@@ -26,8 +26,8 @@ void TilePicker::open(const std::string& tileset_name, int current_tile, int bg_
// Calcular dimensiones del tileset en tiles (teniendo en cuenta spacing de entrada)
int src_cell = Tile::SIZE + spacing_in_;
tileset_width_ = static_cast<int>(tileset_->getWidth()) / src_cell;
tileset_height_ = static_cast<int>(tileset_->getHeight()) / src_cell;
tileset_width_ = tileset_->getWidth() / src_cell;
tileset_height_ = tileset_->getHeight() / src_cell;
// Corregir si el último tile cabe sin spacing
if (tileset_width_ == 0 && tileset_->getWidth() >= Tile::SIZE) { tileset_width_ = 1; }
if (tileset_height_ == 0 && tileset_->getHeight() >= Tile::SIZE) { tileset_height_ = 1; }
+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
+191 -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);
}
}
@@ -623,6 +596,27 @@ namespace Options {
}
}
// Carga opciones de la pantalla de carga desde YAML
void loadLoadingFromYaml(const fkyaml::node& yaml) {
if (!yaml.contains("loading")) { return; }
const auto& ld = yaml["loading"];
if (ld.contains("show")) {
try {
loading.show = ld["show"].get_value<bool>();
} catch (...) { loading.show = Defaults::Loading::SHOW; }
}
if (ld.contains("show_resource_name")) {
try {
loading.show_resource_name = ld["show_resource_name"].get_value<bool>();
} catch (...) { loading.show_resource_name = Defaults::Loading::SHOW_RESOURCE_NAME; }
}
if (ld.contains("wait_for_input")) {
try {
loading.wait_for_input = ld["wait_for_input"].get_value<bool>();
} catch (...) { loading.wait_for_input = Defaults::Loading::WAIT_FOR_INPUT; }
}
}
// Establece la ruta del fichero de configuración
void setConfigFile(const std::string& path) {
config_file_path = path;
@@ -673,6 +667,7 @@ namespace Options {
loadPlayerConfigFromYaml(yaml);
loadKioskConfigFromYaml(yaml);
loadLocalizationFromYaml(yaml);
loadLoadingFromYaml(yaml);
std::cout << "Config file loaded successfully\n\n";
@@ -764,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";
@@ -810,6 +801,14 @@ namespace Options {
file << "localization:\n";
file << " language: \"" << language << "\"\n";
// LOADING SCREEN
file << "\n";
file << "# LOADING SCREEN\n";
file << "loading:\n";
file << " show: " << (loading.show ? "true" : "false") << "\n";
file << " show_resource_name: " << (loading.show_resource_name ? "true" : "false") << "\n";
file << " wait_for_input: " << (loading.wait_for_input ? "true" : "false") << " # solo si show=true\n";
file.close();
std::cout << "Config file saved successfully\n\n";
@@ -861,7 +860,8 @@ namespace Options {
if (node.contains(key)) {
try {
target = node[key].get_value<float>();
} catch (...) {}
} catch (...) { /* @INTENTIONAL: camp YAML malformat → conservem default */
}
}
}
@@ -890,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);
}
}
@@ -935,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;
@@ -1020,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;
}
@@ -1132,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;
}
+22 -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
};
@@ -134,6 +126,13 @@ namespace Options {
float volume{Defaults::Audio::VOLUME}; // Volumen al que suenan el audio (0-128 internamente)
};
// Estructura para las opciones de la pantalla de carga
struct Loading {
bool show{Defaults::Loading::SHOW}; // Muestra la pantalla de carga (si no, pantalla en negro)
bool show_resource_name{Defaults::Loading::SHOW_RESOURCE_NAME}; // Muestra el nombre del recurso sobre la barra de progreso
bool wait_for_input{Defaults::Loading::WAIT_FOR_INPUT}; // Al terminar la carga, espera tecla antes de continuar (solo si show=true)
};
// Estructura para las opciones de juego
struct Game {
float width{Defaults::Canvas::WIDTH}; // Ancho de la resolucion del juego
@@ -144,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
@@ -182,6 +186,7 @@ namespace Options {
inline Stats stats{}; // Datos con las estadisticas de juego
inline Window window{}; // Opciones relativas a la ventana
inline Audio audio{}; // Opciones relativas al audio
inline Loading loading{}; // Opciones de la pantalla de carga
inline KeyboardControls keyboard_controls{}; // Teclas usadas para jugar
inline GamepadControls gamepad_controls{}; // Botones del gamepad usados para jugar
inline Kiosk kiosk{}; // Opciones del modo kiosko
+6 -3
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,8 @@
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
TITLE, // Pantalla de título/menú principal
@@ -25,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
@@ -34,7 +37,7 @@ namespace SceneManager {
};
// --- Variables de estado globales ---
inline Scene current = Scene::LOGO; // Escena actual (en _DEBUG sobrescrito por Director tras cargar debug.yaml)
inline Scene current = Scene::BOOT_LOADER; // Arranca siempre cargando recursos; Director conmuta a LOGO al terminar
inline Options options = Options::LOGO_TO_LOADING_SCREEN; // Opciones de la escena actual
inline Scene scene_before_restart = Scene::LOGO; // escena a relanzar tras RESTART_CURRENT
+14
View File
@@ -0,0 +1,14 @@
#include "game/scenes/boot_loader.hpp"
#include "core/resources/resource_cache.hpp"
#include "game/scene_manager.hpp"
void BootLoader::iterate() {
Resource::Cache::get()->renderProgress();
}
void BootLoader::handleEvent(const SDL_Event& event) {
if (event.type == SDL_EVENT_QUIT) {
SceneManager::current = SceneManager::Scene::QUIT;
}
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <SDL3/SDL.h>
#include "game/scenes/scene.hpp"
// Escena mínima que Director usa mientras el cache se carga incrementalmente.
// No avanza la carga — lo hace Director::iterate() llamando a Cache::loadStep()
// antes de despachar la escena. Aquí solo se pinta la barra de progreso.
class BootLoader : public Scene {
public:
BootLoader() = default;
~BootLoader() override = default;
void iterate() override;
void handleEvent(const SDL_Event& event) override;
};
+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)});
+6 -5
View File
@@ -2,12 +2,13 @@
#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
class AnimatedSprite; // lines 11-11
class Surface;
class PixelReveal;
class DeltaTimer;
@@ -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;
+7 -6
View File
@@ -2,13 +2,14 @@
#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
class Surface; // lines 9-9
class Sprite; // lines 8-8
class Surface; // lines 9-9
class PixelReveal;
class DeltaTimer;
@@ -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"));

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