Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 551cd23318 | |||
| 3b675246bb | |||
| 88b295bc13 | |||
| 86bdfb8f73 | |||
| 7a4b340ee4 | |||
| a43c3fc5d1 | |||
| bdbb6bc764 | |||
| fa2dc9bbf3 | |||
| 73f210bc2c | |||
| 74d96047c7 | |||
| 20325ddd5a | |||
| ac997c185d | |||
| 5fcbce6e7b | |||
| 984d1fca50 | |||
| 66ad34b667 | |||
| bded70a52a | |||
| 1129f1116e | |||
| 1ddc821f6f | |||
| 49be109560 | |||
| 63eaaa8b5c | |||
| 748673f41b | |||
| 8af4b0c259 | |||
| be1a9a1d9b | |||
| 7bd4d4d114 | |||
| 0148ccc4d5 | |||
| b558ea0b4c | |||
| 635662d65d | |||
| 2a69eaf041 | |||
| 4f7333ba46 | |||
| 54ef4c85eb | |||
| 36d50ade82 | |||
| 91c5b9d2b2 | |||
| 93af6dd58d | |||
| 13875e7b8c | |||
| eac2d42a1b | |||
| c920f99c82 | |||
| fe240c750e | |||
| 2b57bfa4dd | |||
| f1a6636222 | |||
| 081a7e02c7 | |||
| 77718d4515 | |||
| 3ac495f444 | |||
| a8c0386355 | |||
| ebfcad6f22 | |||
| a40931c7ca | |||
| 659e37e5a1 | |||
| 7006207b7e | |||
| 415ce17f3b | |||
| 6b0337b750 | |||
| 4c7f28d746 | |||
| e57944398a | |||
| e887b77dcb | |||
| 91add6f2fe | |||
| 169a5ea7aa | |||
| f10be8c277 | |||
| 7d8aac6121 | |||
| 76d0c72b85 | |||
| 6c6643b890 | |||
| 97977d19e8 | |||
| d9004caa2a | |||
| cc12ef6590 | |||
| 1e6cb3bb24 | |||
| 40e1140734 | |||
| d72630523a | |||
| 479d9d941a | |||
| 37cb3c782a | |||
| be95b8afab | |||
| 9f6d38cf48 | |||
| ee2dd0bc2c | |||
| 3421f34a84 | |||
| 18cd287808 | |||
| b1392d0c00 | |||
| be18f51735 | |||
| 48af959814 | |||
| 0bc55f5732 | |||
| 9a2da460cc | |||
| 0ee117135c | |||
| dc622c7bae | |||
| 1912200b21 | |||
| 88fa3f296f | |||
| ceb5324d23 | |||
| ce8eee07ff | |||
| 2282377ae7 | |||
| 118626dff6 | |||
| e2bc6aa5c0 | |||
| c86e020312 | |||
| 285b094dad | |||
| cf436f0014 | |||
| 7a09c0aa89 | |||
| 6f9bdcbeb6 | |||
| 6bdb5c207c | |||
| 6246b5d89d | |||
| 34a41ad25c | |||
| 20b9a95619 | |||
| 513eacf356 | |||
| 5889df2a47 | |||
| 7f703390f9 | |||
| 1bb0ebdef8 | |||
| 5fec0110b3 | |||
| 55caef3210 | |||
| 007c1d3554 | |||
| 28606a9fe1 | |||
| 294e665b11 | |||
| 0faa605ad9 | |||
| c3534ace9c | |||
| 517bc2caa1 | |||
| f9b0f64b81 | |||
| e0498d642d | |||
| ccdf9732d1 | |||
| 1451327fcc | |||
| a035fecb04 | |||
| 9d70138855 | |||
| dfe0a3d4e6 | |||
| 66c3e0089c | |||
| 86323a0e56 | |||
| 58cacf7bda | |||
| 978cbcc9fc | |||
| fb023df1e1 | |||
| 555f347375 | |||
| 85a47c1a2b | |||
| 06d4712493 | |||
| 18c4d6032d | |||
| 9365f80e8b | |||
| 4bd07216f3 |
+2
-2
@@ -2,8 +2,8 @@ BasedOnStyle: Google
|
|||||||
IndentWidth: 4
|
IndentWidth: 4
|
||||||
NamespaceIndentation: All
|
NamespaceIndentation: All
|
||||||
IndentAccessModifiers: false
|
IndentAccessModifiers: false
|
||||||
ColumnLimit: 0 # Sin limite de longitud de linea
|
ColumnLimit: 0 # Sin límite de longitud de línea
|
||||||
BreakBeforeBraces: Attach # Llaves en la misma linea
|
BreakBeforeBraces: Attach # Llaves en la misma línea
|
||||||
AllowShortIfStatementsOnASingleLine: true
|
AllowShortIfStatementsOnASingleLine: true
|
||||||
AllowShortBlocksOnASingleLine: true
|
AllowShortBlocksOnASingleLine: true
|
||||||
AllowShortFunctionsOnASingleLine: All
|
AllowShortFunctionsOnASingleLine: All
|
||||||
|
|||||||
+76
-53
@@ -2,83 +2,106 @@ Checks:
|
|||||||
- readability-*
|
- readability-*
|
||||||
- modernize-*
|
- modernize-*
|
||||||
- performance-*
|
- performance-*
|
||||||
- bugprone-unchecked-optional-access
|
- bugprone-*
|
||||||
- bugprone-sizeof-expression
|
|
||||||
- bugprone-suspicious-missing-comma
|
|
||||||
- bugprone-suspicious-index
|
|
||||||
- bugprone-undefined-memory-manipulation
|
|
||||||
- bugprone-use-after-move
|
|
||||||
- bugprone-out-of-bound-access
|
|
||||||
- -readability-identifier-length
|
- -readability-identifier-length
|
||||||
- -readability-magic-numbers
|
- -readability-magic-numbers
|
||||||
- -bugprone-narrowing-conversions
|
|
||||||
- -performance-enum-size
|
|
||||||
- -performance-inefficient-string-concatenation
|
|
||||||
- -bugprone-integer-division
|
- -bugprone-integer-division
|
||||||
- -bugprone-easily-swappable-parameters
|
- -bugprone-easily-swappable-parameters
|
||||||
- -modernize-avoid-c-arrays,-warnings-as-errors
|
- -bugprone-narrowing-conversions
|
||||||
|
- -modernize-avoid-c-arrays
|
||||||
|
|
||||||
WarningsAsErrors: '*'
|
WarningsAsErrors: '*'
|
||||||
# Excluye jail_audio.hpp, stb_image.h y stb_vorbis.c del analisis
|
# Headers nostres (excloem source/external/ que conté dependències de tercers no editables)
|
||||||
HeaderFilterRegex: 'source/(?!jail_audio\.hpp|stb_image\.h|stb_vorbis\.c).*'
|
HeaderFilterRegex: 'source/(core|game|utils)/'
|
||||||
FormatStyle: file
|
FormatStyle: file
|
||||||
|
|
||||||
CheckOptions:
|
CheckOptions:
|
||||||
# Variables locales en snake_case
|
# bugprone-empty-catch: aceptar catches vacíos marcados con @INTENTIONAL en un comentario
|
||||||
|
- { key: bugprone-empty-catch.IgnoreCatchWithKeywords, value: '@INTENTIONAL' }
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 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.VariableCase, value: lower_case }
|
||||||
|
- { key: readability-identifier-naming.LocalVariableCase, value: lower_case }
|
||||||
|
|
||||||
# Miembros privados en snake_case con sufijo _
|
# Parámetros de función
|
||||||
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
|
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
|
||||||
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
|
|
||||||
|
|
||||||
# Miembros protegidos en snake_case con sufijo _
|
# Variables estáticas no-const (static locales, static file-scope,
|
||||||
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
|
# y static members no-const de clase como el instance_ de un Singleton).
|
||||||
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
|
# Sufijo _ para marcar que tienen storage estático.
|
||||||
|
|
||||||
# Miembros publicos en snake_case (sin sufijo)
|
|
||||||
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
|
|
||||||
|
|
||||||
# Namespaces en CamelCase
|
|
||||||
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
|
|
||||||
|
|
||||||
# Variables estaticas privadas como miembros privados
|
|
||||||
- { key: readability-identifier-naming.StaticVariableCase, value: lower_case }
|
- { key: readability-identifier-naming.StaticVariableCase, value: lower_case }
|
||||||
- { key: readability-identifier-naming.StaticVariableSuffix, value: _ }
|
- { key: readability-identifier-naming.StaticVariableSuffix, value: _ }
|
||||||
|
|
||||||
# Constantes estaticas sin sufijo
|
# =====================================================================
|
||||||
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }
|
# MIEMBROS DE CLASE NO-CONST
|
||||||
|
# =====================================================================
|
||||||
|
# Privados: snake_case con sufijo _
|
||||||
|
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
|
||||||
|
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
|
||||||
|
|
||||||
# Constantes globales en UPPER_CASE
|
# Protegidos: snake_case con sufijo _
|
||||||
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
|
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
|
||||||
|
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
|
||||||
|
|
||||||
# Variables constexpr globales en UPPER_CASE
|
# Públicos: snake_case sin sufijo
|
||||||
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE }
|
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
|
||||||
|
|
||||||
# Constantes locales en UPPER_CASE
|
# =====================================================================
|
||||||
- { key: readability-identifier-naming.LocalConstantCase, value: UPPER_CASE }
|
# TIPOS
|
||||||
|
# =====================================================================
|
||||||
# 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
|
|
||||||
- { key: readability-identifier-naming.ClassCase, value: CamelCase }
|
- { key: readability-identifier-naming.ClassCase, value: CamelCase }
|
||||||
- { key: readability-identifier-naming.StructCase, value: CamelCase }
|
- { key: readability-identifier-naming.StructCase, value: CamelCase }
|
||||||
- { key: readability-identifier-naming.EnumCase, 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
|
# Namespaces
|
||||||
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
|
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
|
||||||
|
|
||||||
# Metodos 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.MethodCase, value: camelBack }
|
||||||
- { key: readability-identifier-naming.PrivateMethodCase, value: camelBack }
|
- { key: readability-identifier-naming.PrivateMethodCase, value: camelBack }
|
||||||
- { key: readability-identifier-naming.ProtectedMethodCase, value: camelBack }
|
- { key: readability-identifier-naming.ProtectedMethodCase, value: camelBack }
|
||||||
- { key: readability-identifier-naming.PublicMethodCase, value: camelBack }
|
- { key: readability-identifier-naming.PublicMethodCase, value: camelBack }
|
||||||
|
- { key: readability-identifier-naming.ConstexprMethodCase, value: camelBack }
|
||||||
# Funciones en camelBack
|
|
||||||
- { key: readability-identifier-naming.FunctionCase, value: camelBack }
|
|
||||||
|
|
||||||
# Parametros en lower_case
|
|
||||||
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
|
|
||||||
|
|||||||
Executable
+92
@@ -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
|
||||||
+4
-1
@@ -1,7 +1,7 @@
|
|||||||
.vscode
|
.vscode
|
||||||
build/
|
build/
|
||||||
|
compile_commands.json
|
||||||
dist/
|
dist/
|
||||||
data/config/config.txt
|
|
||||||
*.DS_Store
|
*.DS_Store
|
||||||
thumbs.db
|
thumbs.db
|
||||||
*.exe
|
*.exe
|
||||||
@@ -14,3 +14,6 @@ thumbs.db
|
|||||||
coffee_crisis
|
coffee_crisis
|
||||||
coffee_crisis_debug
|
coffee_crisis_debug
|
||||||
release/windows/coffee.res
|
release/windows/coffee.res
|
||||||
|
resources.pack
|
||||||
|
tools/pack_resources/pack_resources
|
||||||
|
.cache/
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v2.4.0
|
||||||
|
|
||||||
|
### Novetats principals
|
||||||
|
|
||||||
|
- **Migració a temps real (time-based)**. Tot el joc ja no depèn d'una cadència fixa de 60 frames per segon: els moviments, les animacions, les fades, els temporitzadors i els comptadors es calculen a partir del temps real transcorregut entre frames (`dt_s`). Això corregeix l'acceleració del jugador i les animacions en monitors de 144 Hz, i prepara el joc per a qualsevol freqüència de refresc. El refactor s'ha fet entitat per entitat (Bullet, Item, Player, Balloon) i, finalment, al motor principal `Game`.
|
||||||
|
- **Demo multi-set**. El sistema de demo s'ha portat al patró time-based de Coffee Crisis Arcade Edition: 3 fitxers de demo distints (`demo1.bin`, `demo2.bin`, `demo3.bin`) es trien aleatòriament en cada execució, l'índex es calcula a partir del temps acumulat i el playback és immune a salts de frames. En saltar-la amb una tecla es torna correctament al títol amb el menú visible.
|
||||||
|
- **Modes de presentació del canvas**. Nou cicle de presentació de la imatge a la finestra: `integer_scale` (escalat enter), `letterbox` (manté l'aspect ratio amb barres negres), `stretched` (omple la finestra deformant) i `overscan` (omple la finestra retallant). El valor antic `integer_scale` del config es migra automàticament.
|
||||||
|
|
||||||
|
### Hotkeys (notificacions visibles a la part superior del canvas)
|
||||||
|
|
||||||
|
| Tecla | Acció |
|
||||||
|
|------:|-------|
|
||||||
|
| F1 / F2 | Reduir / augmentar el zoom de la finestra |
|
||||||
|
| F3 | Alternar pantalla completa |
|
||||||
|
| F4 | Activar/desactivar post-procesat (shaders) |
|
||||||
|
| F5 | Alternar entre shader PostFX i CrtPi |
|
||||||
|
| F6 | Següent preset del shader actiu |
|
||||||
|
| F7 | Activar/desactivar V-Sync |
|
||||||
|
| F8 | Cicla el mode de presentació (integer_scale → letterbox → stretched → overscan) |
|
||||||
|
| F10 | Mostrar/amagar comptador de FPS (cantonada superior dreta) |
|
||||||
|
| F11 | Mostrar nom de l'app + versió + hash de git |
|
||||||
|
| F12 | Pausa |
|
||||||
|
| ESC | Eixir (doble pulsació per confirmar) |
|
||||||
|
| BACKSPACE | Cancel·lar a menús |
|
||||||
|
|
||||||
|
Les notificacions tenen ara una paleta semàntica (informació, toggle, confirmació, èxit, perill) i un color més saturat (a mig camí entre pastel i color pur).
|
||||||
|
|
||||||
|
### Millores
|
||||||
|
|
||||||
|
- **Cap tecla de funció ni ESC fa "saltar" cap secció**. Logo, intro, instruccions, títol i demo només es passen amb tecles humanes (Enter, disparar, moviment, botons de gamepad) — les F1–F12 i ESC es reserven íntegrament per als hotkeys globals.
|
||||||
|
- **Notificacions en mode overscan**. Quan el mode `overscan` retalla la franja superior del canvas segons l'aspect ratio de la finestra, les notificacions i el comptador de FPS es desplacen automàticament a la primera fila visible.
|
||||||
|
- **Versió única de l'aplicació**. La cadena de versió ja només viu a `source/utils/defines.hpp`; CMake l'extreu via regex per al projecte i el Makefile l'usa per als noms dels release. F11 mostra `Coffee Crisis v2.4.0 (<git_hash>)`.
|
||||||
|
- **Comptador de FPS** dibuixat a la part superior dreta en verd, recalcat cada segon de temps real.
|
||||||
|
- **Confirmació d'eixida (ESC × 2)** amb finestra visual de confirmació en roig.
|
||||||
|
- **Pausa amb compte enrere configurable** (`gameplay.pause_countdown` a `config.yaml`).
|
||||||
|
- **Zoom màxim de la finestra detectat automàticament** segons la resolució del display.
|
||||||
|
- **Paquet de recursos més robust**: `resources.pack` localitzat via `SDL_GetBasePath` amb fallback al filesystem.
|
||||||
|
- **Build estandarditzada**: llista explícita de fonts en lloc de `GLOB_RECURSE`, `-Wextra -Wpedantic` activats i warnings netejats.
|
||||||
|
- **`pre-commit` hooks** per `clang-format`, `clang-tidy` i `cppcheck`.
|
||||||
|
|
||||||
|
### Correccions de bugs
|
||||||
|
|
||||||
|
- Demo congelada en obrir-se per culpa d'un doble `DeltaTime::tick()` al títol (Game rebia `dt ≈ 0`).
|
||||||
|
- Salt visual al fons diagonal del títol per posició inicial no ancorada al cicle de tile.
|
||||||
|
- Rotació de la `PowerBall` perduda en passar per `Game::startAllBalloons` després del rellotge.
|
||||||
|
- Pausa que es disparava amb el flanc residual de CANCEL/EXIT en entrar al menú.
|
||||||
|
- Animació del jugador accelerada en monitors de 144 Hz (no propagava `dt_s` als sprites).
|
||||||
|
- Sub-bucles aniats de pausa, game over, instruccions i demo aplanats a un únic loop SDL3.
|
||||||
|
- WASM/Emscripten: reset en fer "exit", eliminades les opcions d'eixida, fix de fullscreen, mode `integer_scale=false` per defecte.
|
||||||
|
- Windows: parsers de text amb finals de línia CRLF, headers SPV del PostFX regenerats.
|
||||||
|
- `pack_resources` anava a la rel en comptes de `build/`.
|
||||||
|
|
||||||
|
### Canvis interns destacats
|
||||||
|
|
||||||
|
- **Pipeline SDL3 GPU + fallback `SDL_Renderer`**: shaders PostFX i CrtPi (Vulkan/Metal/D3D12), presets persistents en YAML, scanlines analítiques sense supersampling.
|
||||||
|
- **Sistema d'opcions modern**: tot a `config.yaml` amb fkyaml; `postfx.yaml` i `crtpi.yaml` per als presets.
|
||||||
|
- **Sistema d'àudio nou** (`Ja::` namespace, streaming d'OGG, tipus sense prefix `JA_`).
|
||||||
|
- **API SDL3 Callbacks** (`SDL_AppInit`/`Iterate`/`Event`/`Quit`).
|
||||||
|
- Migració a `enum class` per a Input::Action, Input::Device, Input::Repeat, Input::Disable, Fade::Type i Bullet::Kind.
|
||||||
|
- Convencions `CamelCase` aplicades a tipus que encara duien `_t`/`_e` o eren niats sense format.
|
||||||
|
- Singletons (Lang, Audio, Input, Resource) i sistema de loaders de recursos.
|
||||||
|
- Migració d'estructura a `source/core/{audio,input,locale,rendering,resources,system}/` i `source/game/{entities,scenes,ui}/`.
|
||||||
|
- Generació automàtica de `version.h` amb el hash curt de git.
|
||||||
|
- `clang-format` i `clang-tidy` unificats amb la resta de projectes germans.
|
||||||
@@ -4,67 +4,90 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
Coffee Crisis is a C++20 arcade game built with SDL2. The player controls a character defending the UPV (university) from bouncing coffee-ball enemies across 10 stages. Supports 1-2 players, keyboard and gamepad input, and multiple languages (Spanish, Basque, English).
|
Coffee Crisis is a C++20 arcade game built with SDL3. The player controls a character defending the UPV (university) from bouncing coffee-ball enemies across 10 stages. Supports 1-2 players, keyboard and gamepad input, and multiple languages (Spanish, Basque, English).
|
||||||
|
|
||||||
## Build Commands
|
## Build Commands
|
||||||
|
|
||||||
Dependencies: `libsdl2-dev` and `g++` (Linux) or `clang++` (macOS).
|
Dependencies: `libsdl3-dev` and `g++` (Linux) or `clang++` (macOS). Build system is CMake (driven by `Makefile` wrappers).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Linux
|
make # Release build
|
||||||
make linux # Release build → ./coffee_crisis
|
make debug # Debug build (defines DEBUG)
|
||||||
make linux_debug # Debug build (defines DEBUG and PAUSE) → ./coffee_crisis_debug
|
make release # Empaqueta .tar.gz / .dmg / .zip segons SO
|
||||||
|
make pack # Regenera resources.pack
|
||||||
# macOS
|
make compile_shaders # Compila shaders GLSL → headers SPIR-V (requereix glslc)
|
||||||
make macos # Release build with clang++
|
make controllerdb # Descarga gamecontrollerdb.txt
|
||||||
make macos_debug # Debug build
|
make format # clang-format -i
|
||||||
|
make tidy # clang-tidy
|
||||||
# Windows (MinGW)
|
make cppcheck # cppcheck
|
||||||
make windows # Release build → coffee_crisis.exe
|
|
||||||
make windows_debug # Debug build
|
|
||||||
|
|
||||||
# Release packaging
|
|
||||||
make linux_release # Builds and creates .tar.gz
|
|
||||||
make macos_release # Builds Intel + Apple Silicon .dmg files
|
|
||||||
make windows_release # Builds and creates .zip
|
|
||||||
```
|
```
|
||||||
|
|
||||||
There is also a CMakeLists.txt available as an alternative build system.
|
|
||||||
|
|
||||||
There are no tests or linter configured.
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
All source code is in `source/`. The game uses a section-based architecture controlled by the **Director** class:
|
Source layout (alineat amb la resta de projectes germans):
|
||||||
|
|
||||||
- **Director** (`director.h/cpp`): Top-level controller. Initializes SDL, manages the window/renderer, and runs sections in sequence: Logo → Intro → Title → Game → Quit. Owns all shared objects (Screen, Input, Lang, Asset).
|
```
|
||||||
- **Game** (`game.h/cpp`): Core gameplay logic. Manages players, balloons (enemies), bullets, items, stages, menace level, and collision detection. Contains its own update/render loop plus sub-loops for pause and game over screens.
|
source/
|
||||||
- **Screen** (`screen.h/cpp`): Rendering abstraction. Manages a virtual canvas (256×192) that gets scaled to the actual window. Handles fullscreen/windowed modes, border rendering, and fade effects.
|
├── main.cpp
|
||||||
- **Input** (`input.h/cpp`): Abstracts keyboard and gamepad input.
|
├── core/
|
||||||
- **Asset** (`asset.h/cpp`): Resource file index. Files are registered with `add()` and retrieved by name with `get()`. All paths are relative to the executable.
|
│ ├── audio/ jail_audio.hpp
|
||||||
- **Lang** (`lang.h/cpp`): i18n system loading text strings from files in `data/lang/`.
|
│ ├── input/ input.*, mouse.*
|
||||||
|
│ ├── locale/ lang.*
|
||||||
|
│ ├── rendering/ screen, fade, text, writer, texture, sprite + animated/moving/smart
|
||||||
|
│ │ ├── shader_backend.hpp (interfície abstracta de post-procesado)
|
||||||
|
│ │ └── sdl3gpu/ (pipeline SDL3 GPU)
|
||||||
|
│ │ ├── sdl3gpu_shader.* (implementació del backend GPU)
|
||||||
|
│ │ └── spv/ (headers SPIR-V generats — protegits amb dummies `.clang-*`)
|
||||||
|
│ ├── resources/ asset, resource, resource_pack, resource_loader, resource_helper
|
||||||
|
│ └── system/ director
|
||||||
|
├── game/
|
||||||
|
│ ├── defaults.hpp (constants de gameplay: block size, canvas, áreas, colors)
|
||||||
|
│ ├── game.* (hub de gameplay)
|
||||||
|
│ ├── entities/ player, balloon, bullet, item
|
||||||
|
│ ├── scenes/ logo, intro, title, instructions
|
||||||
|
│ └── ui/ menu
|
||||||
|
├── utils/
|
||||||
|
│ ├── defines.hpp (macros de build)
|
||||||
|
│ └── utils.* (helpers, enum de dificultat, circle_t, ...)
|
||||||
|
└── external/ (stb_image, stb_vorbis — protegits amb dummies `.clang-*`)
|
||||||
|
```
|
||||||
|
|
||||||
### Sprite hierarchy
|
Flux general controlat per la classe **Director** (`core/system/director.h`): inicialitza SDL, finestra/renderer i executa seccions en seqüència **Logo → Intro → Title → Game → Quit**. Les classes principals:
|
||||||
|
|
||||||
- **Sprite** → base class for drawing from a PNG spritesheet
|
- **Game** (`game/game.h`): gameplay nuclear. Gestiona jugadors, balloons, bullets, items, stages, nivell d'amenaça i col·lisions. Té el seu bucle d'update/render i sub-bucles per pausa i game-over.
|
||||||
- **AnimatedSprite** → extends Sprite with frame-based animation (loaded from `.ani` files)
|
- **Screen** (`core/rendering/screen.h`): abstracció de render. Canvas virtual 256×192 escalat a la finestra. Fullscreen/windowed, borders, fades.
|
||||||
- **MovingSprite** → sprite with movement
|
- **Input** (`core/input/input.h`): abstracció de teclat i gamepad.
|
||||||
- **SmartSprite** → sprite with autonomous behavior (score popups, thrown items)
|
- **Asset** (`core/resources/asset.h`): índex de fitxers de recurs (`add`/`get` per nom).
|
||||||
|
- **Lang** (`core/locale/lang.h`): i18n, carrega strings des de `data/lang/`.
|
||||||
|
|
||||||
### Game entities
|
### Sprite hierarchy (`core/rendering/`)
|
||||||
|
|
||||||
- **Player** (`player.h/cpp`): Player character state and rendering
|
- **Sprite** → base per dibuixar des d'un spritesheet PNG
|
||||||
- **Balloon** (`balloon.h/cpp`): Enemy entities with multiple types and split-on-pop behavior
|
- **AnimatedSprite** → afegeix animació per frames (arxius `.ani`)
|
||||||
- **Bullet** (`bullet.h/cpp`): Projectiles fired by the player (left/center/right)
|
- **MovingSprite** → sprite amb posició/velocitat
|
||||||
- **Item** (`item.h/cpp`): Collectible items (points, clock, coffee, power-ups)
|
- **SmartSprite** → sprite autònom (score popups, objectes llençats)
|
||||||
|
|
||||||
### Audio
|
### Audio
|
||||||
|
|
||||||
**jail_audio** (`jail_audio.h/cpp`): Custom audio library wrapping SDL2 audio. Uses stb_vorbis for OGG decoding. Provides `JA_*` functions for music and sound effects with channel-based mixing.
|
**jail_audio** (`core/audio/jail_audio.hpp`): wrapper audio SDL3 first-party. Usa stb_vorbis per OGG. API `JA_*` per música i efectes amb mesclat per canals.
|
||||||
|
|
||||||
### Key constants
|
### GPU / shaders (post-procesado)
|
||||||
|
|
||||||
Defined in `const.h`: block size (8px), virtual canvas (256×192), play area bounds, section/subsection IDs, and color definitions.
|
Pipeline SDL3 GPU portat de `coffee_crisis_arcade_edition`. El canvas 256×192 es pot passar per un backend GPU que aplica PostFX (vinyeta, scanlines, chroma, gamma, mask, curvatura, bleeding, flicker) o CrtPi (scanlines continues amb bloom). Fallback transparent al `SDL_Renderer` clàssic si la GPU falla o si es desactiva.
|
||||||
|
|
||||||
|
- **Interfície**: `core/rendering/shader_backend.hpp` (`Rendering::ShaderBackend`).
|
||||||
|
- **Implementació**: `core/rendering/sdl3gpu/sdl3gpu_shader.*` + shaders GLSL a `data/shaders/` compilats a `spv/*_spv.h` via `glslc` (o precompilats si no hi ha `glslc`).
|
||||||
|
- **Emscripten**: compile-time `NO_SHADERS` → sempre ruta clàssica.
|
||||||
|
- **macOS**: shaders Metal (MSL) inline dins `sdl3gpu_shader.cpp`; no SPIR-V.
|
||||||
|
- **Opcions persistents** a `config.txt` (migració a YAML pendent):
|
||||||
|
- `videoGpuAcceleration` (bool)
|
||||||
|
- `videoGpuPreferredDriver` (string, buit = auto)
|
||||||
|
- `videoShaderEnabled` (bool)
|
||||||
|
- `videoShaderType` (0=POSTFX, 1=CRTPI)
|
||||||
|
- **Hotkeys** (provisionals fins que hi hagi menú d'opcions): `F4` activa/desactiva post-procesado · `F5` alterna POSTFX ↔ CRTPI (només si està actiu) · `F6` següent preset (només si està actiu). No hi ha tecla per a preset anterior.
|
||||||
|
- **API** a `Screen`: `setGpuAcceleration`/`toggleGpuAcceleration`/`isGpuAccelerated`, `setShaderEnabled`/`toggleShaderEnabled`/`isShaderEnabled`, `setActiveShader`/`toggleActiveShader`/`getActiveShader`.
|
||||||
|
|
||||||
|
Presets PostFX/CrtPi i cicle de presets encara **no** estan implementats — arribaran amb la migració a YAML. Per ara, valors per defecte hardcoded.
|
||||||
|
|
||||||
## Data Directory
|
## Data Directory
|
||||||
|
|
||||||
@@ -72,8 +95,9 @@ Defined in `const.h`: block size (8px), virtual canvas (256×192), play area bou
|
|||||||
- `data/font/` — bitmap font files
|
- `data/font/` — bitmap font files
|
||||||
- `data/music/` and `data/sound/` — audio assets
|
- `data/music/` and `data/sound/` — audio assets
|
||||||
- `data/lang/` — language files (es_ES, ba_BA, en_UK)
|
- `data/lang/` — language files (es_ES, ba_BA, en_UK)
|
||||||
- `data/config/` — gamecontroller DB, demo recording data
|
- `data/demo/` — demo recording data (gamecontrollerdb.txt vive en la raíz del proyecto, fuera del pack)
|
||||||
- `data/menu/` — menu definition files
|
- `data/menu/` — menu definition files
|
||||||
|
- `data/shaders/` — fonts GLSL per al post-procesado SDL3 GPU (no van al pack; s'empotren al binari via headers SPIR-V)
|
||||||
|
|
||||||
## Language
|
## Language
|
||||||
|
|
||||||
|
|||||||
+285
-31
@@ -1,12 +1,23 @@
|
|||||||
# CMakeLists.txt
|
# CMakeLists.txt
|
||||||
|
|
||||||
cmake_minimum_required(VERSION 3.10)
|
cmake_minimum_required(VERSION 3.10)
|
||||||
project(coffee_crisis VERSION 1.00)
|
|
||||||
|
|
||||||
# Configuración de compilador para MinGW en Windows
|
# La versió de l'app es defineix una sola vegada a source/utils/defines.hpp
|
||||||
if(WIN32 AND NOT CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
|
# (Defines::VERSION). El Makefile ja la grepeja per als noms de release; aqui
|
||||||
set(CMAKE_CXX_COMPILER "g++")
|
# l'extreiem perque project(... VERSION ...) i tots els consumidors interns
|
||||||
set(CMAKE_C_COMPILER "gcc")
|
# de CMake (CPack, install, etc.) usin la mateixa font de veritat.
|
||||||
|
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/source/utils/defines.hpp" _DEFINES_CONTENT)
|
||||||
|
string(REGEX MATCH "VERSION = \"([0-9]+\\.[0-9]+\\.[0-9]+)\"" _ "${_DEFINES_CONTENT}")
|
||||||
|
set(APP_VERSION "${CMAKE_MATCH_1}")
|
||||||
|
if(APP_VERSION STREQUAL "")
|
||||||
|
message(FATAL_ERROR "No s'ha pogut extreure VERSION de source/utils/defines.hpp")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
project(coffee_crisis VERSION ${APP_VERSION})
|
||||||
|
|
||||||
|
# Tipus de build per defecte (Debug) si no se n'ha especificat cap
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Establecer estándar de C++
|
# Establecer estándar de C++
|
||||||
@@ -14,34 +25,194 @@ set(CMAKE_CXX_STANDARD 20)
|
|||||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
# Configuración global de flags de compilación
|
# --- GENERACIÓN DE VERSIÓN AUTOMÁTICA ---
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
|
# Si GIT_HASH se ha pasado desde fuera (p.ej. desde el Makefile via -DGIT_HASH=xxx),
|
||||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Os -ffunction-sections -fdata-sections")
|
# lo usamos tal cual. Esto evita problemas con Docker/emscripten, donde git aborta por
|
||||||
|
# "dubious ownership" en el volumen montado. En builds locales sin -DGIT_HASH, se
|
||||||
|
# resuelve aquí ejecutando git directamente.
|
||||||
|
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
|
||||||
|
find_package(Git QUIET)
|
||||||
|
if(GIT_FOUND)
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
OUTPUT_VARIABLE GIT_HASH
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
ERROR_QUIET
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
|
||||||
|
set(GIT_HASH "unknown")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Configurar archivo de versión
|
||||||
|
configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/version.h @ONLY)
|
||||||
|
|
||||||
# Define el directorio de los archivos fuente
|
# Define el directorio de los archivos fuente
|
||||||
set(DIR_SOURCES "${CMAKE_SOURCE_DIR}/source")
|
set(DIR_SOURCES "${CMAKE_SOURCE_DIR}/source")
|
||||||
|
|
||||||
# Cargar todos los archivos fuente en DIR_SOURCES
|
# --- LISTA EXPLÍCITA DE FUENTES ---
|
||||||
file(GLOB SOURCES "${DIR_SOURCES}/*.cpp")
|
set(APP_SOURCES
|
||||||
|
source/main.cpp
|
||||||
|
|
||||||
# Verificar si se encontraron archivos fuente
|
# --- core/audio ---
|
||||||
if(NOT SOURCES)
|
source/core/audio/audio.cpp
|
||||||
message(FATAL_ERROR "No se encontraron archivos fuente en ${DIR_SOURCES}.")
|
source/core/audio/audio_adapter.cpp
|
||||||
endif()
|
|
||||||
|
# --- core/input ---
|
||||||
|
source/core/input/global_inputs.cpp
|
||||||
|
source/core/input/input.cpp
|
||||||
|
source/core/input/mouse.cpp
|
||||||
|
|
||||||
|
# --- core/locale ---
|
||||||
|
source/core/locale/lang.cpp
|
||||||
|
|
||||||
|
# --- core/rendering ---
|
||||||
|
source/core/rendering/animatedsprite.cpp
|
||||||
|
source/core/rendering/fade.cpp
|
||||||
|
source/core/rendering/movingsprite.cpp
|
||||||
|
source/core/rendering/notifications.cpp
|
||||||
|
source/core/rendering/screen.cpp
|
||||||
|
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
||||||
|
source/core/rendering/smartsprite.cpp
|
||||||
|
source/core/rendering/sprite.cpp
|
||||||
|
source/core/rendering/text.cpp
|
||||||
|
source/core/rendering/texture.cpp
|
||||||
|
source/core/rendering/writer.cpp
|
||||||
|
|
||||||
|
# --- core/resources ---
|
||||||
|
source/core/resources/asset.cpp
|
||||||
|
source/core/resources/resource.cpp
|
||||||
|
source/core/resources/resource_helper.cpp
|
||||||
|
source/core/resources/resource_loader.cpp
|
||||||
|
source/core/resources/resource_pack.cpp
|
||||||
|
|
||||||
|
# --- core/system ---
|
||||||
|
source/core/system/delta_time.cpp
|
||||||
|
source/core/system/demo.cpp
|
||||||
|
source/core/system/director.cpp
|
||||||
|
|
||||||
|
# --- game ---
|
||||||
|
source/game/game.cpp
|
||||||
|
source/game/options.cpp
|
||||||
|
|
||||||
|
# --- game/entities ---
|
||||||
|
source/game/entities/balloon.cpp
|
||||||
|
source/game/entities/bullet.cpp
|
||||||
|
source/game/entities/item.cpp
|
||||||
|
source/game/entities/player.cpp
|
||||||
|
|
||||||
|
# --- game/scenes ---
|
||||||
|
source/game/scenes/instructions.cpp
|
||||||
|
source/game/scenes/intro.cpp
|
||||||
|
source/game/scenes/logo.cpp
|
||||||
|
source/game/scenes/title.cpp
|
||||||
|
|
||||||
|
# --- game/ui ---
|
||||||
|
source/game/ui/menu.cpp
|
||||||
|
|
||||||
|
# --- utils ---
|
||||||
|
source/utils/utils.cpp
|
||||||
|
)
|
||||||
|
|
||||||
# Configuración de SDL3
|
# Configuración de SDL3
|
||||||
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
|
if(EMSCRIPTEN)
|
||||||
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
|
# En Emscripten, SDL3 se compila desde source con FetchContent
|
||||||
|
include(FetchContent)
|
||||||
|
FetchContent_Declare(
|
||||||
|
SDL3
|
||||||
|
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
|
||||||
|
GIT_TAG release-3.4.4
|
||||||
|
GIT_SHALLOW TRUE
|
||||||
|
)
|
||||||
|
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_STATIC ON CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE)
|
||||||
|
FetchContent_MakeAvailable(SDL3)
|
||||||
|
message(STATUS "SDL3 compilado desde source para Emscripten")
|
||||||
|
else()
|
||||||
|
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
|
||||||
|
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
|
||||||
|
endif()
|
||||||
|
|
||||||
# Configuración común de salida de ejecutables en el directorio raíz
|
# --- SHADER COMPILATION (Linux/Windows only - macOS uses Metal, Emscripten no soporta SDL3 GPU) ---
|
||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
|
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/spv")
|
||||||
|
|
||||||
|
set(ALL_SHADER_HEADERS
|
||||||
|
"${HEADERS_DIR}/postfx_vert_spv.h"
|
||||||
|
"${HEADERS_DIR}/postfx_frag_spv.h"
|
||||||
|
"${HEADERS_DIR}/crtpi_frag_spv.h"
|
||||||
|
)
|
||||||
|
set(ALL_SHADER_SOURCES
|
||||||
|
"${SHADERS_DIR}/postfx.vert"
|
||||||
|
"${SHADERS_DIR}/postfx.frag"
|
||||||
|
"${SHADERS_DIR}/crtpi_frag.glsl"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(GLSLC_EXE)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${ALL_SHADER_HEADERS}
|
||||||
|
COMMAND ${CMAKE_COMMAND}
|
||||||
|
-D GLSLC=${GLSLC_EXE}
|
||||||
|
-D SHADERS_DIR=${SHADERS_DIR}
|
||||||
|
-D HEADERS_DIR=${HEADERS_DIR}
|
||||||
|
-P ${CMAKE_SOURCE_DIR}/tools/shaders/compile_spirv.cmake
|
||||||
|
DEPENDS ${ALL_SHADER_SOURCES}
|
||||||
|
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||||
|
COMMENT "Compilando shaders SPIR-V..."
|
||||||
|
)
|
||||||
|
add_custom_target(shaders DEPENDS ${ALL_SHADER_HEADERS})
|
||||||
|
message(STATUS "glslc encontrado: shaders se compilarán automáticamente")
|
||||||
|
else()
|
||||||
|
foreach(HDR ${ALL_SHADER_HEADERS})
|
||||||
|
if(NOT EXISTS "${HDR}")
|
||||||
|
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)"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
message(STATUS "glslc no encontrado - usando headers SPIR-V precompilados")
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
if(EMSCRIPTEN)
|
||||||
|
message(STATUS "Emscripten: shaders SPIR-V omitidos (SDL3 GPU no soportado en WebGL2)")
|
||||||
|
else()
|
||||||
|
message(STATUS "macOS: shaders SPIR-V omitidos (usa Metal inline)")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
# Añadir ejecutable principal
|
# Añadir ejecutable principal
|
||||||
add_executable(${PROJECT_NAME} ${SOURCES})
|
if(EMSCRIPTEN)
|
||||||
|
# En Emscripten no compilem sdl3gpu_shader (SDL3 GPU no està suportat en WebGL2)
|
||||||
|
set(APP_SOURCES_WASM ${APP_SOURCES})
|
||||||
|
list(REMOVE_ITEM APP_SOURCES_WASM source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp)
|
||||||
|
add_executable(${PROJECT_NAME} ${APP_SOURCES_WASM})
|
||||||
|
else()
|
||||||
|
add_executable(${PROJECT_NAME} ${APP_SOURCES})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT APPLE AND NOT EMSCRIPTEN AND GLSLC_EXE)
|
||||||
|
add_dependencies(${PROJECT_NAME} shaders)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Includes relatius a source/ (p.e. `#include "core/rendering/texture.h"`)
|
||||||
|
# ${CMAKE_BINARY_DIR} per al version.h generat per configure_file.
|
||||||
|
target_include_directories(${PROJECT_NAME} PRIVATE ${DIR_SOURCES} ${CMAKE_BINARY_DIR})
|
||||||
|
|
||||||
|
# Flags de compilació per-target
|
||||||
|
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:-Os -ffunction-sections -fdata-sections>)
|
||||||
|
|
||||||
# Añadir definiciones de compilación dependiendo del tipo de build
|
# Añadir definiciones de compilación dependiendo del tipo de build
|
||||||
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
||||||
$<$<CONFIG:DEBUG>:DEBUG PAUSE>
|
$<$<CONFIG:DEBUG>:DEBUG>
|
||||||
$<$<CONFIG:RELEASE>:RELEASE_BUILD>
|
$<$<CONFIG:RELEASE>:RELEASE_BUILD>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,6 +237,19 @@ elseif(APPLE)
|
|||||||
-rpath @executable_path/../Frameworks/
|
-rpath @executable_path/../Frameworks/
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
elseif(EMSCRIPTEN)
|
||||||
|
target_compile_definitions(${PROJECT_NAME} PRIVATE EMSCRIPTEN_BUILD NO_SHADERS)
|
||||||
|
# En wasm NO empaquetamos un resources.pack: el propio --preload-file de
|
||||||
|
# emscripten ya hace el mismo trabajo (bundle del directorio en un .data),
|
||||||
|
# así que metemos directamente 'data' y dejamos que el Resource lea por
|
||||||
|
# filesystem (MEMFS). Evita doble empaquetado y el uso de memoria extra.
|
||||||
|
target_link_options(${PROJECT_NAME} PRIVATE
|
||||||
|
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/data@/data"
|
||||||
|
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt@/gamecontrollerdb.txt"
|
||||||
|
-sALLOW_MEMORY_GROWTH=1
|
||||||
|
-sMAX_WEBGL_VERSION=2
|
||||||
|
)
|
||||||
|
set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".html")
|
||||||
elseif(UNIX AND NOT APPLE)
|
elseif(UNIX AND NOT APPLE)
|
||||||
target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD)
|
target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD)
|
||||||
target_link_options(${PROJECT_NAME} PRIVATE -Wl,--gc-sections)
|
target_link_options(${PROJECT_NAME} PRIVATE -Wl,--gc-sections)
|
||||||
@@ -77,6 +261,7 @@ endif()
|
|||||||
|
|
||||||
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
|
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
|
||||||
find_program(CLANG_FORMAT_EXE NAMES clang-format)
|
find_program(CLANG_FORMAT_EXE NAMES clang-format)
|
||||||
|
find_program(CPPCHECK_EXE NAMES cppcheck)
|
||||||
|
|
||||||
# Recopilar todos los archivos fuente para analisis
|
# Recopilar todos los archivos fuente para analisis
|
||||||
file(GLOB_RECURSE ALL_SOURCE_FILES
|
file(GLOB_RECURSE ALL_SOURCE_FILES
|
||||||
@@ -84,17 +269,12 @@ file(GLOB_RECURSE ALL_SOURCE_FILES
|
|||||||
"${CMAKE_SOURCE_DIR}/source/*.h"
|
"${CMAKE_SOURCE_DIR}/source/*.h"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Excluir stb_image.h y stb_vorbis.c del analisis
|
|
||||||
set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES})
|
set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES})
|
||||||
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*stb_image\\.h$")
|
|
||||||
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*stb_vorbis\\.c$")
|
|
||||||
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*jail_audio\\.hpp$")
|
|
||||||
|
|
||||||
# Excluir stb y jail_audio del formateo tambien
|
# Para cppcheck, pasar solo .cpp (los headers se procesan transitivamente).
|
||||||
set(FORMAT_SOURCES ${ALL_SOURCE_FILES})
|
set(CPPCHECK_SOURCES ${ALL_SOURCE_FILES})
|
||||||
list(FILTER FORMAT_SOURCES EXCLUDE REGEX ".*stb_image\\.h$")
|
list(FILTER CPPCHECK_SOURCES INCLUDE REGEX ".*\\.cpp$")
|
||||||
list(FILTER FORMAT_SOURCES EXCLUDE REGEX ".*stb_vorbis\\.c$")
|
list(FILTER CPPCHECK_SOURCES EXCLUDE REGEX ".*/source/external/.*")
|
||||||
list(FILTER FORMAT_SOURCES EXCLUDE REGEX ".*jail_audio\\.hpp$")
|
|
||||||
|
|
||||||
# Targets de clang-tidy
|
# Targets de clang-tidy
|
||||||
if(CLANG_TIDY_EXE)
|
if(CLANG_TIDY_EXE)
|
||||||
@@ -123,7 +303,7 @@ if(CLANG_FORMAT_EXE)
|
|||||||
add_custom_target(format
|
add_custom_target(format
|
||||||
COMMAND ${CLANG_FORMAT_EXE}
|
COMMAND ${CLANG_FORMAT_EXE}
|
||||||
-i
|
-i
|
||||||
${FORMAT_SOURCES}
|
${ALL_SOURCE_FILES}
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
COMMENT "Running clang-format..."
|
COMMENT "Running clang-format..."
|
||||||
)
|
)
|
||||||
@@ -132,10 +312,84 @@ if(CLANG_FORMAT_EXE)
|
|||||||
COMMAND ${CLANG_FORMAT_EXE}
|
COMMAND ${CLANG_FORMAT_EXE}
|
||||||
--dry-run
|
--dry-run
|
||||||
--Werror
|
--Werror
|
||||||
${FORMAT_SOURCES}
|
${ALL_SOURCE_FILES}
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
COMMENT "Checking clang-format..."
|
COMMENT "Checking clang-format..."
|
||||||
)
|
)
|
||||||
else()
|
else()
|
||||||
message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles")
|
message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
# --- EINA STANDALONE: pack_resources ---
|
||||||
|
# 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
|
||||||
|
# Després executar: ./build/pack_resources data resources.pack
|
||||||
|
if(NOT EMSCRIPTEN)
|
||||||
|
add_executable(pack_resources EXCLUDE_FROM_ALL
|
||||||
|
tools/pack_resources/pack_resources.cpp
|
||||||
|
source/core/resources/resource_pack.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
|
||||||
|
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
|
||||||
|
# Regeneració automàtica de resources.pack en cada build si canvia data/.
|
||||||
|
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
|
||||||
|
set(RESOURCE_PACK "${CMAKE_BINARY_DIR}/resources.pack")
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
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(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()
|
||||||
|
|||||||
@@ -2,20 +2,42 @@
|
|||||||
# DIRECTORIES
|
# DIRECTORIES
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
|
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
|
||||||
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
|
BUILDDIR := build
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# TARGET NAMES
|
# TARGET NAMES
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
TARGET_NAME := coffee_crisis
|
TARGET_NAME := coffee_crisis
|
||||||
TARGET_FILE := $(DIR_BIN)$(TARGET_NAME)
|
TARGET_FILE := $(BUILDDIR)/$(TARGET_NAME)
|
||||||
APP_NAME := Coffee Crisis
|
APP_NAME := Coffee Crisis
|
||||||
VERSION := v2.3.3
|
|
||||||
DIST_DIR := dist
|
DIST_DIR := dist
|
||||||
RELEASE_FOLDER := dist/_tmp
|
RELEASE_FOLDER := dist/_tmp
|
||||||
RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
|
RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
|
||||||
RESOURCE_FILE := release/windows/coffee.res
|
RESOURCE_FILE := release/windows/coffee.res
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# VERSION (extracted from defines.hpp)
|
||||||
|
# ==============================================================================
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
VERSION := $(shell powershell -Command "(Select-String -Path 'source/utils/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value")
|
||||||
|
else
|
||||||
|
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
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# RELEASE NAMES
|
# RELEASE NAMES
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -50,47 +72,99 @@ endif
|
|||||||
# WINDOWS-SPECIFIC VARIABLES
|
# WINDOWS-SPECIFIC VARIABLES
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
ifeq ($(OS),Windows_NT)
|
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 := $(RELEASE_FOLDER)/$(APP_NAME)
|
||||||
|
# Escapa apòstrofs per a PowerShell (duplica ' → ''). Sense això, APP_NAMEs
|
||||||
|
# com "JailDoctor's Dilemma" trencarien el parsing de -Destination '...'.
|
||||||
|
WIN_RELEASE_FILE_PS := $(subst ','',$(WIN_RELEASE_FILE))
|
||||||
else
|
else
|
||||||
WIN_TARGET_FILE := $(TARGET_FILE)
|
WIN_TARGET_FILE := $(TARGET_FILE)
|
||||||
WIN_RELEASE_FILE := $(RELEASE_FILE)
|
WIN_RELEASE_FILE := $(RELEASE_FILE)
|
||||||
|
WIN_RELEASE_FILE_PS := $(WIN_RELEASE_FILE)
|
||||||
|
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
|
endif
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# COMPILACIÓN CON CMAKE
|
# COMPILACIÓN CON CMAKE
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
all:
|
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
|
@cmake --build build
|
||||||
|
|
||||||
debug:
|
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
|
@cmake --build build
|
||||||
|
|
||||||
|
run: all
|
||||||
|
@./$(TARGET_FILE)
|
||||||
|
|
||||||
|
run-debug: debug
|
||||||
|
@./$(TARGET_FILE)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@rm -rf $(BUILDDIR)
|
||||||
|
|
||||||
|
rebuild: clean all
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# EMPAQUETADO DE RECURSOS (build previ de l'eina + execució)
|
||||||
|
# ==============================================================================
|
||||||
|
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
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# RELEASE AUTOMÁTICO (detecta SO)
|
# RELEASE AUTOMÁTICO (detecta SO)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
release:
|
release:
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
@"$(MAKE)" windows_release
|
@"$(MAKE)" _windows-release
|
||||||
else
|
else
|
||||||
ifeq ($(UNAME_S),Darwin)
|
ifeq ($(UNAME_S),Darwin)
|
||||||
@$(MAKE) macos_release
|
@$(MAKE) _macos-release
|
||||||
else
|
else
|
||||||
@$(MAKE) linux_release
|
@$(MAKE) _linux-release
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# COMPILACIÓN PARA WINDOWS (RELEASE)
|
# COMPILACIÓN PARA WINDOWS (RELEASE)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
windows_release:
|
_windows-release:
|
||||||
|
@$(MAKE) pack
|
||||||
@echo off
|
@echo off
|
||||||
@echo Creando release para Windows - Version: $(VERSION)
|
@echo Creando release para Windows - Version: $(VERSION)
|
||||||
|
|
||||||
# Compila con cmake
|
# Compila con cmake
|
||||||
@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
|
@cmake --build build
|
||||||
|
|
||||||
# Crea carpeta de distribución y carpeta temporal
|
# Crea carpeta de distribución y carpeta temporal
|
||||||
@@ -99,11 +173,12 @@ windows_release:
|
|||||||
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
|
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
|
||||||
|
|
||||||
# Copia ficheros
|
# Copia ficheros
|
||||||
@powershell -Command "Copy-Item -Path 'data' -Destination '$(RELEASE_FOLDER)' -Recurse -Force"
|
@powershell -Command "Copy-Item 'build/resources.pack' -Destination '$(RELEASE_FOLDER)'"
|
||||||
|
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
|
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
|
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
|
@powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "Copy-Item -Path '$(TARGET_FILE)' -Destination '\"$(WIN_RELEASE_FILE).exe\"'"
|
@powershell -Command "Copy-Item -Path '$(TARGET_FILE).exe' -Destination '$(WIN_RELEASE_FILE_PS).exe'"
|
||||||
strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
|
strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
|
||||||
|
|
||||||
# Crea el fichero .zip
|
# Crea el fichero .zip
|
||||||
@@ -117,15 +192,32 @@ windows_release:
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# COMPILACIÓN PARA MACOS (RELEASE)
|
# COMPILACIÓN PARA MACOS (RELEASE)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
macos_release:
|
_macos-release:
|
||||||
|
@$(MAKE) pack
|
||||||
@echo "Creando release para macOS - Version: $(VERSION)"
|
@echo "Creando release para macOS - Version: $(VERSION)"
|
||||||
|
|
||||||
# Verificar e instalar create-dmg si es necesario
|
# Verifica dependencias necesarias (create-dmg). Si falta, intenta instalarla
|
||||||
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg)
|
# con brew; si brew tampoco está, indica el comando exacto al usuario.
|
||||||
|
@command -v create-dmg >/dev/null 2>&1 || { \
|
||||||
# Compila la versión para procesadores Intel con cmake
|
echo ""; \
|
||||||
@cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON
|
echo "============================================"; \
|
||||||
@cmake --build build/intel
|
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
|
# Elimina datos de compilaciones anteriores
|
||||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||||
@@ -140,7 +232,8 @@ macos_release:
|
|||||||
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
|
|
||||||
# Copia carpetas y ficheros
|
# Copia carpetas y ficheros
|
||||||
cp -R data "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
cp build/resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
|
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
|
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/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
|
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
|
||||||
@@ -153,31 +246,50 @@ 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>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"
|
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
|
# Compila y empaqueta la versión Intel (best-effort: si falla, se omite el
|
||||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
# DMG Intel y continúa con la build de Apple Silicon).
|
||||||
|
@echo ""
|
||||||
# Firma la aplicación
|
@echo "============================================"
|
||||||
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
|
@echo " Compilando version Intel (x86_64)"
|
||||||
|
@echo "============================================"
|
||||||
# Empaqueta el .dmg de la versión Intel con create-dmg
|
@if cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release \
|
||||||
@echo "Creando DMG Intel..."
|
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
|
||||||
create-dmg \
|
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \
|
||||||
--volname "$(APP_NAME)" \
|
-DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH) \
|
||||||
--window-pos 200 120 \
|
&& cmake --build build/intel; then \
|
||||||
--window-size 720 300 \
|
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"; \
|
||||||
--icon-size 96 \
|
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"; \
|
||||||
--text-size 12 \
|
echo "Creando DMG Intel..."; \
|
||||||
--icon "$(APP_NAME).app" 278 102 \
|
create-dmg \
|
||||||
--icon "LICENSE" 441 102 \
|
--volname "$(APP_NAME)" \
|
||||||
--icon "README.md" 604 102 \
|
--window-pos 200 120 \
|
||||||
--app-drop-link 115 102 \
|
--window-size 720 300 \
|
||||||
--hide-extension "$(APP_NAME).app" \
|
--icon-size 96 \
|
||||||
"$(MACOS_INTEL_RELEASE)" \
|
--text-size 12 \
|
||||||
"$(RELEASE_FOLDER)" || true
|
--icon "$(APP_NAME).app" 278 102 \
|
||||||
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"
|
--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
|
# 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
|
@cmake --build build/arm
|
||||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
||||||
|
|
||||||
@@ -210,11 +322,12 @@ macos_release:
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# COMPILACIÓN PARA LINUX (RELEASE)
|
# COMPILACIÓN PARA LINUX (RELEASE)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
linux_release:
|
_linux-release:
|
||||||
|
@$(MAKE) pack
|
||||||
@echo "Creando release para Linux - Version: $(VERSION)"
|
@echo "Creando release para Linux - Version: $(VERSION)"
|
||||||
|
|
||||||
# Compila con cmake
|
# Compila con cmake
|
||||||
@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
|
@cmake --build build
|
||||||
|
|
||||||
# Elimina carpeta temporal previa y la recrea
|
# Elimina carpeta temporal previa y la recrea
|
||||||
@@ -222,7 +335,8 @@ linux_release:
|
|||||||
$(MKDIR) "$(RELEASE_FOLDER)"
|
$(MKDIR) "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
# Copia ficheros
|
# Copia ficheros
|
||||||
cp -R data "$(RELEASE_FOLDER)"
|
cp build/resources.pack "$(RELEASE_FOLDER)"
|
||||||
|
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
|
||||||
cp LICENSE "$(RELEASE_FOLDER)"
|
cp LICENSE "$(RELEASE_FOLDER)"
|
||||||
cp README.md "$(RELEASE_FOLDER)"
|
cp README.md "$(RELEASE_FOLDER)"
|
||||||
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
|
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
|
||||||
@@ -236,10 +350,94 @@ linux_release:
|
|||||||
# Elimina la carpeta temporal
|
# Elimina la carpeta temporal
|
||||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# COMPILACIÓN PARA WEBASSEMBLY (requiere Docker)
|
||||||
|
# ==============================================================================
|
||||||
|
wasm:
|
||||||
|
@$(MAKE) pack
|
||||||
|
@echo "Compilando para WebAssembly - Version: $(VERSION)"
|
||||||
|
docker run --rm \
|
||||||
|
--user $(shell id -u):$(shell id -g) \
|
||||||
|
-v $(DIR_ROOT):/src \
|
||||||
|
-w /src \
|
||||||
|
emscripten/emsdk:latest \
|
||||||
|
bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm"
|
||||||
|
$(MKDIR) "$(DIST_DIR)/wasm"
|
||||||
|
cp build/wasm/$(TARGET_NAME).html $(DIST_DIR)/wasm/
|
||||||
|
cp build/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/
|
||||||
|
cp build/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/
|
||||||
|
cp build/wasm/$(TARGET_NAME).data $(DIST_DIR)/wasm/
|
||||||
|
@echo "Output: $(DIST_DIR)/wasm/"
|
||||||
|
scp $(DIST_DIR)/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/$(TARGET_NAME).data \
|
||||||
|
maverick:/home/sergio/gitea/web_jailgames/static/games/coffee-crisis/wasm/
|
||||||
|
ssh maverick 'cd /home/sergio/gitea/web_jailgames && ./deploy.sh'
|
||||||
|
@echo "Deployed to maverick"
|
||||||
|
|
||||||
|
# Versió Debug del build wasm: build local sense deploy. Sortida a dist/wasm-debug/.
|
||||||
|
wasm-debug:
|
||||||
|
@$(MAKE) pack
|
||||||
|
@echo "Compilando WebAssembly Debug - 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-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
|
||||||
|
|
||||||
|
# SHADERS (SPIR-V) — sólo Linux/Windows. Requiere glslc en el PATH.
|
||||||
|
compile-shaders:
|
||||||
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
|
@cmake --build build --target shaders
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# 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
|
# REGLAS ESPECIALES
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
show_version:
|
show-version:
|
||||||
@echo "Version actual: $(VERSION)"
|
@echo "Version actual: $(VERSION)"
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@@ -250,14 +448,32 @@ help:
|
|||||||
@echo " make - Compilar con cmake (Release)"
|
@echo " make - Compilar con cmake (Release)"
|
||||||
@echo " make debug - Compilar con cmake (Debug)"
|
@echo " make debug - Compilar con cmake (Debug)"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
@echo " Ejecucion:"
|
||||||
|
@echo " make run - Compilar (Release) y ejecutar"
|
||||||
|
@echo " make run-debug - Compilar (Debug) y ejecutar"
|
||||||
|
@echo ""
|
||||||
@echo " Release:"
|
@echo " Release:"
|
||||||
@echo " make release - Crear release (detecta SO automaticamente)"
|
@echo " make release - Crear release (detecta SO automaticamente)"
|
||||||
@echo " make windows_release - Crear release para Windows"
|
@echo " make wasm - Compilar para WebAssembly (requiere Docker) y deploy a maverick"
|
||||||
@echo " make linux_release - Crear release para Linux"
|
@echo " make wasm-debug - Compilar WebAssembly Debug local (sin deploy)"
|
||||||
@echo " make macos_release - Crear release para macOS"
|
@echo ""
|
||||||
|
@echo " Herramientas:"
|
||||||
|
@echo " make pack - Empaquetar recursos a $(BUILDDIR)/resources.pack"
|
||||||
|
@echo " make compile-shaders - Compilar shaders GLSL a headers SPIR-V (requiere glslc)"
|
||||||
|
@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 ""
|
||||||
@echo " Otros:"
|
@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"
|
@echo " make help - Mostrar esta ayuda"
|
||||||
|
|
||||||
.PHONY: all debug release windows_release macos_release linux_release show_version help
|
.PHONY: all debug run run-debug clean rebuild release _windows-release _macos-release _linux-release wasm wasm-debug controllerdb pack format format-check tidy tidy-fix cppcheck compile-shaders hooks-install show-version help
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
+25
-1
@@ -140,7 +140,7 @@ CONTINUAR?
|
|||||||
CONTINUAR
|
CONTINUAR
|
||||||
|
|
||||||
## 47 - MENU DE PAUSA
|
## 47 - MENU DE PAUSA
|
||||||
EIXIR DEL JOC
|
TORNAR AL TITOL
|
||||||
|
|
||||||
## 48 - MENU GAME OVER
|
## 48 - MENU GAME OVER
|
||||||
SI
|
SI
|
||||||
@@ -279,3 +279,27 @@ MODE FORA DE LINEA
|
|||||||
|
|
||||||
## 93 - MENU OPCIONES
|
## 93 - MENU OPCIONES
|
||||||
TAULER DE PUNTS
|
TAULER DE PUNTS
|
||||||
|
|
||||||
|
## 94 - NOTIFICACIO COMANDAMENT
|
||||||
|
CONNECTAT
|
||||||
|
|
||||||
|
## 95 - NOTIFICACIO COMANDAMENT
|
||||||
|
DESCONNECTAT
|
||||||
|
|
||||||
|
## 96 - NOTIFICACIO HOTKEY
|
||||||
|
Zoom
|
||||||
|
|
||||||
|
## 97 - NOTIFICACIO HOTKEY
|
||||||
|
Pantalla completa
|
||||||
|
|
||||||
|
## 98 - NOTIFICACIO HOTKEY
|
||||||
|
Finestra
|
||||||
|
|
||||||
|
## 99 - NOTIFICACIO HOTKEY
|
||||||
|
Shader
|
||||||
|
|
||||||
|
## 100 - NOTIFICACIO HOTKEY
|
||||||
|
Preset
|
||||||
|
|
||||||
|
## 101 - NOTIFICACIO HOTKEY
|
||||||
|
Torna a premer ESC per a eixir
|
||||||
+25
-1
@@ -140,7 +140,7 @@ CONTINUE?
|
|||||||
CONTINUE
|
CONTINUE
|
||||||
|
|
||||||
## 47 - MENU DE PAUSA
|
## 47 - MENU DE PAUSA
|
||||||
LEAVE GAME
|
BACK TO TITLE
|
||||||
|
|
||||||
## 48 - MENU GAME OVER
|
## 48 - MENU GAME OVER
|
||||||
YES
|
YES
|
||||||
@@ -279,3 +279,27 @@ OFFLINE MODE
|
|||||||
|
|
||||||
## 93 - MENU OPCIONES
|
## 93 - MENU OPCIONES
|
||||||
HISCORE TABLE
|
HISCORE TABLE
|
||||||
|
|
||||||
|
## 94 - GAMEPAD NOTIFICATION
|
||||||
|
CONNECTED
|
||||||
|
|
||||||
|
## 95 - GAMEPAD NOTIFICATION
|
||||||
|
DISCONNECTED
|
||||||
|
|
||||||
|
## 96 - HOTKEY NOTIFICATION
|
||||||
|
Zoom
|
||||||
|
|
||||||
|
## 97 - HOTKEY NOTIFICATION
|
||||||
|
Fullscreen
|
||||||
|
|
||||||
|
## 98 - HOTKEY NOTIFICATION
|
||||||
|
Window
|
||||||
|
|
||||||
|
## 99 - HOTKEY NOTIFICATION
|
||||||
|
Shader
|
||||||
|
|
||||||
|
## 100 - HOTKEY NOTIFICATION
|
||||||
|
Preset
|
||||||
|
|
||||||
|
## 101 - HOTKEY NOTIFICATION
|
||||||
|
Press ESC again to quit
|
||||||
+25
-1
@@ -140,7 +140,7 @@ CONTINUAR?
|
|||||||
CONTINUAR
|
CONTINUAR
|
||||||
|
|
||||||
## 47 - MENU DE PAUSA
|
## 47 - MENU DE PAUSA
|
||||||
SALIR DEL JUEGO
|
VOLVER AL TITULO
|
||||||
|
|
||||||
## 48 - MENU GAME OVER
|
## 48 - MENU GAME OVER
|
||||||
SI
|
SI
|
||||||
@@ -279,3 +279,27 @@ MODO SIN CONEXION
|
|||||||
|
|
||||||
## 93 - MENU OPCIONES
|
## 93 - MENU OPCIONES
|
||||||
TABLA DE PUNTUACIONES
|
TABLA DE PUNTUACIONES
|
||||||
|
|
||||||
|
## 94 - NOTIFICACION MANDO
|
||||||
|
CONECTADO
|
||||||
|
|
||||||
|
## 95 - NOTIFICACION MANDO
|
||||||
|
DESCONECTADO
|
||||||
|
|
||||||
|
## 96 - NOTIFICACION HOTKEY
|
||||||
|
Zoom
|
||||||
|
|
||||||
|
## 97 - NOTIFICACION HOTKEY
|
||||||
|
Pantalla completa
|
||||||
|
|
||||||
|
## 98 - NOTIFICACION HOTKEY
|
||||||
|
Ventana
|
||||||
|
|
||||||
|
## 99 - NOTIFICACION HOTKEY
|
||||||
|
Shader
|
||||||
|
|
||||||
|
## 100 - NOTIFICACION HOTKEY
|
||||||
|
Preset
|
||||||
|
|
||||||
|
## 101 - NOTIFICACION HOTKEY
|
||||||
|
Vuelve a pulsar ESC para salir
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// Vulkan GLSL fragment shader — CRT-Pi PostFX
|
||||||
|
// Algoritmo de scanlines continuas con pesos gaussianos, bloom y máscara de fósforo.
|
||||||
|
// Basado en el shader CRT-Pi original (GLSL 3.3), portado a GLSL 4.50 con parámetros uniformes.
|
||||||
|
//
|
||||||
|
// Compile: glslc -fshader-stage=frag --target-env=vulkan1.0 crtpi_frag.glsl -o crtpi_frag.spv
|
||||||
|
// xxd -i crtpi_frag.spv > ../../source/core/rendering/sdl3gpu/crtpi_frag_spv.h
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 v_uv;
|
||||||
|
layout(location = 0) out vec4 out_color;
|
||||||
|
|
||||||
|
layout(set = 2, binding = 0) uniform sampler2D Texture;
|
||||||
|
|
||||||
|
layout(set = 3, binding = 0) uniform CrtPiBlock {
|
||||||
|
// vec4 #0
|
||||||
|
float scanline_weight; // Ajuste gaussiano de scanlines (default 6.0)
|
||||||
|
float scanline_gap_brightness; // Brillo mínimo entre scanlines (default 0.12)
|
||||||
|
float bloom_factor; // Factor de brillo en zonas iluminadas (default 3.5)
|
||||||
|
float input_gamma; // Gamma de entrada — linealización (default 2.4)
|
||||||
|
// vec4 #1
|
||||||
|
float output_gamma; // Gamma de salida — codificación (default 2.2)
|
||||||
|
float mask_brightness; // Brillo sub-píxeles de la máscara (default 0.80)
|
||||||
|
float curvature_x; // Distorsión barrel eje X (default 0.05)
|
||||||
|
float curvature_y; // Distorsión barrel eje Y (default 0.10)
|
||||||
|
// vec4 #2
|
||||||
|
int mask_type; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
|
||||||
|
int enable_scanlines; // 0 = off, 1 = on
|
||||||
|
int enable_multisample; // 0 = off, 1 = on (antialiasing analítico de scanlines)
|
||||||
|
int enable_gamma; // 0 = off, 1 = on
|
||||||
|
// vec4 #3
|
||||||
|
int enable_curvature; // 0 = off, 1 = on
|
||||||
|
int enable_sharper; // 0 = off, 1 = on
|
||||||
|
float texture_width; // Ancho del canvas lógico en píxeles
|
||||||
|
float texture_height; // Alto del canvas lógico en píxeles
|
||||||
|
} u;
|
||||||
|
|
||||||
|
// Distorsión barrel CRT
|
||||||
|
vec2 distort(vec2 coord, vec2 screen_scale) {
|
||||||
|
vec2 curvature = vec2(u.curvature_x, u.curvature_y);
|
||||||
|
vec2 barrel_scale = 1.0 - (0.23 * curvature);
|
||||||
|
coord *= screen_scale;
|
||||||
|
coord -= vec2(0.5);
|
||||||
|
float rsq = coord.x * coord.x + coord.y * coord.y;
|
||||||
|
coord += coord * (curvature * rsq);
|
||||||
|
coord *= barrel_scale;
|
||||||
|
if (abs(coord.x) >= 0.5 || abs(coord.y) >= 0.5) {
|
||||||
|
return vec2(-1.0); // fuera de pantalla
|
||||||
|
}
|
||||||
|
coord += vec2(0.5);
|
||||||
|
coord /= screen_scale;
|
||||||
|
return coord;
|
||||||
|
}
|
||||||
|
|
||||||
|
float calcScanLineWeight(float dist) {
|
||||||
|
return max(1.0 - dist * dist * u.scanline_weight, u.scanline_gap_brightness);
|
||||||
|
}
|
||||||
|
|
||||||
|
float calcScanLine(float dy, float filter_width) {
|
||||||
|
float weight = calcScanLineWeight(dy);
|
||||||
|
if (u.enable_multisample != 0) {
|
||||||
|
weight += calcScanLineWeight(dy - filter_width);
|
||||||
|
weight += calcScanLineWeight(dy + filter_width);
|
||||||
|
weight *= 0.3333333;
|
||||||
|
}
|
||||||
|
return weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 tex_size = vec2(u.texture_width, u.texture_height);
|
||||||
|
|
||||||
|
// filterWidth: equivalente al original (768.0 / TextureSize.y) / 3.0
|
||||||
|
float filter_width = (768.0 / u.texture_height) / 3.0;
|
||||||
|
|
||||||
|
vec2 texcoord = v_uv;
|
||||||
|
|
||||||
|
// Curvatura barrel opcional
|
||||||
|
if (u.enable_curvature != 0) {
|
||||||
|
texcoord = distort(texcoord, vec2(1.0, 1.0));
|
||||||
|
if (texcoord.x < 0.0) {
|
||||||
|
out_color = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 texcoord_in_pixels = texcoord * tex_size;
|
||||||
|
vec2 tc;
|
||||||
|
float scan_line_weight;
|
||||||
|
|
||||||
|
if (u.enable_sharper != 0) {
|
||||||
|
// Modo SHARPER: filtrado bicúbico-like con subpixel sharpen
|
||||||
|
vec2 temp_coord = floor(texcoord_in_pixels) + 0.5;
|
||||||
|
tc = temp_coord / tex_size;
|
||||||
|
vec2 deltas = texcoord_in_pixels - temp_coord;
|
||||||
|
scan_line_weight = calcScanLine(deltas.y, filter_width);
|
||||||
|
vec2 signs = sign(deltas);
|
||||||
|
deltas.x *= 2.0;
|
||||||
|
deltas = deltas * deltas;
|
||||||
|
deltas.y = deltas.y * deltas.y;
|
||||||
|
deltas.x *= 0.5;
|
||||||
|
deltas.y *= 8.0;
|
||||||
|
deltas /= tex_size;
|
||||||
|
deltas *= signs;
|
||||||
|
tc = tc + deltas;
|
||||||
|
} else {
|
||||||
|
// Modo estándar
|
||||||
|
float temp_y = floor(texcoord_in_pixels.y) + 0.5;
|
||||||
|
float y_coord = temp_y / tex_size.y;
|
||||||
|
float dy = texcoord_in_pixels.y - temp_y;
|
||||||
|
scan_line_weight = calcScanLine(dy, filter_width);
|
||||||
|
float sign_y = sign(dy);
|
||||||
|
dy = dy * dy;
|
||||||
|
dy = dy * dy;
|
||||||
|
dy *= 8.0;
|
||||||
|
dy /= tex_size.y;
|
||||||
|
dy *= sign_y;
|
||||||
|
tc = vec2(texcoord.x, y_coord + dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 colour = texture(Texture, tc).rgb;
|
||||||
|
|
||||||
|
if (u.enable_scanlines != 0) {
|
||||||
|
if (u.enable_gamma != 0) {
|
||||||
|
colour = pow(colour, vec3(u.input_gamma));
|
||||||
|
}
|
||||||
|
colour *= scan_line_weight * u.bloom_factor;
|
||||||
|
if (u.enable_gamma != 0) {
|
||||||
|
colour = pow(colour, vec3(1.0 / u.output_gamma));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Máscara de fósforo
|
||||||
|
if (u.mask_type == 1) {
|
||||||
|
float which_mask = fract(gl_FragCoord.x * 0.5);
|
||||||
|
vec3 mask = (which_mask < 0.5)
|
||||||
|
? vec3(u.mask_brightness, 1.0, u.mask_brightness)
|
||||||
|
: vec3(1.0, u.mask_brightness, 1.0);
|
||||||
|
colour *= mask;
|
||||||
|
} else if (u.mask_type == 2) {
|
||||||
|
float which_mask = fract(gl_FragCoord.x * 0.3333333);
|
||||||
|
vec3 mask = vec3(u.mask_brightness);
|
||||||
|
if (which_mask < 0.3333333)
|
||||||
|
mask.x = 1.0;
|
||||||
|
else if (which_mask < 0.6666666)
|
||||||
|
mask.y = 1.0;
|
||||||
|
else
|
||||||
|
mask.z = 1.0;
|
||||||
|
colour *= mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_color = vec4(colour, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// Vulkan GLSL fragment shader — PostFX effects
|
||||||
|
// Used for SDL3 GPU API (SPIR-V path, Win/Linux).
|
||||||
|
// Compile: glslc postfx.frag -o postfx.frag.spv
|
||||||
|
// xxd -i postfx.frag.spv > ../../source/core/rendering/sdl3gpu/postfx_frag_spv.h
|
||||||
|
//
|
||||||
|
// PostFXUniforms must match exactly the C++ struct in sdl3gpu_shader.hpp
|
||||||
|
// (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;
|
||||||
|
|
||||||
|
layout(set = 2, binding = 0) uniform sampler2D scene;
|
||||||
|
|
||||||
|
layout(set = 3, binding = 0) uniform PostFXUniforms {
|
||||||
|
float vignette_strength;
|
||||||
|
float chroma_min; // intensitat mínima de l'aberració cromàtica
|
||||||
|
float scanline_strength;
|
||||||
|
float screen_height;
|
||||||
|
float mask_strength;
|
||||||
|
float gamma_strength;
|
||||||
|
float curvature;
|
||||||
|
float bleeding;
|
||||||
|
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||||||
|
float time; // seconds since SDL init
|
||||||
|
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(
|
||||||
|
0.299*rgb.r + 0.587*rgb.g + 0.114*rgb.b,
|
||||||
|
-0.169*rgb.r - 0.331*rgb.g + 0.500*rgb.b + 0.5,
|
||||||
|
0.500*rgb.r - 0.419*rgb.g - 0.081*rgb.b + 0.5
|
||||||
|
);
|
||||||
|
}
|
||||||
|
vec3 ycc_to_rgb(vec3 ycc) {
|
||||||
|
float y = ycc.x;
|
||||||
|
float cb = ycc.y - 0.5;
|
||||||
|
float cr = ycc.z - 0.5;
|
||||||
|
return clamp(vec3(
|
||||||
|
y + 1.402*cr,
|
||||||
|
y - 0.344*cb - 0.714*cr,
|
||||||
|
y + 1.772*cb
|
||||||
|
), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 uv = v_uv;
|
||||||
|
|
||||||
|
// Curvatura barrel CRT
|
||||||
|
if (u.curvature > 0.0) {
|
||||||
|
vec2 c = uv - 0.5;
|
||||||
|
float rsq = dot(c, c);
|
||||||
|
vec2 dist = vec2(0.05, 0.1) * u.curvature;
|
||||||
|
vec2 barrelScale = vec2(1.0) - 0.23 * dist;
|
||||||
|
c += c * (dist * rsq);
|
||||||
|
c *= barrelScale;
|
||||||
|
if (abs(c.x) >= 0.5 || abs(c.y) >= 0.5) {
|
||||||
|
out_color = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uv = c + 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Muestra base
|
||||||
|
vec3 base = texture(scene, uv).rgb;
|
||||||
|
|
||||||
|
// Sangrado NTSC — difuminado horizontal de crominancia.
|
||||||
|
// 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 = 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);
|
||||||
|
vec3 ycc_r1 = rgb_to_ycc(texture(scene, uv + vec2(1.0*step, 0.0)).rgb);
|
||||||
|
vec3 ycc_r2 = rgb_to_ycc(texture(scene, uv + vec2(2.0*step, 0.0)).rgb);
|
||||||
|
ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0 + ycc.yz*2.0 + ycc_r1.yz*2.0 + ycc_r2.yz) / 8.0;
|
||||||
|
colour = mix(base, ycc_to_rgb(ycc), u.bleeding);
|
||||||
|
} else {
|
||||||
|
colour = base;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
vec3 lin = pow(colour, vec3(2.4));
|
||||||
|
colour = mix(colour, lin, u.gamma_strength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (u.gamma_strength > 0.0) {
|
||||||
|
vec3 enc = pow(colour, vec3(1.0 / 2.2));
|
||||||
|
colour = mix(colour, enc, u.gamma_strength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viñeta
|
||||||
|
vec2 d = uv - 0.5;
|
||||||
|
float vignette = 1.0 - dot(d, d) * u.vignette_strength;
|
||||||
|
colour *= clamp(vignette, 0.0, 1.0);
|
||||||
|
|
||||||
|
// Máscara de fósforo RGB — después de scanlines (orden original):
|
||||||
|
// filas brillantes saturadas → máscara invisible, filas oscuras → RGB visible.
|
||||||
|
if (u.mask_strength > 0.0) {
|
||||||
|
float whichMask = fract(gl_FragCoord.x * 0.3333333);
|
||||||
|
vec3 mask = vec3(0.80);
|
||||||
|
if (whichMask < 0.3333333)
|
||||||
|
mask.x = 1.0;
|
||||||
|
else if (whichMask < 0.6666666)
|
||||||
|
mask.y = 1.0;
|
||||||
|
else
|
||||||
|
mask.z = 1.0;
|
||||||
|
colour = mix(colour, colour * mask, u.mask_strength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parpadeo de fósforo CRT (~50 Hz)
|
||||||
|
if (u.flicker > 0.0) {
|
||||||
|
float flicker_wave = sin(u.time * 100.0) * 0.5 + 0.5;
|
||||||
|
colour *= 1.0 - u.flicker * 0.04 * flicker_wave;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_color = vec4(colour, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// Vulkan GLSL vertex shader — postfx full-screen triangle
|
||||||
|
// Used for SDL3 GPU API (SPIR-V path, Win/Linux).
|
||||||
|
// Compile: glslc postfx.vert -o postfx.vert.spv
|
||||||
|
// xxd -i postfx.vert.spv > ../../source/core/rendering/sdl3gpu/postfx_vert_spv.h
|
||||||
|
|
||||||
|
layout(location = 0) out vec2 v_uv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Full-screen triangle (no vertex buffer needed)
|
||||||
|
const vec2 positions[3] = vec2[3](
|
||||||
|
vec2(-1.0, -1.0),
|
||||||
|
vec2( 3.0, -1.0),
|
||||||
|
vec2(-1.0, 3.0)
|
||||||
|
);
|
||||||
|
const vec2 uvs[3] = vec2[3](
|
||||||
|
vec2(0.0, 1.0),
|
||||||
|
vec2(2.0, 1.0),
|
||||||
|
vec2(0.0,-1.0)
|
||||||
|
);
|
||||||
|
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
|
||||||
|
v_uv = uvs[gl_VertexIndex];
|
||||||
|
}
|
||||||
@@ -0,0 +1,542 @@
|
|||||||
|
# Arquitectura de **Coffee Crisis**
|
||||||
|
|
||||||
|
> Guía de orientación para un desarrollador nuevo en el proyecto.
|
||||||
|
>
|
||||||
|
> Cada afirmación está anclada a código real: se cita el fichero (y, cuando
|
||||||
|
> ayuda, la función o el número de línea) que la respalda. Donde no he
|
||||||
|
> encontrado algo, o donde el código contradice a la documentación previa, lo
|
||||||
|
> digo explícitamente en lugar de inventarlo.
|
||||||
|
>
|
||||||
|
> **Coffee Crisis** es un arcade en C++20 + SDL3: el jugador defiende la UPV de
|
||||||
|
> globos de café rebotantes a lo largo de 10 fases. Soporta 1–2 jugadores,
|
||||||
|
> teclado y mando, y varios idiomas. Es el **predecesor** de *Coffee Crisis
|
||||||
|
> Arcade Edition*; al final del documento ([§15](#15-diferencias-frente-a-la-arcade-edition))
|
||||||
|
> hay un resumen de las diferencias entre ambos. Los comentarios del código
|
||||||
|
> están en español/valenciano; este documento está en castellano.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Índice
|
||||||
|
|
||||||
|
1. [Visión general](#1-visión-general)
|
||||||
|
2. [Punto de entrada y bucle principal](#2-punto-de-entrada-y-bucle-principal)
|
||||||
|
3. [Secciones y flujo de la aplicación](#3-secciones-y-flujo-de-la-aplicación)
|
||||||
|
4. [Renderizado: de la lógica al píxel](#4-renderizado-de-la-lógica-al-píxel)
|
||||||
|
5. [Entrada](#5-entrada)
|
||||||
|
6. [Lógica del juego: la clase `Game`](#6-lógica-del-juego-la-clase-game)
|
||||||
|
7. [Entidades](#7-entidades)
|
||||||
|
8. [Modo demo y attract mode](#8-modo-demo-y-attract-mode)
|
||||||
|
9. [Recursos](#9-recursos)
|
||||||
|
10. [Audio](#10-audio)
|
||||||
|
11. [Configuración y constantes](#11-configuración-y-constantes)
|
||||||
|
12. [Localización](#12-localización)
|
||||||
|
13. [Convenciones y patrones recurrentes](#13-convenciones-y-patrones-recurrentes)
|
||||||
|
14. [Guía de navegación: "si quieres tocar X, mira Y"](#14-guía-de-navegación-si-quieres-tocar-x-mira-y)
|
||||||
|
15. [Diferencias frente a la Arcade Edition](#15-diferencias-frente-a-la-arcade-edition)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Visión general
|
||||||
|
|
||||||
|
El árbol `source/` separa **motor** y **juego**:
|
||||||
|
|
||||||
|
- **`source/core/`** — motor genérico: `system` (`director`, `delta_time`,
|
||||||
|
`demo`), `rendering` (+ `sdl3gpu`, sprites), `input`, `resources`, `audio`,
|
||||||
|
`locale`.
|
||||||
|
- **`source/game/`** — el juego concreto: `game.*` (el hub de gameplay),
|
||||||
|
`entities/` (player, balloon, bullet, item), `scenes/` (logo, intro, title,
|
||||||
|
instructions), `ui/` (menu), `options.*` y `defaults.hpp`.
|
||||||
|
- **`source/utils/`** — `utils.*` (helpers, `struct Section`, `Color`,
|
||||||
|
dificultad…) y `defines.hpp` (macros de build).
|
||||||
|
- **`source/external/`** — vendorizado: `stb_image`, `stb_vorbis` (y headers
|
||||||
|
YAML/JSON).
|
||||||
|
|
||||||
|
~51 ficheros C++ y ~16.000 líneas. **Nota sobre cabeceras**: los módulos
|
||||||
|
antiguos usan extensión **`.h`** (p.ej. `director.h`, `game.h`, `screen.h`); los
|
||||||
|
módulos nuevos usan **`.hpp`** (p.ej. `demo.hpp`, `options.hpp`,
|
||||||
|
`delta_time.hpp`). Es un proyecto en migración, y eso se nota en varias capas.
|
||||||
|
|
||||||
|
**Ideas-fuerza que conviene interiorizar:**
|
||||||
|
|
||||||
|
1. El flujo se controla con un **`struct Section { name, subsection }`** que el
|
||||||
|
`Director` lee cada frame (§3).
|
||||||
|
2. El render dibuja con **texturas GPU** (`SDL_Renderer`) sobre un **canvas
|
||||||
|
virtual de 256×192**, con post-procesado opcional vía un backend SDL3 GPU
|
||||||
|
(§4).
|
||||||
|
3. El gameplay es **monolítico**: casi todo vive en la clase `Game`, con
|
||||||
|
vectores de **punteros crudos** y `new`/`delete` manual (§6, §7).
|
||||||
|
4. **Sí hay modo demo** (*attract mode*): **reproducción de input grabado**, no
|
||||||
|
IA, orquestada desde la pantalla de título (§8).
|
||||||
|
5. El proyecto está **migrando de frame-based a time-based** y de `config.txt`
|
||||||
|
a YAML; conviven ambos mundos (§2, §11).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
SDL[SDL3 callbacks · main.cpp] --> DIR[Director]
|
||||||
|
DIR -->|struct Section| ST{handleSectionTransition}
|
||||||
|
ST --> SEC["Logo / Intro / Title / Game"]
|
||||||
|
SEC --> TITLE[Title] -.attract.-> NESTED["Game anidado en demo + Instructions"]
|
||||||
|
SEC --> GAME["Game (monolítico)"]
|
||||||
|
GAME --> ENT["Player* / Balloon* / Bullet* / Item* (punteros crudos)"]
|
||||||
|
GAME --> DEMOSYS["Demo (playback grabado)"] -.-> ENT
|
||||||
|
GAME -->|SDL_RenderTexture| CANVAS["game_canvas_ 256×192"]
|
||||||
|
CANVAS --> SCREEN[Screen] --> SB["ShaderBackend PostFX/CrtPi"] --> WIN[Ventana]
|
||||||
|
RES["Asset / Resource"] -.-> GAME & SEC
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Punto de entrada y bucle principal
|
||||||
|
|
||||||
|
### 2.1. SDL conduce el bucle (callbacks)
|
||||||
|
|
||||||
|
`source/main.cpp` define `SDL_MAIN_USE_CALLBACKS`: no hay `while` propio. SDL
|
||||||
|
llama a cuatro funciones, todas delegando en el `Director`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
SDL_AppInit → new Director(argc, argv);
|
||||||
|
SDL_AppIterate→ Director::iterate(); // un frame
|
||||||
|
SDL_AppEvent → Director::handleEvent(event);
|
||||||
|
SDL_AppQuit → delete Director;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2. El `Director`
|
||||||
|
|
||||||
|
`source/core/system/director.h` / `.cpp`. Inicializa SDL, ventana y renderer,
|
||||||
|
crea la carpeta de sistema, monta input/audio/recursos, y mantiene **un
|
||||||
|
`unique_ptr` por sección** (`logo_`, `intro_`, `title_`, `game_`) de los que
|
||||||
|
**solo uno está vivo** (`director.h:55`). Guarda un puntero a la
|
||||||
|
`struct Section* section_` que comparte con la sección activa.
|
||||||
|
|
||||||
|
`Director::iterate()` cada frame: comprueba salida (doble ESC vía
|
||||||
|
`GlobalInputs::wantsQuit()`), actualiza la visibilidad del cursor, llama a
|
||||||
|
`handleSectionTransition()` y despacha `iterate()` a la sección activa
|
||||||
|
(`director.cpp`, `switch (active_section_)`).
|
||||||
|
|
||||||
|
### 2.3. Gestión del tiempo (en migración)
|
||||||
|
|
||||||
|
El reloj central es `source/core/system/delta_time.*`: `DeltaTime::tick()`
|
||||||
|
devuelve el delta en segundos consumido al inicio de cada frame de la sección
|
||||||
|
(`game.cpp`, `Game::iterate`). El proyecto **está migrando de frame-based a
|
||||||
|
time-based**: en `game.h` se ven contadores duplicados, el viejo frame-based
|
||||||
|
(`Uint16 death_counter_`) y el nuevo time-based (`float death_counter_s_`),
|
||||||
|
documentados como tales (`game.h:347`). El playback de la demo también es
|
||||||
|
time-based: `index = elapsed_s * 60` (`demo.hpp:11`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Secciones y flujo de la aplicación
|
||||||
|
|
||||||
|
### 3.1. `struct Section`
|
||||||
|
|
||||||
|
`source/utils/utils.h:58` define un POD minimalista:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct Section {
|
||||||
|
Uint8 name; // SECTION_PROG_* (LOGO/INTRO/TITLE/GAME/QUIT)
|
||||||
|
Uint8 subsection; // SUBSECTION_* (p.ej. GAME_PLAY_1P, GAME_PAUSE, TITLE_INSTRUCTIONS…)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Los valores son **constantes `constexpr int` en `source/game/defaults.hpp`**
|
||||||
|
(`SECTION_PROG_LOGO = 0`, …, `SECTION_PROG_QUIT = 4`; `SUBSECTION_GAME_PLAY_1P`,
|
||||||
|
`SUBSECTION_GAME_PAUSE`, `SUBSECTION_GAME_GAMEOVER`, `SUBSECTION_TITLE_INSTRUCTIONS`,
|
||||||
|
etc.; `defaults.hpp:90`). Cualquier parte del código cambia el flujo asignando
|
||||||
|
`section_->name = ...` / `section_->subsection = ...`.
|
||||||
|
|
||||||
|
### 3.2. Transición de secciones
|
||||||
|
|
||||||
|
`Director::handleSectionTransition()` (`director.cpp`):
|
||||||
|
|
||||||
|
- Traduce `section_->name` a un `enum class ActiveSection` (`director.h:24`).
|
||||||
|
- Si coincide con la activa, no hace nada.
|
||||||
|
- Si cambió: libera las cuatro secciones (`reset()`) y construye la nueva. Para
|
||||||
|
`GAME` decide el nº de jugadores según `section_->subsection`
|
||||||
|
(`SUBSECTION_GAME_PLAY_1P` → 1, si no → 2) y crea
|
||||||
|
`Game(NUM_PLAYERS, 0, renderer_, /*demo=*/false, section_)`.
|
||||||
|
|
||||||
|
Cada sección recibe el `renderer_` y el `Section*`, y expone `iterate()` (y, en
|
||||||
|
`Game`, `handleEvent()`).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
LOGO --> INTRO --> TITLE
|
||||||
|
TITLE -->|jugar| GAME --> TITLE
|
||||||
|
TITLE -->|attract / manual| INSTR["Instructions (dentro de Title)"]
|
||||||
|
TITLE -->|attract| DEMOG["Game anidado en demo (dentro de Title)"]
|
||||||
|
TITLE --> QUIT
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Matiz importante**: `Instructions` y la **demo** no son secciones del
|
||||||
|
> `Director`. Viven **dentro de `Title`**, que ejecuta un *attract loop* (ver
|
||||||
|
> §8). El `Director` solo conoce Logo/Intro/Title/Game/Quit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Renderizado: de la lógica al píxel
|
||||||
|
|
||||||
|
Los sprites son **texturas GPU** dibujadas por `SDL_Renderer` sobre un canvas
|
||||||
|
virtual; el post-procesado va por un backend SDL3 GPU.
|
||||||
|
|
||||||
|
### 4.1. El canvas virtual 256×192
|
||||||
|
|
||||||
|
`Screen` (`core/rendering/screen.h`) crea `game_canvas_`, una `SDL_Texture` de
|
||||||
|
**256×192** (`GAMECANVAS_WIDTH/HEIGHT` en `defaults.hpp:64`) con
|
||||||
|
`SDL_TEXTUREACCESS_TARGET` (`screen.cpp:106`). Toda la geometría del juego se
|
||||||
|
deriva de esa resolución y de un `BLOCK` base (áreas de juego en
|
||||||
|
`defaults.hpp`).
|
||||||
|
|
||||||
|
`Screen::start()` (`screen.cpp:166`) fija el render-target a `game_canvas_`;
|
||||||
|
a partir de ahí, la sección activa dibuja sus sprites sobre él.
|
||||||
|
|
||||||
|
### 4.2. Texturas y jerarquía de sprites
|
||||||
|
|
||||||
|
- `core/rendering/texture.h` — `Texture` envuelve un `SDL_Texture*` cargado de
|
||||||
|
PNG; método `render(...)` con clip/zoom/flip.
|
||||||
|
- `core/rendering/sprite.h` y derivados:
|
||||||
|
- `Sprite` — dibuja desde un *spritesheet*.
|
||||||
|
- `AnimatedSprite` — animación por fotogramas, definida en ficheros **`.ani`**.
|
||||||
|
- `MovingSprite` — añade posición/velocidad (p.ej. las nubes del fondo).
|
||||||
|
- `SmartSprite` — sprite autónomo (popups de puntuación, el café que salta al
|
||||||
|
recibir un golpe).
|
||||||
|
- Texto: `core/rendering/text.h` + `writer.h` (fuentes bitmap).
|
||||||
|
- Transiciones: `core/rendering/fade.h`. Notificaciones:
|
||||||
|
`core/rendering/notifications.*`.
|
||||||
|
|
||||||
|
### 4.3. Post-procesado y presentación
|
||||||
|
|
||||||
|
El path de presentación (`screen.cpp:185`) decide cómo llega el canvas a la
|
||||||
|
ventana:
|
||||||
|
|
||||||
|
- **Con backend GPU acelerado**: lee los píxeles de `game_canvas_` con
|
||||||
|
`SDL_RenderReadPixels` a un `pixel_buffer_` (ARGB8888;
|
||||||
|
`screen.h:162`), los sube al backend (`shader_backend_->uploadPixels(...)`) y
|
||||||
|
este renderiza con el shader activo a la ventana.
|
||||||
|
- **Sin backend / desactivado** (fallback): `SDL_RenderTexture` del
|
||||||
|
`game_canvas_` a la ventana y `SDL_RenderPresent` (`screen.cpp:233`).
|
||||||
|
|
||||||
|
El backend vive en `core/rendering/sdl3gpu/` (interfaz abstracta en
|
||||||
|
`shader_backend.hpp`). Dos shaders: **PostFX** (viñeta, scanlines, chroma,
|
||||||
|
gamma, máscara, curvatura, *bleeding*, flicker) y **CrtPi** (scanlines continuas
|
||||||
|
con bloom). Los GLSL de `data/shaders/` se compilan a SPIR-V (`spv/*_spv.h`) vía
|
||||||
|
`glslc`; en macOS se usan shaders **Metal (MSL)** inline (`sdl3gpu/msl/`). El
|
||||||
|
build `NO_SHADERS` (Emscripten) fuerza la ruta clásica.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
OBJ["fondo, globos, jugador, balas, items…"] -->|SDL_RenderTexture| CANVAS["game_canvas_ 256×192 (render target)"]
|
||||||
|
CANVAS -->|RenderReadPixels → uploadPixels| SHADER["ShaderBackend (PostFX / CrtPi)"]
|
||||||
|
SHADER --> WIN[Ventana]
|
||||||
|
CANVAS -.fallback sin GPU.-> WIN
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4. Modos de escalado y efectos
|
||||||
|
|
||||||
|
La presentación a la ventana respeta `Options::video.presentation_mode`
|
||||||
|
(`INTEGER_SCALE`, `LETTERBOX`, `STRETCHED`, `OVERSCAN`; `options.hpp:24`). El
|
||||||
|
`Game` añade efectos como *flash*, *shake* y un *death shake* intenso
|
||||||
|
(`game.h:100`, `DeathShake`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Entrada
|
||||||
|
|
||||||
|
### 5.1. `Input`
|
||||||
|
|
||||||
|
`source/core/input/input.h` — abstracción de teclado y mando bajo un enum de
|
||||||
|
acciones (`Input::Action`). El jugador se mueve con flechas y dispara
|
||||||
|
izquierda/centro/derecha; el `Input::Device` selecciona teclado o mando por
|
||||||
|
jugador (`Game::player_one_control_`, `game.h:379`).
|
||||||
|
|
||||||
|
### 5.2. Hotkeys globales y salida
|
||||||
|
|
||||||
|
`source/core/input/global_inputs.*` gestiona las teclas de sistema (ventana,
|
||||||
|
vídeo, post-FX, idioma, FPS overlay…) y la **salida en dos pasos**: la primera
|
||||||
|
pulsación de ESC arma una confirmación y la segunda activa `wantsQuit()`, que el
|
||||||
|
`Director` traduce a `SECTION_PROG_QUIT` (`director.cpp`). El cursor del ratón
|
||||||
|
se autooculta (`core/input/mouse.*`).
|
||||||
|
|
||||||
|
Las hotkeys de shaders documentadas: **F4** activa/desactiva post-procesado,
|
||||||
|
**F5** alterna PostFX↔CrtPi, **F6** siguiente preset (ver `CLAUDE.md`).
|
||||||
|
|
||||||
|
### 5.3. Cómo llega la entrada al jugador
|
||||||
|
|
||||||
|
Dentro de `Game`, `checkGameInput()` → `processLiveInput()` →
|
||||||
|
`processPlayerLiveInput(player, i)` consulta `Input` y llama a
|
||||||
|
`player->setInput(Input::Action)` y a `createBullet(...)` al disparar
|
||||||
|
(`game.h:228`). En modo demo, esa misma vía la alimenta `processDemoInput()`
|
||||||
|
con datos grabados (§8).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Lógica del juego: la clase `Game`
|
||||||
|
|
||||||
|
`source/game/game.{h,cpp}` es **el hub de gameplay y, con diferencia, la clase
|
||||||
|
más grande** del proyecto: ~400 líneas solo de declaración. A diferencia de la
|
||||||
|
Arcade Edition, **no delega en managers**: las formaciones, fases, globos,
|
||||||
|
balas e ítems se gestionan directamente aquí.
|
||||||
|
|
||||||
|
### 6.1. El frame y sus sub-bucles
|
||||||
|
|
||||||
|
`Game::iterate()` (`game.cpp`) consume el delta con `DeltaTime::tick()` y
|
||||||
|
despacha según `section_->subsection`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
switch (section_->subsection) {
|
||||||
|
case SUBSECTION_GAME_PAUSE: iteratePaused(dt); break;
|
||||||
|
case SUBSECTION_GAME_GAMEOVER: iterateGameOver(dt); break;
|
||||||
|
case SUBSECTION_GAME_PLAY_1P:
|
||||||
|
case SUBSECTION_GAME_PLAY_2P: iteratePlaying(dt); break;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Es decir, **pausa y game-over son sub-estados** del propio `Game` (no secciones
|
||||||
|
del Director), cada uno con su `update`/`render`. En modo demo, entrar en pausa
|
||||||
|
o game-over rebota directamente a `Title` (`game.cpp`, `Game::iterate`).
|
||||||
|
|
||||||
|
### 6.2. Lo que gestiona `Game`
|
||||||
|
|
||||||
|
Todo dentro de la misma clase (`game.h`):
|
||||||
|
|
||||||
|
- **Fases (`Stage stage_[10]`)**: cada fase tiene un *pool* de formaciones
|
||||||
|
enemigas (`EnemyPool`/`EnemyFormation`/`EnemyInit`, structs internos), poder
|
||||||
|
para completarla y umbrales de amenaza. `initEnemyFormations*` precalcula las
|
||||||
|
formaciones (lineales, simétricas, hexágonos…).
|
||||||
|
- **Nivel de amenaza** (`menace_current_`/`menace_threshold_`): si la amenaza
|
||||||
|
cae bajo el umbral, se despliega otra formación (`updateMenace`,
|
||||||
|
`evaluateAndSetMenace`).
|
||||||
|
- **Ítems y power-ups**: disco/gaviota/pacmar (puntos), café (toque extra),
|
||||||
|
máquina de café (power-up), reloj (**detener el tiempo**,
|
||||||
|
`enableTimeStopItem`), *power ball*. Probabilidades en `Helper`
|
||||||
|
(`game.h:128`).
|
||||||
|
- **Colisiones**: jugador↔globo, jugador↔ítem, bala↔globo (`checkPlayer…`,
|
||||||
|
`checkBulletBalloonCollision`).
|
||||||
|
- **Muerte del jugador**: secuencia con *death shake* y fases
|
||||||
|
(`DeathSequence`/`DeathPhase`, `game.h:113`).
|
||||||
|
- **Marcador, hi-score, fades, fondo** (nubes con parallax via `MovingSprite`),
|
||||||
|
menús de pausa y game-over (`Menu`), audio (`Ja::Sound*`/`Ja::Music*`).
|
||||||
|
|
||||||
|
### 6.3. Gestión de memoria
|
||||||
|
|
||||||
|
Las entidades viven como **vectores de punteros crudos**
|
||||||
|
(`std::vector<Player*>`, `<Balloon*>`, `<Bullet*>`, `<Item*>`,
|
||||||
|
`<SmartSprite*>`; `game.h:264`), creados con `new` y liberados con métodos
|
||||||
|
`freeBalloons()`, `freeBullets()`, `freeItems()`, `deleteAllVectorObjects()`.
|
||||||
|
Es un estilo más antiguo que el de la Arcade Edition (smart pointers).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Entidades
|
||||||
|
|
||||||
|
`source/game/entities/`:
|
||||||
|
|
||||||
|
- **`Player`** (`player.h`) — movimiento, disparo (tres direcciones),
|
||||||
|
animaciones, power-up, invulnerabilidad, vidas/score. Puede usar teclado o
|
||||||
|
mando.
|
||||||
|
- **`Balloon`** (`balloon.h`) — enemigo básico que rebota; al explotar puede
|
||||||
|
generar globos hijos. Tiene varios contadores de estado.
|
||||||
|
- **`Bullet`** (`bullet.h`) — proyectil con `Kind` (UP/LEFT/RIGHT) y estado de
|
||||||
|
power-up.
|
||||||
|
- **`Item`** (`item.h`) — power-ups y objetos de puntos que caen, con `Id` por
|
||||||
|
tipo.
|
||||||
|
|
||||||
|
No hay clase base de entidad común ni managers: el ciclo de vida lo lleva
|
||||||
|
`Game` directamente sobre los vectores (§6.3). Los efectos visuales tipo
|
||||||
|
"popup de puntuación" o "café arrojado" se modelan como `SmartSprite`
|
||||||
|
(`core/rendering/smartsprite.h`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Modo demo y attract mode
|
||||||
|
|
||||||
|
> **El modo demo SÍ existe, y NO es IA**: es **reproducción de input
|
||||||
|
> pregrabado**, igual concepto que en la Arcade Edition.
|
||||||
|
|
||||||
|
### 8.1. Formato
|
||||||
|
|
||||||
|
`source/core/system/demo.hpp`: cada fotograma es un `DemoKeys` con seis banderas
|
||||||
|
(`left`, `right`, `no_input`, `fire`, `fire_left`, `fire_right`). Una demo es un
|
||||||
|
`vector<DemoKeys>` de `TOTAL_DEMO_DATA = 2000` fotogramas "a 60 Hz de
|
||||||
|
referencia" (`demo.hpp:9`). Hay tres ficheros: `data/demo/demo{1,2,3}.bin`. El
|
||||||
|
playback es **time-based**: `index = elapsed_s * 60`.
|
||||||
|
|
||||||
|
### 8.2. Reproducción
|
||||||
|
|
||||||
|
Cuando un `Game` corre en modo demo, `processDemoInput()` (`game.cpp`) lee el
|
||||||
|
fotograma actual del set seleccionado y lo inyecta por la **misma vía que un
|
||||||
|
humano** sobre `players_[0]`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
const DemoKeys &keys = dd.at(demo_.index % dd.size());
|
||||||
|
if (keys.left == 1) players_[0]->setInput(Input::Action::LEFT);
|
||||||
|
if (keys.fire == 1 && players_[0]->canFire()) {
|
||||||
|
players_[0]->setInput(Input::Action::FIRE_CENTER);
|
||||||
|
createBullet(...); players_[0]->setFireCooldown(10);
|
||||||
|
}
|
||||||
|
// … (right, no_input, fire_left, fire_right)
|
||||||
|
```
|
||||||
|
|
||||||
|
No hay toma de decisiones: repite las pulsaciones grabadas. Al agotar el
|
||||||
|
playback (`index >= TOTAL_DEMO_DATA`) vuelve a `Title`.
|
||||||
|
|
||||||
|
### 8.3. Attract mode (dentro de `Title`)
|
||||||
|
|
||||||
|
El bucle de atracción vive en `source/game/scenes/title.cpp`: el Title arma un
|
||||||
|
*timeout* (`demo_remaining_s_`) y, al agotarse, lanza un **`Game` anidado en
|
||||||
|
modo demo** (`runDemoGame()`, `demo_game_`, `demo_game_active_`;
|
||||||
|
`title.cpp:323`). Title tiquea ese `demo_game_->iterate()` directamente y, al
|
||||||
|
terminar la demo, encadena las **instrucciones**
|
||||||
|
(`demo_then_instructions_` → `runInstructions(Instructions::Mode::AUTO)`,
|
||||||
|
`title.cpp:334`) antes de volver al título. Así el Title alterna
|
||||||
|
atracción → demo → instrucciones de forma autónoma.
|
||||||
|
|
||||||
|
> Es una diferencia notable con la Arcade Edition, donde la demo es una sección
|
||||||
|
> `GAME_DEMO` propia del Director. Aquí el `Director` ni se entera: todo el
|
||||||
|
> attract está encapsulado en `Title`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Recursos
|
||||||
|
|
||||||
|
- **`Asset`** (`core/resources/asset.h`) — índice de ficheros de recurso
|
||||||
|
(`add`/`get` por nombre).
|
||||||
|
- **`Resource`** (`core/resources/resource.h`) — carga y caché de los recursos
|
||||||
|
(texturas, sonidos, música, fuentes, animaciones).
|
||||||
|
- **Pack**: `resource_pack.*` + `resource_loader.*` + `resource_helper.*`
|
||||||
|
sirven desde **`resources.pack`**, con *fallback* al filesystem en desarrollo.
|
||||||
|
- **Formatos**: PNG (spritesheets) + ficheros **`.ani`** (definición de
|
||||||
|
animaciones); OGG (audio, vía `stb_vorbis`); fuentes bitmap en `data/font/`.
|
||||||
|
Los shaders GLSL de `data/shaders/` **no** van al pack (se embeben en el
|
||||||
|
binario como cabeceras SPIR-V).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Audio
|
||||||
|
|
||||||
|
`source/core/audio/` — `Audio` (`audio.hpp`) + `audio_adapter` sobre
|
||||||
|
**`jail_audio`** (`jail_audio.hpp`), wrapper de audio SDL3 *first-party* (no
|
||||||
|
librería externa) que usa `stb_vorbis` para OGG y mezcla por canales (API
|
||||||
|
`JA_*`). `Game` mantiene punteros `Ja::Sound*` para cada efecto (explosión,
|
||||||
|
disparo, colisión, reloj, etc.) y un `Ja::Music* game_music_`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Configuración y constantes
|
||||||
|
|
||||||
|
- **`Options`** (`source/game/options.hpp`) — opciones persistentes en el
|
||||||
|
namespace `Options::` (`window`, `video` con `gpu`/`shader`, `audio`,
|
||||||
|
`loading`, `settings`, `gameplay`, `inputs`), más presets `PostFXPreset` y
|
||||||
|
`CrtPiPreset`.
|
||||||
|
|
||||||
|
> ⚠️ **El `CLAUDE.md` está desactualizado en este punto**: dice que la config
|
||||||
|
> vive en `config.txt` con "migración a YAML pendiente". El código real
|
||||||
|
> (`options.hpp:16`) ya **persiste en `config.yaml` vía fkyaml**, con presets
|
||||||
|
> de shaders en `postfx.yaml`/`crtpi.yaml`. El código manda.
|
||||||
|
|
||||||
|
- **`defaults.hpp`** (`source/game/`) — constantes de gameplay y layout: tamaño
|
||||||
|
de canvas (256×192), `BLOCK`, áreas de juego, colores, y las constantes
|
||||||
|
`SECTION_PROG_*` / `SUBSECTION_*` del flujo (§3).
|
||||||
|
- **`utils/defines.hpp`** — macros de build.
|
||||||
|
|
||||||
|
### Builds condicionales
|
||||||
|
|
||||||
|
Aparecen sobre todo en `Director`/`Screen`: `__EMSCRIPTEN__` (web: no se puede
|
||||||
|
salir, reinicia al logo; `NO_SHADERS` forzado), `DEBUG`, y la selección de
|
||||||
|
plataforma para shaders (SPIR-V vs Metal). `make release` empaqueta `.tar.gz` /
|
||||||
|
`.dmg` / `.zip` según el SO.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Localización
|
||||||
|
|
||||||
|
`source/core/locale/lang.*` — `Lang` carga las cadenas desde `data/lang/`
|
||||||
|
(es_ES, ba_BA/euskera, en_UK). El idioma se elige en `Options::settings.language`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Convenciones y patrones recurrentes
|
||||||
|
|
||||||
|
- **Cabeceras mixtas `.h` / `.hpp`**: `.h` en lo antiguo, `.hpp` en lo nuevo —
|
||||||
|
pista fiable de qué módulos se han reescrito.
|
||||||
|
- **Punteros crudos + `new`/`delete`** en el gameplay (`Game`), frente a smart
|
||||||
|
pointers en el resto (secciones, `Screen`). Migración a medias.
|
||||||
|
- **Migración frame-based → time-based**: contadores duplicados
|
||||||
|
(`x_counter_` + `x_counter_s_`) conviviendo; el reloj es `DeltaTime::tick()`.
|
||||||
|
- **Flujo por `struct Section` + constantes `SECTION_PROG_*`** (no enums
|
||||||
|
tipados ni objetos de transición).
|
||||||
|
- **Sub-estados dentro de la sección** (pausa/game-over como `subsection` de
|
||||||
|
`Game`), no como secciones del `Director`.
|
||||||
|
- **Attract mode encapsulado en `Title`** (demo + instrucciones).
|
||||||
|
- **`Game` monolítico**: la lógica no está repartida en managers; todo cuelga
|
||||||
|
de la clase `Game` y de structs internos (`Stage`, `EnemyFormation`, …).
|
||||||
|
- **Comentarios** en español/valenciano; muchos `#include` con comentario
|
||||||
|
"// for X" (estilo IWYU).
|
||||||
|
- **El `CLAUDE.md` puede ir por detrás del código** (caso config.txt→YAML): ante
|
||||||
|
duda, manda el código.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Guía de navegación: "si quieres tocar X, mira Y"
|
||||||
|
|
||||||
|
| Quiero… | Empieza por… |
|
||||||
|
|---|---|
|
||||||
|
| Entender el arranque | `source/core/system/director.cpp` |
|
||||||
|
| Cambiar el flujo de pantallas | `struct Section` (`utils/utils.h`) + constantes en `game/defaults.hpp` + `handleSectionTransition` |
|
||||||
|
| Añadir/editar una pantalla | `source/game/scenes/` (Logo/Intro/Title/Instructions) |
|
||||||
|
| Gestión del tiempo | `source/core/system/delta_time.*` |
|
||||||
|
| Cómo se dibuja todo | `Screen::start`/render (`core/rendering/screen.cpp`) |
|
||||||
|
| Canvas / resolución / áreas | `source/game/defaults.hpp` (256×192, BLOCK) |
|
||||||
|
| Sprites / animaciones `.ani` | `core/rendering/sprite.h` + `animatedsprite.h` + `texture.h` |
|
||||||
|
| Shaders / CRT / post-FX | `core/rendering/sdl3gpu/` + `data/shaders/` + `Options` |
|
||||||
|
| Modos de escalado / efectos | `Screen` + `Options::video.presentation_mode` |
|
||||||
|
| Controles / mandos | `core/input/input.h` |
|
||||||
|
| Hotkeys / salida en dos pasos | `core/input/global_inputs.cpp` |
|
||||||
|
| **Toda la lógica de partida** | `source/game/game.cpp` (`iteratePlaying/Paused/GameOver`) |
|
||||||
|
| Fases / formaciones / amenaza | `Game::initEnemyFormations*`, `Stage stage_[10]`, `updateMenace` |
|
||||||
|
| Globos / balas / ítems | `game/entities/{balloon,bullet,item}.*` (gestionados en `Game`) |
|
||||||
|
| El jugador | `game/entities/player.*` |
|
||||||
|
| Ítems y power-ups | `Game::dropItem/createItem`, `Helper` (`game.h`) |
|
||||||
|
| **Modo demo / attract** | `core/system/demo.*`, `Game::processDemoInput`, `scenes/title.cpp` (`runDemoGame`) |
|
||||||
|
| Cargar un recurso | `core/resources/asset.h` + `resource.h` |
|
||||||
|
| Audio | `core/audio/audio.hpp` + `jail_audio.hpp` |
|
||||||
|
| Opciones del usuario | `game/options.hpp` (+ `config.yaml`) |
|
||||||
|
| Valores por defecto / constantes | `game/defaults.hpp`, `utils/defines.hpp` |
|
||||||
|
| Idiomas | `core/locale/lang.*` + `data/lang/` |
|
||||||
|
| Empaquetar datos | `tools/` + `make pack` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Diferencias frente a la Arcade Edition
|
||||||
|
|
||||||
|
`coffee_crisis` es el **predecesor** de `coffee_crisis_arcade_edition`. Ambos
|
||||||
|
comparten ADN (SDL3, `jail_audio`, demo por input grabado, backend SDL3 GPU con
|
||||||
|
PostFX/CrtPi, capas core/game), pero el código diverge de forma sistemática.
|
||||||
|
Resumen de lo observado leyendo ambos repos:
|
||||||
|
|
||||||
|
| Dimensión | **Coffee Crisis (este)** | **Arcade Edition** |
|
||||||
|
|---|---|---|
|
||||||
|
| Tamaño | ~51 ficheros, ~16k LOC | ~150 ficheros, ~32k LOC |
|
||||||
|
| Cabeceras | mixto `.h` (antiguo) / `.hpp` | todo `.hpp` |
|
||||||
|
| Flujo | `struct Section{name,subsection}` + `enum ActiveSection`; **4 secciones** (Logo/Intro/Title/Game) | variable global `Section::name` con **muchas más** (Preload, HiScore, Credits, GameDemo, Instructions…) |
|
||||||
|
| Arranque | directo | **no bloqueante** con sección `PRELOAD` + `Resource::loadStep(50ms)` |
|
||||||
|
| Gameplay | **`Game` monolítico**; formaciones/fases como structs internos | **managers** (`BalloonManager`, `BulletManager`, `StageManager` con `IStageInfo`) |
|
||||||
|
| Memoria de entidades | **punteros crudos** + `new`/`delete` | `shared_ptr`/`unique_ptr` + listas |
|
||||||
|
| Pausa / game-over | **sub-estados** dentro de `Game` (`subsection`) | FSM de estados de `Game` + managers dedicados |
|
||||||
|
| Demo / attract | **encapsulado en `Title`** (Game anidado en demo + instrucciones) | sección `GAME_DEMO` propia del Director + attract Title↔Logo/Demo |
|
||||||
|
| Canvas | **256×192** fijo (`defaults.hpp`) | parametrizable (`param_320x*.txt`) |
|
||||||
|
| Render | un único `game_canvas_` → readback → shader / fallback | dos render-targets (`canvas_` zona de juego → `game_canvas_`) → shader / fallback |
|
||||||
|
| Sprites | `Sprite/AnimatedSprite/MovingSprite/SmartSprite` | añade `PathSprite`, `CardSprite` |
|
||||||
|
| Reinicio en caliente | (no observado a nivel de `relaunch()`) | `Director::relaunch()` vía `execv` |
|
||||||
|
| Plataformas | Linux/macOS/Windows/Emscripten | + Raspberry Pi, Anbernic |
|
||||||
|
| Estado del código | **en migración**: frame→time-based, `config.txt`→YAML | más consolidado (YAML, time-based) |
|
||||||
|
|
||||||
|
En una frase: la Arcade Edition es esta misma idea **refactorizada y ampliada**
|
||||||
|
— se troceó el `Game` monolítico en managers, se pasó a smart pointers, se
|
||||||
|
añadieron secciones y plataformas, y se consolidó la migración a time-based y
|
||||||
|
YAML que aquí todavía está a medias.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Documento generado a partir de la lectura directa del código en el commit
|
||||||
|
actual de la rama `main`. Si algo aquí no cuadra con el código, el código
|
||||||
|
manda: actualiza este documento.*
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,458 +0,0 @@
|
|||||||
#include "animatedsprite.h"
|
|
||||||
|
|
||||||
#include <fstream> // for basic_ostream, operator<<, basic_istream, basic...
|
|
||||||
#include <iostream> // for cout
|
|
||||||
#include <sstream> // for basic_stringstream
|
|
||||||
|
|
||||||
#include "texture.h" // for Texture
|
|
||||||
|
|
||||||
// Carga la animación desde un fichero
|
|
||||||
animatedSprite_t loadAnimationFromFile(Texture *texture, std::string filePath, bool verbose) {
|
|
||||||
// Inicializa variables
|
|
||||||
animatedSprite_t as;
|
|
||||||
as.texture = texture;
|
|
||||||
int framesPerRow = 0;
|
|
||||||
int frameWidth = 0;
|
|
||||||
int frameHeight = 0;
|
|
||||||
int maxTiles = 0;
|
|
||||||
|
|
||||||
const std::string filename = filePath.substr(filePath.find_last_of("\\/") + 1);
|
|
||||||
std::ifstream file(filePath);
|
|
||||||
std::string line;
|
|
||||||
|
|
||||||
// El fichero se puede abrir
|
|
||||||
if (file.good()) {
|
|
||||||
// Procesa el fichero linea a linea
|
|
||||||
if (verbose) {
|
|
||||||
std::cout << "Animation loaded: " << filename << std::endl;
|
|
||||||
}
|
|
||||||
while (std::getline(file, line)) {
|
|
||||||
// Si la linea contiene el texto [animation] se realiza el proceso de carga de una animación
|
|
||||||
if (line == "[animation]") {
|
|
||||||
animation_t buffer;
|
|
||||||
buffer.counter = 0;
|
|
||||||
buffer.currentFrame = 0;
|
|
||||||
buffer.completed = false;
|
|
||||||
|
|
||||||
do {
|
|
||||||
std::getline(file, line);
|
|
||||||
|
|
||||||
// Encuentra la posición del caracter '='
|
|
||||||
int pos = line.find("=");
|
|
||||||
|
|
||||||
// Procesa las dos subcadenas
|
|
||||||
if (pos != (int)line.npos) {
|
|
||||||
if (line.substr(0, pos) == "name") {
|
|
||||||
buffer.name = line.substr(pos + 1, line.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (line.substr(0, pos) == "speed") {
|
|
||||||
buffer.speed = std::stoi(line.substr(pos + 1, line.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (line.substr(0, pos) == "loop") {
|
|
||||||
buffer.loop = std::stoi(line.substr(pos + 1, line.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (line.substr(0, pos) == "frames") {
|
|
||||||
// Se introducen los valores separados por comas en un vector
|
|
||||||
std::stringstream ss(line.substr(pos + 1, line.length()));
|
|
||||||
std::string tmp;
|
|
||||||
SDL_Rect rect = {0, 0, frameWidth, frameHeight};
|
|
||||||
while (getline(ss, tmp, ',')) {
|
|
||||||
// Comprueba que el tile no sea mayor que el maximo indice permitido
|
|
||||||
const int numTile = std::stoi(tmp) > maxTiles ? 0 : std::stoi(tmp);
|
|
||||||
rect.x = (numTile % framesPerRow) * frameWidth;
|
|
||||||
rect.y = (numTile / framesPerRow) * frameHeight;
|
|
||||||
buffer.frames.push_back(rect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << line.substr(0, pos).c_str() << "\"" << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (line != "[/animation]");
|
|
||||||
|
|
||||||
// Añade la animación al vector de animaciones
|
|
||||||
as.animations.push_back(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// En caso contrario se parsea el fichero para buscar las variables y los valores
|
|
||||||
else {
|
|
||||||
// Encuentra la posición del caracter '='
|
|
||||||
int pos = line.find("=");
|
|
||||||
|
|
||||||
// Procesa las dos subcadenas
|
|
||||||
if (pos != (int)line.npos) {
|
|
||||||
if (line.substr(0, pos) == "framesPerRow") {
|
|
||||||
framesPerRow = std::stoi(line.substr(pos + 1, line.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (line.substr(0, pos) == "frameWidth") {
|
|
||||||
frameWidth = std::stoi(line.substr(pos + 1, line.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (line.substr(0, pos) == "frameHeight") {
|
|
||||||
frameHeight = std::stoi(line.substr(pos + 1, line.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << line.substr(0, pos).c_str() << "\"" << std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normaliza valores
|
|
||||||
if (framesPerRow == 0 && frameWidth > 0) {
|
|
||||||
framesPerRow = texture->getWidth() / frameWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxTiles == 0 && frameWidth > 0 && frameHeight > 0) {
|
|
||||||
const int w = texture->getWidth() / frameWidth;
|
|
||||||
const int h = texture->getHeight() / frameHeight;
|
|
||||||
maxTiles = w * h;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cierra el fichero
|
|
||||||
file.close();
|
|
||||||
}
|
|
||||||
// El fichero no se puede abrir
|
|
||||||
else {
|
|
||||||
if (verbose) {
|
|
||||||
std::cout << "Warning: Unable to open " << filename.c_str() << " file" << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return as;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constructor
|
|
||||||
AnimatedSprite::AnimatedSprite(Texture *texture, SDL_Renderer *renderer, std::string file, std::vector<std::string> *buffer) {
|
|
||||||
// Copia los punteros
|
|
||||||
setTexture(texture);
|
|
||||||
setRenderer(renderer);
|
|
||||||
|
|
||||||
// Carga las animaciones
|
|
||||||
if (file != "") {
|
|
||||||
animatedSprite_t as = loadAnimationFromFile(texture, file);
|
|
||||||
|
|
||||||
// Copia los datos de las animaciones
|
|
||||||
for (auto animation : as.animations) {
|
|
||||||
this->animation.push_back(animation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (buffer) {
|
|
||||||
loadFromVector(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inicializa variables
|
|
||||||
currentAnimation = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Constructor
|
|
||||||
AnimatedSprite::AnimatedSprite(SDL_Renderer *renderer, animatedSprite_t *animation) {
|
|
||||||
// Copia los punteros
|
|
||||||
setTexture(animation->texture);
|
|
||||||
setRenderer(renderer);
|
|
||||||
|
|
||||||
// Inicializa variables
|
|
||||||
currentAnimation = 0;
|
|
||||||
|
|
||||||
// Copia los datos de las animaciones
|
|
||||||
for (auto a : animation->animations) {
|
|
||||||
this->animation.push_back(a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destructor
|
|
||||||
AnimatedSprite::~AnimatedSprite() {
|
|
||||||
for (auto &a : animation) {
|
|
||||||
a.frames.clear();
|
|
||||||
}
|
|
||||||
animation.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene el indice de la animación a partir del nombre
|
|
||||||
int AnimatedSprite::getIndex(std::string name) {
|
|
||||||
int index = -1;
|
|
||||||
|
|
||||||
for (auto a : animation) {
|
|
||||||
index++;
|
|
||||||
if (a.name == name) {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "** Warning: could not find \"" << name.c_str() << "\" animation" << std::endl;
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calcula el frame correspondiente a la animación
|
|
||||||
void AnimatedSprite::animate() {
|
|
||||||
if (!enabled || animation[currentAnimation].speed == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calcula el frame actual a partir del contador
|
|
||||||
animation[currentAnimation].currentFrame = animation[currentAnimation].counter / animation[currentAnimation].speed;
|
|
||||||
|
|
||||||
// Si alcanza el final de la animación, reinicia el contador de la animación
|
|
||||||
// en función de la variable loop y coloca el nuevo frame
|
|
||||||
if (animation[currentAnimation].currentFrame >= (int)animation[currentAnimation].frames.size()) {
|
|
||||||
if (animation[currentAnimation].loop == -1) { // Si no hay loop, deja el último frame
|
|
||||||
animation[currentAnimation].currentFrame = animation[currentAnimation].frames.size();
|
|
||||||
animation[currentAnimation].completed = true;
|
|
||||||
} else { // Si hay loop, vuelve al frame indicado
|
|
||||||
animation[currentAnimation].counter = 0;
|
|
||||||
animation[currentAnimation].currentFrame = animation[currentAnimation].loop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// En caso contrario
|
|
||||||
else {
|
|
||||||
// Escoge el frame correspondiente de la animación
|
|
||||||
setSpriteClip(animation[currentAnimation].frames[animation[currentAnimation].currentFrame]);
|
|
||||||
|
|
||||||
// Incrementa el contador de la animacion
|
|
||||||
animation[currentAnimation].counter++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene el numero de frames de la animación actual
|
|
||||||
int AnimatedSprite::getNumFrames() {
|
|
||||||
return (int)animation[currentAnimation].frames.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el frame actual de la animación
|
|
||||||
void AnimatedSprite::setCurrentFrame(int num) {
|
|
||||||
// Descarta valores fuera de rango
|
|
||||||
if (num >= (int)animation[currentAnimation].frames.size()) {
|
|
||||||
num = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cambia el valor de la variable
|
|
||||||
animation[currentAnimation].currentFrame = num;
|
|
||||||
animation[currentAnimation].counter = 0;
|
|
||||||
|
|
||||||
// Escoge el frame correspondiente de la animación
|
|
||||||
setSpriteClip(animation[currentAnimation].frames[animation[currentAnimation].currentFrame]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el valor del contador
|
|
||||||
void AnimatedSprite::setAnimationCounter(std::string name, int num) {
|
|
||||||
animation[getIndex(name)].counter = num;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece la velocidad de una animación
|
|
||||||
void AnimatedSprite::setAnimationSpeed(std::string name, int speed) {
|
|
||||||
animation[getIndex(name)].counter = speed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece la velocidad de una animación
|
|
||||||
void AnimatedSprite::setAnimationSpeed(int index, int speed) {
|
|
||||||
animation[index].counter = speed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece si la animación se reproduce en bucle
|
|
||||||
void AnimatedSprite::setAnimationLoop(std::string name, int loop) {
|
|
||||||
animation[getIndex(name)].loop = loop;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece si la animación se reproduce en bucle
|
|
||||||
void AnimatedSprite::setAnimationLoop(int index, int loop) {
|
|
||||||
animation[index].loop = loop;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void AnimatedSprite::setAnimationCompleted(std::string name, bool value) {
|
|
||||||
animation[getIndex(name)].completed = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// OLD - Establece el valor de la variable
|
|
||||||
void AnimatedSprite::setAnimationCompleted(int index, bool value) {
|
|
||||||
animation[index].completed = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprueba si ha terminado la animación
|
|
||||||
bool AnimatedSprite::animationIsCompleted() {
|
|
||||||
return animation[currentAnimation].completed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Devuelve el rectangulo de una animación y frame concreto
|
|
||||||
SDL_Rect AnimatedSprite::getAnimationClip(std::string name, Uint8 index) {
|
|
||||||
return animation[getIndex(name)].frames[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Devuelve el rectangulo de una animación y frame concreto
|
|
||||||
SDL_Rect AnimatedSprite::getAnimationClip(int indexA, Uint8 indexF) {
|
|
||||||
return animation[indexA].frames[indexF];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carga la animación desde un vector
|
|
||||||
bool AnimatedSprite::loadFromVector(std::vector<std::string> *source) {
|
|
||||||
// Inicializa variables
|
|
||||||
int framesPerRow = 0;
|
|
||||||
int frameWidth = 0;
|
|
||||||
int frameHeight = 0;
|
|
||||||
int maxTiles = 0;
|
|
||||||
|
|
||||||
// Indicador de éxito en el proceso
|
|
||||||
bool success = true;
|
|
||||||
std::string line;
|
|
||||||
|
|
||||||
// Recorre todo el vector
|
|
||||||
int index = 0;
|
|
||||||
while (index < (int)source->size()) {
|
|
||||||
// Lee desde el vector
|
|
||||||
line = source->at(index);
|
|
||||||
|
|
||||||
// Si la linea contiene el texto [animation] se realiza el proceso de carga de una animación
|
|
||||||
if (line == "[animation]") {
|
|
||||||
animation_t buffer;
|
|
||||||
buffer.counter = 0;
|
|
||||||
buffer.currentFrame = 0;
|
|
||||||
buffer.completed = false;
|
|
||||||
|
|
||||||
do {
|
|
||||||
// Aumenta el indice para leer la siguiente linea
|
|
||||||
index++;
|
|
||||||
line = source->at(index);
|
|
||||||
|
|
||||||
// Encuentra la posición del caracter '='
|
|
||||||
int pos = line.find("=");
|
|
||||||
|
|
||||||
// Procesa las dos subcadenas
|
|
||||||
if (pos != (int)line.npos) {
|
|
||||||
if (line.substr(0, pos) == "name") {
|
|
||||||
buffer.name = line.substr(pos + 1, line.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (line.substr(0, pos) == "speed") {
|
|
||||||
buffer.speed = std::stoi(line.substr(pos + 1, line.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (line.substr(0, pos) == "loop") {
|
|
||||||
buffer.loop = std::stoi(line.substr(pos + 1, line.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (line.substr(0, pos) == "frames") {
|
|
||||||
// Se introducen los valores separados por comas en un vector
|
|
||||||
std::stringstream ss(line.substr(pos + 1, line.length()));
|
|
||||||
std::string tmp;
|
|
||||||
SDL_Rect rect = {0, 0, frameWidth, frameHeight};
|
|
||||||
while (getline(ss, tmp, ',')) {
|
|
||||||
// Comprueba que el tile no sea mayor que el maximo indice permitido
|
|
||||||
const int numTile = std::stoi(tmp) > maxTiles ? 0 : std::stoi(tmp);
|
|
||||||
rect.x = (numTile % framesPerRow) * frameWidth;
|
|
||||||
rect.y = (numTile / framesPerRow) * frameHeight;
|
|
||||||
buffer.frames.push_back(rect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
std::cout << "Warning: unknown parameter " << line.substr(0, pos).c_str() << std::endl;
|
|
||||||
success = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (line != "[/animation]");
|
|
||||||
|
|
||||||
// Añade la animación al vector de animaciones
|
|
||||||
animation.push_back(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// En caso contrario se parsea el fichero para buscar las variables y los valores
|
|
||||||
else {
|
|
||||||
// Encuentra la posición del caracter '='
|
|
||||||
int pos = line.find("=");
|
|
||||||
|
|
||||||
// Procesa las dos subcadenas
|
|
||||||
if (pos != (int)line.npos) {
|
|
||||||
if (line.substr(0, pos) == "framesPerRow") {
|
|
||||||
framesPerRow = std::stoi(line.substr(pos + 1, line.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (line.substr(0, pos) == "frameWidth") {
|
|
||||||
frameWidth = std::stoi(line.substr(pos + 1, line.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (line.substr(0, pos) == "frameHeight") {
|
|
||||||
frameHeight = std::stoi(line.substr(pos + 1, line.length()));
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
std::cout << "Warning: unknown parameter " << line.substr(0, pos).c_str() << std::endl;
|
|
||||||
success = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normaliza valores
|
|
||||||
if (framesPerRow == 0 && frameWidth > 0) {
|
|
||||||
framesPerRow = texture->getWidth() / frameWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxTiles == 0 && frameWidth > 0 && frameHeight > 0) {
|
|
||||||
const int w = texture->getWidth() / frameWidth;
|
|
||||||
const int h = texture->getHeight() / frameHeight;
|
|
||||||
maxTiles = w * h;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Una vez procesada la linea, aumenta el indice para pasar a la siguiente
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pone un valor por defecto
|
|
||||||
setRect({0, 0, frameWidth, frameHeight});
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece la animacion actual
|
|
||||||
void AnimatedSprite::setCurrentAnimation(std::string name) {
|
|
||||||
const int newAnimation = getIndex(name);
|
|
||||||
if (currentAnimation != newAnimation) {
|
|
||||||
currentAnimation = newAnimation;
|
|
||||||
animation[currentAnimation].currentFrame = 0;
|
|
||||||
animation[currentAnimation].counter = 0;
|
|
||||||
animation[currentAnimation].completed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece la animacion actual
|
|
||||||
void AnimatedSprite::setCurrentAnimation(int index) {
|
|
||||||
const int newAnimation = index;
|
|
||||||
if (currentAnimation != newAnimation) {
|
|
||||||
currentAnimation = newAnimation;
|
|
||||||
animation[currentAnimation].currentFrame = 0;
|
|
||||||
animation[currentAnimation].counter = 0;
|
|
||||||
animation[currentAnimation].completed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actualiza las variables del objeto
|
|
||||||
void AnimatedSprite::update() {
|
|
||||||
animate();
|
|
||||||
MovingSprite::update();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el rectangulo para un frame de una animación
|
|
||||||
void AnimatedSprite::setAnimationFrames(Uint8 index_animation, Uint8 index_frame, int x, int y, int w, int h) {
|
|
||||||
animation[index_animation].frames.push_back({x, y, w, h});
|
|
||||||
}
|
|
||||||
|
|
||||||
// OLD - Establece el contador para todas las animaciones
|
|
||||||
void AnimatedSprite::setAnimationCounter(int value) {
|
|
||||||
for (auto &a : animation) {
|
|
||||||
a.counter = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reinicia la animación
|
|
||||||
void AnimatedSprite::resetAnimation() {
|
|
||||||
animation[currentAnimation].currentFrame = 0;
|
|
||||||
animation[currentAnimation].counter = 0;
|
|
||||||
animation[currentAnimation].completed = false;
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
|
|
||||||
#include <string> // for string, basic_string
|
|
||||||
#include <vector> // for vector
|
|
||||||
|
|
||||||
#include "movingsprite.h" // for MovingSprite
|
|
||||||
class Texture;
|
|
||||||
|
|
||||||
struct animation_t {
|
|
||||||
std::string name; // Nombre de la animacion
|
|
||||||
std::vector<SDL_Rect> frames; // Cada uno de los frames que componen la animación
|
|
||||||
int speed; // Velocidad de la animación
|
|
||||||
int loop; // Indica a que frame vuelve la animación al terminar. -1 para que no vuelva
|
|
||||||
bool completed; // Indica si ha finalizado la animación
|
|
||||||
int currentFrame; // Frame actual
|
|
||||||
int counter; // Contador para las animaciones
|
|
||||||
};
|
|
||||||
|
|
||||||
struct animatedSprite_t {
|
|
||||||
std::vector<animation_t> animations; // Vector con las diferentes animaciones
|
|
||||||
Texture *texture; // Textura con los graficos para el sprite
|
|
||||||
};
|
|
||||||
|
|
||||||
// Carga la animación desde un fichero
|
|
||||||
animatedSprite_t loadAnimationFromFile(Texture *texture, std::string filePath, bool verbose = false);
|
|
||||||
|
|
||||||
class AnimatedSprite : public MovingSprite {
|
|
||||||
private:
|
|
||||||
// Variables
|
|
||||||
std::vector<animation_t> animation; // Vector con las diferentes animaciones
|
|
||||||
int currentAnimation; // Animacion activa
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Constructor
|
|
||||||
AnimatedSprite(Texture *texture = nullptr, SDL_Renderer *renderer = nullptr, std::string file = "", std::vector<std::string> *buffer = nullptr);
|
|
||||||
AnimatedSprite(SDL_Renderer *renderer, animatedSprite_t *animation);
|
|
||||||
|
|
||||||
// Destructor
|
|
||||||
~AnimatedSprite();
|
|
||||||
|
|
||||||
// Calcula el frame correspondiente a la animación actual
|
|
||||||
void animate();
|
|
||||||
|
|
||||||
// Obtiene el numero de frames de la animación actual
|
|
||||||
int getNumFrames();
|
|
||||||
|
|
||||||
// Establece el frame actual de la animación
|
|
||||||
void setCurrentFrame(int num);
|
|
||||||
|
|
||||||
// Establece el valor del contador
|
|
||||||
void setAnimationCounter(std::string name, int num);
|
|
||||||
|
|
||||||
// Establece la velocidad de una animación
|
|
||||||
void setAnimationSpeed(std::string name, int speed);
|
|
||||||
void setAnimationSpeed(int index, int speed);
|
|
||||||
|
|
||||||
// Establece el frame al que vuelve la animación al finalizar
|
|
||||||
void setAnimationLoop(std::string name, int loop);
|
|
||||||
void setAnimationLoop(int index, int loop);
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void setAnimationCompleted(std::string name, bool value);
|
|
||||||
void setAnimationCompleted(int index, bool value);
|
|
||||||
|
|
||||||
// Comprueba si ha terminado la animación
|
|
||||||
bool animationIsCompleted();
|
|
||||||
|
|
||||||
// Devuelve el rectangulo de una animación y frame concreto
|
|
||||||
SDL_Rect getAnimationClip(std::string name = "default", Uint8 index = 0);
|
|
||||||
SDL_Rect getAnimationClip(int indexA = 0, Uint8 indexF = 0);
|
|
||||||
|
|
||||||
// Obtiene el indice de la animación a partir del nombre
|
|
||||||
int getIndex(std::string name);
|
|
||||||
|
|
||||||
// Carga la animación desde un vector
|
|
||||||
bool loadFromVector(std::vector<std::string> *source);
|
|
||||||
|
|
||||||
// Establece la animacion actual
|
|
||||||
void setCurrentAnimation(std::string name = "default");
|
|
||||||
void setCurrentAnimation(int index = 0);
|
|
||||||
|
|
||||||
// Actualiza las variables del objeto
|
|
||||||
void update();
|
|
||||||
|
|
||||||
// OLD - Establece el rectangulo para un frame de una animación
|
|
||||||
void setAnimationFrames(Uint8 index_animation, Uint8 index_frame, int x, int y, int w, int h);
|
|
||||||
|
|
||||||
// OLD - Establece el contador para todas las animaciones
|
|
||||||
void setAnimationCounter(int value);
|
|
||||||
|
|
||||||
// Reinicia la animación
|
|
||||||
void resetAnimation();
|
|
||||||
};
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
#include "asset.h"
|
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
#include <stddef.h> // for size_t
|
|
||||||
|
|
||||||
#include <iostream> // for basic_ostream, operator<<, cout, endl
|
|
||||||
|
|
||||||
// Constructor
|
|
||||||
Asset::Asset(std::string executablePath) {
|
|
||||||
this->executablePath = executablePath.substr(0, executablePath.find_last_of("\\/"));
|
|
||||||
longestName = 0;
|
|
||||||
verbose = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Añade un elemento a la lista
|
|
||||||
void Asset::add(std::string file, enum assetType type, bool required, bool absolute) {
|
|
||||||
item_t temp;
|
|
||||||
temp.file = absolute ? file : executablePath + file;
|
|
||||||
temp.type = type;
|
|
||||||
temp.required = required;
|
|
||||||
fileList.push_back(temp);
|
|
||||||
|
|
||||||
const std::string filename = file.substr(file.find_last_of("\\/") + 1);
|
|
||||||
longestName = SDL_max(longestName, filename.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Devuelve el fichero de un elemento de la lista a partir de una cadena
|
|
||||||
std::string Asset::get(std::string text) {
|
|
||||||
for (auto f : fileList) {
|
|
||||||
const size_t lastIndex = f.file.find_last_of("/") + 1;
|
|
||||||
const std::string file = f.file.substr(lastIndex, std::string::npos);
|
|
||||||
|
|
||||||
if (file == text) {
|
|
||||||
return f.file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (verbose) {
|
|
||||||
std::cout << "Warning: file " << text.c_str() << " not found" << std::endl;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprueba que existen todos los elementos
|
|
||||||
bool Asset::check() {
|
|
||||||
bool success = true;
|
|
||||||
|
|
||||||
if (verbose) {
|
|
||||||
std::cout << "\n** Checking files" << std::endl;
|
|
||||||
|
|
||||||
std::cout << "Executable path is: " << executablePath << std::endl;
|
|
||||||
std::cout << "Sample filepath: " << fileList.back().file << std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprueba la lista de ficheros clasificandolos por tipo
|
|
||||||
for (int type = 0; type < t_maxAssetType; ++type) {
|
|
||||||
// Comprueba si hay ficheros de ese tipo
|
|
||||||
bool any = false;
|
|
||||||
|
|
||||||
for (auto f : fileList) {
|
|
||||||
if ((f.required) && (f.type == type)) {
|
|
||||||
any = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si hay ficheros de ese tipo, comprueba si existen
|
|
||||||
if (any) {
|
|
||||||
if (verbose) {
|
|
||||||
std::cout << "\n>> " << getTypeName(type).c_str() << " FILES" << std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto f : fileList) {
|
|
||||||
if ((f.required) && (f.type == type)) {
|
|
||||||
success &= checkFile(f.file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resultado
|
|
||||||
if (verbose) {
|
|
||||||
if (success) {
|
|
||||||
std::cout << "\n** All files OK.\n"
|
|
||||||
<< std::endl;
|
|
||||||
} else {
|
|
||||||
std::cout << "\n** A file is missing. Exiting.\n"
|
|
||||||
<< std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprueba que existe un fichero
|
|
||||||
bool Asset::checkFile(std::string path) {
|
|
||||||
bool success = false;
|
|
||||||
std::string result = "ERROR";
|
|
||||||
|
|
||||||
// Comprueba si existe el fichero
|
|
||||||
const std::string filename = path.substr(path.find_last_of("\\/") + 1);
|
|
||||||
SDL_IOStream *file = SDL_IOFromFile(path.c_str(), "rb");
|
|
||||||
|
|
||||||
if (file != nullptr) {
|
|
||||||
result = "OK";
|
|
||||||
success = true;
|
|
||||||
SDL_CloseIO(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (verbose) {
|
|
||||||
std::cout.setf(std::ios::left, std::ios::adjustfield);
|
|
||||||
std::cout << "Checking file: ";
|
|
||||||
std::cout.width(longestName + 2);
|
|
||||||
std::cout.fill('.');
|
|
||||||
std::cout << filename + " ";
|
|
||||||
std::cout << " [" + result + "]" << std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Devuelve el nombre del tipo de recurso
|
|
||||||
std::string Asset::getTypeName(int type) {
|
|
||||||
switch (type) {
|
|
||||||
case t_bitmap:
|
|
||||||
return "BITMAP";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case t_music:
|
|
||||||
return "MUSIC";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case t_sound:
|
|
||||||
return "SOUND";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case t_font:
|
|
||||||
return "FONT";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case t_lang:
|
|
||||||
return "LANG";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case t_data:
|
|
||||||
return "DATA";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case t_room:
|
|
||||||
return "ROOM";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case t_enemy:
|
|
||||||
return "ENEMY";
|
|
||||||
break;
|
|
||||||
|
|
||||||
case t_item:
|
|
||||||
return "ITEM";
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return "ERROR";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece si ha de mostrar texto por pantalla
|
|
||||||
void Asset::setVerbose(bool value) {
|
|
||||||
verbose = value;
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string> // for string, basic_string
|
|
||||||
#include <vector> // for vector
|
|
||||||
|
|
||||||
enum assetType {
|
|
||||||
t_bitmap,
|
|
||||||
t_music,
|
|
||||||
t_sound,
|
|
||||||
t_font,
|
|
||||||
t_lang,
|
|
||||||
t_data,
|
|
||||||
t_room,
|
|
||||||
t_enemy,
|
|
||||||
t_item,
|
|
||||||
t_maxAssetType
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clase Asset
|
|
||||||
class Asset {
|
|
||||||
private:
|
|
||||||
// Estructura para definir un item
|
|
||||||
struct item_t {
|
|
||||||
std::string file; // Ruta del fichero desde la raiz del directorio
|
|
||||||
enum assetType type; // Indica el tipo de recurso
|
|
||||||
bool required; // Indica si es un fichero que debe de existir
|
|
||||||
// bool absolute; // Indica si la ruta que se ha proporcionado es una ruta absoluta
|
|
||||||
};
|
|
||||||
|
|
||||||
// Variables
|
|
||||||
int longestName; // Contiene la longitud del nombre de fichero mas largo
|
|
||||||
std::vector<item_t> fileList; // Listado con todas las rutas a los ficheros
|
|
||||||
std::string executablePath; // Ruta al ejecutable
|
|
||||||
bool verbose; // Indica si ha de mostrar información por pantalla
|
|
||||||
|
|
||||||
// Comprueba que existe un fichero
|
|
||||||
bool checkFile(std::string executablePath);
|
|
||||||
|
|
||||||
// Devuelve el nombre del tipo de recurso
|
|
||||||
std::string getTypeName(int type);
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Constructor
|
|
||||||
Asset(std::string path);
|
|
||||||
|
|
||||||
// Añade un elemento a la lista
|
|
||||||
void add(std::string file, enum assetType type, bool required = true, bool absolute = false);
|
|
||||||
|
|
||||||
// Devuelve un elemento de la lista a partir de una cadena
|
|
||||||
std::string get(std::string text);
|
|
||||||
|
|
||||||
// Comprueba que existen todos los elementos
|
|
||||||
bool check();
|
|
||||||
|
|
||||||
// Establece si ha de mostrar texto por pantalla
|
|
||||||
void setVerbose(bool value);
|
|
||||||
};
|
|
||||||
@@ -1,781 +0,0 @@
|
|||||||
#include "balloon.h"
|
|
||||||
|
|
||||||
#include <math.h> // for abs
|
|
||||||
|
|
||||||
#include "animatedsprite.h" // for AnimatedSprite
|
|
||||||
#include "const.h" // for PLAY_AREA_LEFT, PLAY_AREA_RIGHT, PLAY_AR...
|
|
||||||
#include "movingsprite.h" // for MovingSprite
|
|
||||||
#include "sprite.h" // for Sprite
|
|
||||||
#include "texture.h" // for Texture
|
|
||||||
|
|
||||||
// Constructor
|
|
||||||
Balloon::Balloon(float x, float y, Uint8 kind, float velx, float speed, Uint16 creationtimer, Texture *texture, std::vector<std::string> *animation, SDL_Renderer *renderer) {
|
|
||||||
sprite = new AnimatedSprite(texture, renderer, "", animation);
|
|
||||||
disable();
|
|
||||||
|
|
||||||
enabled = true;
|
|
||||||
|
|
||||||
switch (kind) {
|
|
||||||
case BALLOON_1:
|
|
||||||
// Alto y ancho del objeto
|
|
||||||
width = BALLOON_WIDTH_1;
|
|
||||||
height = BALLOON_WIDTH_1;
|
|
||||||
size = BALLOON_SIZE_1;
|
|
||||||
power = 1;
|
|
||||||
|
|
||||||
// Inicializa los valores de velocidad y gravedad
|
|
||||||
this->velX = velx;
|
|
||||||
velY = 0;
|
|
||||||
maxVelY = 3.0f;
|
|
||||||
gravity = 0.09f;
|
|
||||||
defaultVelY = 2.6f;
|
|
||||||
|
|
||||||
// Puntos que da el globo al ser destruido
|
|
||||||
score = BALLOON_SCORE_1;
|
|
||||||
|
|
||||||
// Amenaza que genera el globo
|
|
||||||
menace = 1;
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BALLOON_2:
|
|
||||||
// Alto y ancho del objeto
|
|
||||||
width = BALLOON_WIDTH_2;
|
|
||||||
height = BALLOON_WIDTH_2;
|
|
||||||
size = BALLOON_SIZE_2;
|
|
||||||
power = 3;
|
|
||||||
|
|
||||||
// Inicializa los valores de velocidad y gravedad
|
|
||||||
this->velX = velx;
|
|
||||||
velY = 0;
|
|
||||||
maxVelY = 3.0f;
|
|
||||||
gravity = 0.10f;
|
|
||||||
defaultVelY = 3.5f;
|
|
||||||
|
|
||||||
// Puntos que da el globo al ser destruido
|
|
||||||
score = BALLOON_SCORE_2;
|
|
||||||
|
|
||||||
// Amenaza que genera el globo
|
|
||||||
menace = 2;
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BALLOON_3:
|
|
||||||
// Alto y ancho del objeto
|
|
||||||
width = BALLOON_WIDTH_3;
|
|
||||||
height = BALLOON_WIDTH_3;
|
|
||||||
size = BALLOON_SIZE_3;
|
|
||||||
power = 7;
|
|
||||||
|
|
||||||
// Inicializa los valores de velocidad y gravedad
|
|
||||||
this->velX = velx;
|
|
||||||
velY = 0;
|
|
||||||
maxVelY = 3.0f;
|
|
||||||
gravity = 0.10f;
|
|
||||||
defaultVelY = 4.50f;
|
|
||||||
|
|
||||||
// Puntos que da el globo al ser destruido
|
|
||||||
score = BALLOON_SCORE_3;
|
|
||||||
|
|
||||||
// Amenaza que genera el globo
|
|
||||||
menace = 4;
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BALLOON_4:
|
|
||||||
// Alto y ancho del objeto
|
|
||||||
width = BALLOON_WIDTH_4;
|
|
||||||
height = BALLOON_WIDTH_4;
|
|
||||||
size = BALLOON_SIZE_4;
|
|
||||||
power = 15;
|
|
||||||
|
|
||||||
// Inicializa los valores de velocidad y gravedad
|
|
||||||
this->velX = velx;
|
|
||||||
velY = 0;
|
|
||||||
maxVelY = 3.0f;
|
|
||||||
gravity = 0.10f;
|
|
||||||
defaultVelY = 4.95f;
|
|
||||||
|
|
||||||
// Puntos que da el globo al ser destruido
|
|
||||||
score = BALLOON_SCORE_4;
|
|
||||||
|
|
||||||
// Amenaza que genera el globo
|
|
||||||
menace = 8;
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HEXAGON_1:
|
|
||||||
// Alto y ancho del objeto
|
|
||||||
width = BALLOON_WIDTH_1;
|
|
||||||
height = BALLOON_WIDTH_1;
|
|
||||||
size = BALLOON_SIZE_1;
|
|
||||||
power = 1;
|
|
||||||
|
|
||||||
// Inicializa los valores de velocidad y gravedad
|
|
||||||
this->velX = velx;
|
|
||||||
velY = abs(velx) * 2;
|
|
||||||
maxVelY = abs(velx) * 2;
|
|
||||||
gravity = 0.00f;
|
|
||||||
defaultVelY = abs(velx) * 2;
|
|
||||||
|
|
||||||
// Puntos que da el globo al ser destruido
|
|
||||||
score = BALLOON_SCORE_1;
|
|
||||||
|
|
||||||
// Amenaza que genera el globo
|
|
||||||
menace = 1;
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HEXAGON_2:
|
|
||||||
// Alto y ancho del objeto
|
|
||||||
width = BALLOON_WIDTH_2;
|
|
||||||
height = BALLOON_WIDTH_2;
|
|
||||||
size = BALLOON_SIZE_2;
|
|
||||||
power = 3;
|
|
||||||
|
|
||||||
// Inicializa los valores de velocidad y gravedad
|
|
||||||
this->velX = velx;
|
|
||||||
velY = abs(velx) * 2;
|
|
||||||
maxVelY = abs(velx) * 2;
|
|
||||||
gravity = 0.00f;
|
|
||||||
defaultVelY = abs(velx) * 2;
|
|
||||||
|
|
||||||
// Puntos que da el globo al ser destruido
|
|
||||||
score = BALLOON_SCORE_2;
|
|
||||||
|
|
||||||
// Amenaza que genera el globo
|
|
||||||
menace = 2;
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HEXAGON_3:
|
|
||||||
// Alto y ancho del objeto
|
|
||||||
width = BALLOON_WIDTH_3;
|
|
||||||
height = BALLOON_WIDTH_3;
|
|
||||||
size = BALLOON_SIZE_3;
|
|
||||||
power = 7;
|
|
||||||
|
|
||||||
// Inicializa los valores de velocidad y gravedad
|
|
||||||
this->velX = velx;
|
|
||||||
velY = abs(velx) * 2;
|
|
||||||
maxVelY = abs(velx) * 2;
|
|
||||||
gravity = 0.00f;
|
|
||||||
defaultVelY = abs(velx) * 2;
|
|
||||||
|
|
||||||
// Puntos que da el globo al ser destruido
|
|
||||||
score = BALLOON_SCORE_3;
|
|
||||||
|
|
||||||
// Amenaza que genera el globo
|
|
||||||
menace = 4;
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HEXAGON_4:
|
|
||||||
// Alto y ancho del objeto
|
|
||||||
width = BALLOON_WIDTH_4;
|
|
||||||
height = BALLOON_WIDTH_4;
|
|
||||||
size = BALLOON_SIZE_4;
|
|
||||||
power = 15;
|
|
||||||
|
|
||||||
// Inicializa los valores de velocidad y gravedad
|
|
||||||
this->velX = velx;
|
|
||||||
velY = abs(velx) * 2;
|
|
||||||
maxVelY = abs(velx) * 2;
|
|
||||||
gravity = 0.00f;
|
|
||||||
defaultVelY = abs(velx) * 2;
|
|
||||||
|
|
||||||
// Puntos que da el globo al ser destruido
|
|
||||||
score = BALLOON_SCORE_4;
|
|
||||||
|
|
||||||
// Amenaza que genera el globo
|
|
||||||
menace = 8;
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case POWER_BALL:
|
|
||||||
// Alto y ancho del objeto
|
|
||||||
width = BALLOON_WIDTH_4;
|
|
||||||
height = BALLOON_WIDTH_4;
|
|
||||||
size = 4;
|
|
||||||
power = 0;
|
|
||||||
|
|
||||||
// Inicializa los valores de velocidad y gravedad
|
|
||||||
this->velX = velx;
|
|
||||||
velY = 0;
|
|
||||||
maxVelY = 3.0f;
|
|
||||||
gravity = 0.10f;
|
|
||||||
defaultVelY = 4.95f;
|
|
||||||
|
|
||||||
// Puntos que da el globo al ser destruido
|
|
||||||
score = 0;
|
|
||||||
|
|
||||||
// Amenaza que genera el globo
|
|
||||||
menace = 0;
|
|
||||||
|
|
||||||
// Añade rotación al sprite
|
|
||||||
sprite->setRotate(false);
|
|
||||||
sprite->setRotateSpeed(0);
|
|
||||||
if (velX > 0.0f) {
|
|
||||||
sprite->setRotateAmount(2.0);
|
|
||||||
} else {
|
|
||||||
sprite->setRotateAmount(-2.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Posición inicial
|
|
||||||
posX = x;
|
|
||||||
posY = y;
|
|
||||||
|
|
||||||
// Valores para el efecto de rebote
|
|
||||||
bouncing.enabled = false;
|
|
||||||
bouncing.counter = 0;
|
|
||||||
bouncing.speed = 2;
|
|
||||||
bouncing.zoomW = 1.0f;
|
|
||||||
bouncing.zoomH = 1.0f;
|
|
||||||
bouncing.despX = 0.0f;
|
|
||||||
bouncing.despY = 0.0f;
|
|
||||||
bouncing.w = {1.10f, 1.05f, 1.00f, 0.95f, 0.90f, 0.95f, 1.00f, 1.02f, 1.05f, 1.02f};
|
|
||||||
bouncing.h = {0.90f, 0.95f, 1.00f, 1.05f, 1.10f, 1.05f, 1.00f, 0.98f, 0.95f, 0.98f};
|
|
||||||
|
|
||||||
// Alto y ancho del sprite
|
|
||||||
sprite->setWidth(width);
|
|
||||||
sprite->setHeight(height);
|
|
||||||
|
|
||||||
// Posición X,Y del sprite
|
|
||||||
sprite->setPosX((int)posX);
|
|
||||||
sprite->setPosY((int)posY);
|
|
||||||
|
|
||||||
// Tamaño del circulo de colisión
|
|
||||||
collider.r = width / 2;
|
|
||||||
|
|
||||||
// Alinea el circulo de colisión con el objeto
|
|
||||||
updateColliders();
|
|
||||||
|
|
||||||
// Inicializa variables
|
|
||||||
stopped = true;
|
|
||||||
stoppedCounter = 0;
|
|
||||||
blinking = false;
|
|
||||||
visible = true;
|
|
||||||
invulnerable = true;
|
|
||||||
beingCreated = true;
|
|
||||||
creationCounter = creationtimer;
|
|
||||||
creationCounterIni = creationtimer;
|
|
||||||
popping = false;
|
|
||||||
|
|
||||||
// Actualiza valores
|
|
||||||
beingCreated = creationCounter == 0 ? false : true;
|
|
||||||
invulnerable = beingCreated == false ? false : true;
|
|
||||||
|
|
||||||
counter = 0;
|
|
||||||
travelY = 1.0f;
|
|
||||||
this->speed = speed;
|
|
||||||
|
|
||||||
// Tipo
|
|
||||||
this->kind = kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destructor
|
|
||||||
Balloon::~Balloon() {
|
|
||||||
delete sprite;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Centra el globo en la posición X
|
|
||||||
void Balloon::allignTo(int x) {
|
|
||||||
posX = float(x - (width / 2));
|
|
||||||
|
|
||||||
if (posX < PLAY_AREA_LEFT)
|
|
||||||
posX = PLAY_AREA_LEFT + 1;
|
|
||||||
else if ((posX + width) > PLAY_AREA_RIGHT)
|
|
||||||
posX = float(PLAY_AREA_RIGHT - width - 1);
|
|
||||||
|
|
||||||
// Posición X,Y del sprite
|
|
||||||
sprite->setPosX(getPosX());
|
|
||||||
sprite->setPosY(getPosY());
|
|
||||||
|
|
||||||
// Alinea el circulo de colisión con el objeto
|
|
||||||
updateColliders();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pinta el globo en la pantalla
|
|
||||||
void Balloon::render() {
|
|
||||||
if ((visible) && (enabled)) {
|
|
||||||
if (bouncing.enabled) {
|
|
||||||
if (kind != POWER_BALL) {
|
|
||||||
// Aplica desplazamiento para el zoom
|
|
||||||
sprite->setPosX(getPosX() + bouncing.despX);
|
|
||||||
sprite->setPosY(getPosY() + bouncing.despY);
|
|
||||||
sprite->render();
|
|
||||||
sprite->setPosX(getPosX() - bouncing.despX);
|
|
||||||
sprite->setPosY(getPosY() - bouncing.despY);
|
|
||||||
}
|
|
||||||
} else if (isBeingCreated()) {
|
|
||||||
// Aplica alpha blending
|
|
||||||
sprite->getTexture()->setAlpha(255 - (int)((float)creationCounter * (255.0f / (float)creationCounterIni)));
|
|
||||||
sprite->render();
|
|
||||||
if (kind == POWER_BALL) {
|
|
||||||
Sprite *sp = new Sprite(sprite->getRect(), sprite->getTexture(), sprite->getRenderer());
|
|
||||||
sp->setSpriteClip(407, 0, 37, 37);
|
|
||||||
sp->render();
|
|
||||||
delete sp;
|
|
||||||
}
|
|
||||||
sprite->getTexture()->setAlpha(255);
|
|
||||||
} else {
|
|
||||||
sprite->render();
|
|
||||||
|
|
||||||
if (kind == POWER_BALL and !popping) {
|
|
||||||
Sprite *sp = new Sprite(sprite->getRect(), sprite->getTexture(), sprite->getRenderer());
|
|
||||||
sp->setSpriteClip(407, 0, 37, 37);
|
|
||||||
sp->render();
|
|
||||||
delete sp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actualiza la posición y estados del globo
|
|
||||||
void Balloon::move() {
|
|
||||||
// Comprueba si se puede mover
|
|
||||||
if (!isStopped()) {
|
|
||||||
// Lo mueve a izquierda o derecha
|
|
||||||
posX += (velX * speed);
|
|
||||||
|
|
||||||
// Si queda fuera de pantalla, corregimos su posición y cambiamos su sentido
|
|
||||||
if ((posX < PLAY_AREA_LEFT) || (posX + width > PLAY_AREA_RIGHT)) {
|
|
||||||
// Corrige posición
|
|
||||||
posX -= (velX * speed);
|
|
||||||
|
|
||||||
// Invierte sentido
|
|
||||||
velX = -velX;
|
|
||||||
|
|
||||||
// Invierte la rotación
|
|
||||||
sprite->switchRotate();
|
|
||||||
|
|
||||||
// Activa el efecto de rebote
|
|
||||||
if (kind != POWER_BALL) {
|
|
||||||
bounceStart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mueve el globo hacia arriba o hacia abajo
|
|
||||||
posY += (velY * speed);
|
|
||||||
|
|
||||||
// Si se sale por arriba
|
|
||||||
if (posY < PLAY_AREA_TOP) {
|
|
||||||
// Corrige
|
|
||||||
posY = PLAY_AREA_TOP;
|
|
||||||
|
|
||||||
// Invierte sentido
|
|
||||||
velY = -velY;
|
|
||||||
|
|
||||||
// Activa el efecto de rebote
|
|
||||||
if (kind != POWER_BALL) {
|
|
||||||
bounceStart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si el globo se sale por la parte inferior
|
|
||||||
if (posY + height > PLAY_AREA_BOTTOM) {
|
|
||||||
// Corrige
|
|
||||||
posY = PLAY_AREA_BOTTOM - height;
|
|
||||||
|
|
||||||
// Invierte colocando una velocidad por defecto
|
|
||||||
velY = -defaultVelY;
|
|
||||||
|
|
||||||
// Activa el efecto de rebote
|
|
||||||
if (kind != POWER_BALL) {
|
|
||||||
bounceStart();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
Para aplicar la gravedad, el diseño original la aplicaba en cada iteración del bucle
|
|
||||||
Al añadir el modificador de velocidad se reduce la distancia que recorre el objeto y por
|
|
||||||
tanto recibe mas gravedad. Para solucionarlo se va a aplicar la gravedad cuando se haya
|
|
||||||
recorrido una distancia igual a la velocidad en Y, que era el cálculo inicial
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Incrementa la variable que calcula la distancia acumulada en Y
|
|
||||||
travelY += speed;
|
|
||||||
|
|
||||||
// Si la distancia acumulada en Y es igual a la velocidad, se aplica la gravedad
|
|
||||||
if (travelY >= 1.0f) {
|
|
||||||
// Quita el excedente
|
|
||||||
travelY -= 1.0f;
|
|
||||||
|
|
||||||
// Aplica la gravedad al objeto sin pasarse de una velocidad máxima
|
|
||||||
velY += gravity;
|
|
||||||
|
|
||||||
// Al parecer esta asignación se quedó sin hacer y ahora el juego no funciona
|
|
||||||
// correctamente si se aplica, así que se deja sin efecto
|
|
||||||
// velY = std::min(velY, maxVelY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actualiza la posición del sprite
|
|
||||||
sprite->setPosX(getPosX());
|
|
||||||
sprite->setPosY(getPosY());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deshabilita el globo y pone a cero todos los valores
|
|
||||||
void Balloon::disable() {
|
|
||||||
beingCreated = false;
|
|
||||||
blinking = false;
|
|
||||||
collider.r = 0;
|
|
||||||
collider.x = 0;
|
|
||||||
collider.y = 0;
|
|
||||||
counter = 0;
|
|
||||||
creationCounter = 0;
|
|
||||||
creationCounterIni = 0;
|
|
||||||
defaultVelY = 0.0f;
|
|
||||||
enabled = false;
|
|
||||||
gravity = 0.0f;
|
|
||||||
height = 0;
|
|
||||||
invulnerable = false;
|
|
||||||
kind = 0;
|
|
||||||
maxVelY = 0.0f;
|
|
||||||
menace = 0;
|
|
||||||
popping = false;
|
|
||||||
posX = 0.0f;
|
|
||||||
posY = 0.0f;
|
|
||||||
power = 0;
|
|
||||||
score = 0;
|
|
||||||
size = 0;
|
|
||||||
speed = 0;
|
|
||||||
stopped = false;
|
|
||||||
stoppedCounter = 0;
|
|
||||||
travelY = 0;
|
|
||||||
velX = 0.0f;
|
|
||||||
velY = 0.0f;
|
|
||||||
visible = false;
|
|
||||||
width = 0;
|
|
||||||
sprite->clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explosiona el globo
|
|
||||||
void Balloon::pop() {
|
|
||||||
setPopping(true);
|
|
||||||
sprite->disableRotate();
|
|
||||||
setStop(true);
|
|
||||||
setStoppedTimer(2000);
|
|
||||||
setInvulnerable(true);
|
|
||||||
menace = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actualiza al globo a su posicion, animación y controla los contadores
|
|
||||||
void Balloon::update() {
|
|
||||||
if (enabled) {
|
|
||||||
sprite->MovingSprite::update();
|
|
||||||
move();
|
|
||||||
updateAnimation();
|
|
||||||
updateColliders();
|
|
||||||
updateState();
|
|
||||||
updateBounce();
|
|
||||||
counter++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actualiza los estados del globo
|
|
||||||
void Balloon::updateState() {
|
|
||||||
// Si está explotando
|
|
||||||
if (isPopping()) {
|
|
||||||
setInvulnerable(true);
|
|
||||||
setStop(true);
|
|
||||||
if (sprite->animationIsCompleted()) {
|
|
||||||
disable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si se está creando
|
|
||||||
if (isBeingCreated()) {
|
|
||||||
// Actualiza el valor de las variables
|
|
||||||
setStop(true);
|
|
||||||
setInvulnerable(true);
|
|
||||||
|
|
||||||
// Todavia tiene tiempo en el contador
|
|
||||||
if (creationCounter > 0) {
|
|
||||||
// Desplaza lentamente el globo hacia abajo y hacia un lado
|
|
||||||
if (creationCounter % 10 == 0) {
|
|
||||||
posY++;
|
|
||||||
posX += velX;
|
|
||||||
|
|
||||||
// Comprueba no se salga por los laterales
|
|
||||||
if ((posX < PLAY_AREA_LEFT) || (posX > (PLAY_AREA_RIGHT - width))) {
|
|
||||||
// Corrige y cambia el sentido de la velocidad
|
|
||||||
posX -= velX;
|
|
||||||
velX = -velX;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actualiza la posición del sprite
|
|
||||||
sprite->setPosX(getPosX());
|
|
||||||
sprite->setPosY(getPosY());
|
|
||||||
|
|
||||||
// Actualiza la posición del circulo de colisión
|
|
||||||
updateColliders();
|
|
||||||
}
|
|
||||||
|
|
||||||
creationCounter--;
|
|
||||||
}
|
|
||||||
// El contador ha llegado a cero
|
|
||||||
else {
|
|
||||||
setBeingCreated(false);
|
|
||||||
setStop(false);
|
|
||||||
setVisible(true);
|
|
||||||
setInvulnerable(false);
|
|
||||||
if (kind == POWER_BALL) {
|
|
||||||
sprite->setRotate(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Solo comprueba el estado detenido cuando no se está creando
|
|
||||||
else if (isStopped()) {
|
|
||||||
// Si es una powerball deja de rodar
|
|
||||||
if (kind == POWER_BALL) {
|
|
||||||
sprite->setRotate(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reduce el contador
|
|
||||||
if (stoppedCounter > 0) {
|
|
||||||
stoppedCounter--;
|
|
||||||
}
|
|
||||||
// Quitarles el estado "detenido" si no estan explosionando
|
|
||||||
else if (!isPopping()) {
|
|
||||||
// Si es una powerball vuelve a rodar
|
|
||||||
if (kind == POWER_BALL) {
|
|
||||||
sprite->setRotate(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
setStop(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece la animación correspondiente al estado
|
|
||||||
void Balloon::updateAnimation() {
|
|
||||||
std::string creatingAnimation = "blue";
|
|
||||||
std::string normalAnimation = "orange";
|
|
||||||
|
|
||||||
if (kind == POWER_BALL) {
|
|
||||||
creatingAnimation = "powerball";
|
|
||||||
normalAnimation = "powerball";
|
|
||||||
} else if (getClass() == HEXAGON_CLASS) {
|
|
||||||
creatingAnimation = "red";
|
|
||||||
normalAnimation = "green";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el frame de animación
|
|
||||||
if (isPopping()) {
|
|
||||||
sprite->setCurrentAnimation("pop");
|
|
||||||
} else if (isBeingCreated()) {
|
|
||||||
sprite->setCurrentAnimation(creatingAnimation);
|
|
||||||
} else {
|
|
||||||
sprite->setCurrentAnimation(normalAnimation);
|
|
||||||
}
|
|
||||||
|
|
||||||
sprite->animate();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprueba si el globo está habilitado
|
|
||||||
bool Balloon::isEnabled() {
|
|
||||||
return enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
float Balloon::getPosX() {
|
|
||||||
return posX;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
float Balloon::getPosY() {
|
|
||||||
return posY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
float Balloon::getVelY() {
|
|
||||||
return velY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
int Balloon::getWidth() {
|
|
||||||
return width;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
int Balloon::getHeight() {
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void Balloon::setVelY(float velY) {
|
|
||||||
this->velY = velY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void Balloon::setSpeed(float speed) {
|
|
||||||
this->speed = speed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
int Balloon::getKind() {
|
|
||||||
return kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
Uint8 Balloon::getSize() {
|
|
||||||
return size;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene la clase a la que pertenece el globo
|
|
||||||
Uint8 Balloon::getClass() {
|
|
||||||
if ((kind >= BALLOON_1) && (kind <= BALLOON_4)) {
|
|
||||||
return BALLOON_CLASS;
|
|
||||||
}
|
|
||||||
|
|
||||||
else if ((kind >= HEXAGON_1) && (kind <= HEXAGON_4)) {
|
|
||||||
return HEXAGON_CLASS;
|
|
||||||
}
|
|
||||||
|
|
||||||
return BALLOON_CLASS;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void Balloon::setStop(bool state) {
|
|
||||||
stopped = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
bool Balloon::isStopped() {
|
|
||||||
return stopped;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void Balloon::setBlink(bool value) {
|
|
||||||
blinking = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
bool Balloon::isBlinking() {
|
|
||||||
return blinking;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void Balloon::setVisible(bool value) {
|
|
||||||
visible = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
bool Balloon::isVisible() {
|
|
||||||
return visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void Balloon::setInvulnerable(bool value) {
|
|
||||||
invulnerable = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
bool Balloon::isInvulnerable() {
|
|
||||||
return invulnerable;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void Balloon::setBeingCreated(bool value) {
|
|
||||||
beingCreated = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
bool Balloon::isBeingCreated() {
|
|
||||||
return beingCreated;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void Balloon::setPopping(bool value) {
|
|
||||||
popping = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
bool Balloon::isPopping() {
|
|
||||||
return popping;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void Balloon::setStoppedTimer(Uint16 time) {
|
|
||||||
stoppedCounter = time;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
Uint16 Balloon::getStoppedTimer() {
|
|
||||||
return stoppedCounter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
Uint16 Balloon::getScore() {
|
|
||||||
return score;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene el circulo de colisión
|
|
||||||
circle_t &Balloon::getCollider() {
|
|
||||||
return collider;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alinea el circulo de colisión con la posición del objeto globo
|
|
||||||
void Balloon::updateColliders() {
|
|
||||||
collider.x = Uint16(posX + collider.r);
|
|
||||||
collider.y = posY + collider.r;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene le valor de la variable
|
|
||||||
Uint8 Balloon::getMenace() {
|
|
||||||
if (isEnabled()) {
|
|
||||||
return menace;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene le valor de la variable
|
|
||||||
Uint8 Balloon::getPower() {
|
|
||||||
return power;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Balloon::bounceStart() {
|
|
||||||
bouncing.enabled = true;
|
|
||||||
bouncing.zoomW = 1;
|
|
||||||
bouncing.zoomH = 1;
|
|
||||||
sprite->setZoomW(bouncing.zoomW);
|
|
||||||
sprite->setZoomH(bouncing.zoomH);
|
|
||||||
bouncing.despX = 0;
|
|
||||||
bouncing.despY = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Balloon::bounceStop() {
|
|
||||||
bouncing.enabled = false;
|
|
||||||
bouncing.counter = 0;
|
|
||||||
bouncing.zoomW = 1.0f;
|
|
||||||
bouncing.zoomH = 1.0f;
|
|
||||||
sprite->setZoomW(bouncing.zoomW);
|
|
||||||
sprite->setZoomH(bouncing.zoomH);
|
|
||||||
bouncing.despX = 0.0f;
|
|
||||||
bouncing.despY = 0.0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Balloon::updateBounce() {
|
|
||||||
if (bouncing.enabled) {
|
|
||||||
bouncing.zoomW = bouncing.w[bouncing.counter / bouncing.speed];
|
|
||||||
bouncing.zoomH = bouncing.h[bouncing.counter / bouncing.speed];
|
|
||||||
sprite->setZoomW(bouncing.zoomW);
|
|
||||||
sprite->setZoomH(bouncing.zoomH);
|
|
||||||
bouncing.despX = (sprite->getSpriteClip().w - (sprite->getSpriteClip().w * bouncing.zoomW));
|
|
||||||
bouncing.despY = (sprite->getSpriteClip().h - (sprite->getSpriteClip().h * bouncing.zoomH));
|
|
||||||
bouncing.counter++;
|
|
||||||
if ((bouncing.counter / bouncing.speed) > (MAX_BOUNCE - 1)) {
|
|
||||||
bounceStop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
|
|
||||||
#include <string> // for string
|
|
||||||
#include <vector> // for vector
|
|
||||||
|
|
||||||
#include "utils.h" // for circle_t
|
|
||||||
class AnimatedSprite;
|
|
||||||
class Texture;
|
|
||||||
|
|
||||||
// Cantidad de elementos del vector con los valores de la deformación del globo al rebotar
|
|
||||||
constexpr int MAX_BOUNCE = 10;
|
|
||||||
|
|
||||||
// Tipos de globo
|
|
||||||
constexpr int BALLOON_1 = 1;
|
|
||||||
constexpr int BALLOON_2 = 2;
|
|
||||||
constexpr int BALLOON_3 = 3;
|
|
||||||
constexpr int BALLOON_4 = 4;
|
|
||||||
constexpr int HEXAGON_1 = 5;
|
|
||||||
constexpr int HEXAGON_2 = 6;
|
|
||||||
constexpr int HEXAGON_3 = 7;
|
|
||||||
constexpr int HEXAGON_4 = 8;
|
|
||||||
constexpr int POWER_BALL = 9;
|
|
||||||
|
|
||||||
// Puntos de globo
|
|
||||||
constexpr int BALLOON_SCORE_1 = 50;
|
|
||||||
constexpr int BALLOON_SCORE_2 = 100;
|
|
||||||
constexpr int BALLOON_SCORE_3 = 200;
|
|
||||||
constexpr int BALLOON_SCORE_4 = 400;
|
|
||||||
|
|
||||||
// Tamaños de globo
|
|
||||||
constexpr int BALLOON_SIZE_1 = 1;
|
|
||||||
constexpr int BALLOON_SIZE_2 = 2;
|
|
||||||
constexpr int BALLOON_SIZE_3 = 3;
|
|
||||||
constexpr int BALLOON_SIZE_4 = 4;
|
|
||||||
|
|
||||||
// Clases de globo
|
|
||||||
constexpr int BALLOON_CLASS = 0;
|
|
||||||
constexpr int HEXAGON_CLASS = 1;
|
|
||||||
|
|
||||||
// Velocidad del globo
|
|
||||||
constexpr float BALLOON_VELX_POSITIVE = 0.7f;
|
|
||||||
constexpr float BALLOON_VELX_NEGATIVE = -0.7f;
|
|
||||||
|
|
||||||
// Índice para las animaciones de los globos
|
|
||||||
constexpr int BALLOON_MOVING_ANIMATION = 0;
|
|
||||||
constexpr int BALLOON_POP_ANIMATION = 1;
|
|
||||||
constexpr int BALLOON_BORN_ANIMATION = 2;
|
|
||||||
|
|
||||||
// Cantidad posible de globos
|
|
||||||
constexpr int MAX_BALLOONS = 100;
|
|
||||||
|
|
||||||
// Velocidades a las que se mueven los globos
|
|
||||||
constexpr float BALLOON_SPEED_1 = 0.60f;
|
|
||||||
constexpr float BALLOON_SPEED_2 = 0.70f;
|
|
||||||
constexpr float BALLOON_SPEED_3 = 0.80f;
|
|
||||||
constexpr float BALLOON_SPEED_4 = 0.90f;
|
|
||||||
constexpr float BALLOON_SPEED_5 = 1.00f;
|
|
||||||
|
|
||||||
// Tamaño de los globos
|
|
||||||
constexpr int BALLOON_WIDTH_1 = 8;
|
|
||||||
constexpr int BALLOON_WIDTH_2 = 13;
|
|
||||||
constexpr int BALLOON_WIDTH_3 = 21;
|
|
||||||
constexpr int BALLOON_WIDTH_4 = 37;
|
|
||||||
|
|
||||||
// PowerBall
|
|
||||||
constexpr int POWERBALL_SCREENPOWER_MINIMUM = 10;
|
|
||||||
constexpr int POWERBALL_COUNTER = 8;
|
|
||||||
|
|
||||||
// Clase Balloon
|
|
||||||
class Balloon {
|
|
||||||
private:
|
|
||||||
// Estructura para las variables para el efecto de los rebotes
|
|
||||||
struct bouncing {
|
|
||||||
bool enabled; // Si el efecto está activo
|
|
||||||
Uint8 counter; // Countador para el efecto
|
|
||||||
Uint8 speed; // Velocidad a la que transcurre el efecto
|
|
||||||
float zoomW; // Zoom aplicado a la anchura
|
|
||||||
float zoomH; // Zoom aplicado a la altura
|
|
||||||
float despX; // Desplazamiento de pixeles en el eje X antes de pintar el objeto con zoom
|
|
||||||
float despY; // Desplazamiento de pixeles en el eje Y antes de pintar el objeto con zoom
|
|
||||||
std::vector<float> w; // Vector con los valores de zoom para el ancho del globo
|
|
||||||
std::vector<float> h; // Vector con los valores de zoom para el alto del globo
|
|
||||||
};
|
|
||||||
|
|
||||||
// Objetos y punteros
|
|
||||||
AnimatedSprite *sprite; // Sprite del objeto globo
|
|
||||||
|
|
||||||
// Variables
|
|
||||||
float posX; // Posición en el eje X
|
|
||||||
float posY; // Posición en el eje Y
|
|
||||||
Uint8 width; // Ancho
|
|
||||||
Uint8 height; // Alto
|
|
||||||
float velX; // Velocidad en el eje X. Cantidad de pixeles a desplazarse
|
|
||||||
float velY; // Velocidad en el eje Y. Cantidad de pixeles a desplazarse
|
|
||||||
float gravity; // Aceleración en el eje Y. Modifica la velocidad
|
|
||||||
float defaultVelY; // Velocidad inicial que tienen al rebotar contra el suelo
|
|
||||||
float maxVelY; // Máxima velocidad que puede alcanzar el objeto en el eje Y
|
|
||||||
bool beingCreated; // Indica si el globo se está creando
|
|
||||||
bool blinking; // Indica si el globo está intermitente
|
|
||||||
bool enabled; // Indica si el globo esta activo
|
|
||||||
bool invulnerable; // Indica si el globo es invulnerable
|
|
||||||
bool popping; // Indica si el globo está explotando
|
|
||||||
bool stopped; // Indica si el globo está parado
|
|
||||||
bool visible; // Indica si el globo es visible
|
|
||||||
circle_t collider; // Circulo de colisión del objeto
|
|
||||||
Uint16 creationCounter; // Temporizador para controlar el estado "creandose"
|
|
||||||
Uint16 creationCounterIni; // Valor inicial para el temporizador para controlar el estado "creandose"
|
|
||||||
Uint16 score; // Puntos que da el globo al ser destruido
|
|
||||||
Uint16 stoppedCounter; // Contador para controlar el estado "parado"
|
|
||||||
Uint8 kind; // Tipo de globo
|
|
||||||
Uint8 menace; // Cantidad de amenaza que genera el globo
|
|
||||||
Uint32 counter; // Contador interno
|
|
||||||
float travelY; // Distancia que ha de recorrer el globo en el eje Y antes de que se le aplique la gravedad
|
|
||||||
float speed; // Velocidad a la que se mueven los globos
|
|
||||||
Uint8 size; // Tamaño del globo
|
|
||||||
Uint8 power; // Cantidad de poder que alberga el globo
|
|
||||||
bouncing bouncing; // Contiene las variables para el efecto de rebote
|
|
||||||
|
|
||||||
// Alinea el circulo de colisión con la posición del objeto globo
|
|
||||||
void updateColliders();
|
|
||||||
|
|
||||||
// Activa el efecto
|
|
||||||
void bounceStart();
|
|
||||||
|
|
||||||
// Detiene el efecto
|
|
||||||
void bounceStop();
|
|
||||||
|
|
||||||
// Aplica el efecto
|
|
||||||
void updateBounce();
|
|
||||||
|
|
||||||
// Actualiza los estados del globo
|
|
||||||
void updateState();
|
|
||||||
|
|
||||||
// Establece la animación correspondiente
|
|
||||||
void updateAnimation();
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void setBeingCreated(bool value);
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Constructor
|
|
||||||
Balloon(float x, float y, Uint8 kind, float velx, float speed, Uint16 creationtimer, Texture *texture, std::vector<std::string> *animation, SDL_Renderer *renderer);
|
|
||||||
|
|
||||||
// Destructor
|
|
||||||
~Balloon();
|
|
||||||
|
|
||||||
// Centra el globo en la posición X
|
|
||||||
void allignTo(int x);
|
|
||||||
|
|
||||||
// Pinta el globo en la pantalla
|
|
||||||
void render();
|
|
||||||
|
|
||||||
// Actualiza la posición y estados del globo
|
|
||||||
void move();
|
|
||||||
|
|
||||||
// Deshabilita el globo y pone a cero todos los valores
|
|
||||||
void disable();
|
|
||||||
|
|
||||||
// Explosiona el globo
|
|
||||||
void pop();
|
|
||||||
|
|
||||||
// Actualiza al globo a su posicion, animación y controla los contadores
|
|
||||||
void update();
|
|
||||||
|
|
||||||
// Comprueba si el globo está habilitado
|
|
||||||
bool isEnabled();
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
float getPosX();
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
float getPosY();
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
float getVelY();
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
int getWidth();
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
int getHeight();
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void setVelY(float velY);
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void setSpeed(float speed);
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
int getKind();
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
Uint8 getSize();
|
|
||||||
|
|
||||||
// Obtiene la clase a la que pertenece el globo
|
|
||||||
Uint8 getClass();
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void setStop(bool value);
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
bool isStopped();
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void setBlink(bool value);
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
bool isBlinking();
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void setVisible(bool value);
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
bool isVisible();
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void setInvulnerable(bool value);
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
bool isInvulnerable();
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
bool isBeingCreated();
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void setPopping(bool value);
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
bool isPopping();
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void setStoppedTimer(Uint16 time);
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
Uint16 getStoppedTimer();
|
|
||||||
|
|
||||||
// Obtiene del valor de la variable
|
|
||||||
Uint16 getScore();
|
|
||||||
|
|
||||||
// Obtiene el circulo de colisión
|
|
||||||
circle_t &getCollider();
|
|
||||||
|
|
||||||
// Obtiene le valor de la variable
|
|
||||||
Uint8 getMenace();
|
|
||||||
|
|
||||||
// Obtiene le valor de la variable
|
|
||||||
Uint8 getPower();
|
|
||||||
};
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
#include "bullet.h"
|
|
||||||
|
|
||||||
#include "const.h" // for NO_KIND, PLAY_AREA_LEFT, PLAY_AREA_RIGHT, PLAY_A...
|
|
||||||
#include "sprite.h" // for Sprite
|
|
||||||
class Texture;
|
|
||||||
|
|
||||||
// Constructor
|
|
||||||
Bullet::Bullet(int x, int y, int kind, bool poweredUp, int owner, Texture *texture, SDL_Renderer *renderer) {
|
|
||||||
sprite = new Sprite({x, y, 10, 10}, texture, renderer);
|
|
||||||
|
|
||||||
// Posición inicial del objeto
|
|
||||||
posX = x;
|
|
||||||
posY = y;
|
|
||||||
|
|
||||||
// Alto y ancho del objeto
|
|
||||||
width = 10;
|
|
||||||
height = 10;
|
|
||||||
|
|
||||||
// Velocidad inicial en el eje Y
|
|
||||||
velY = -3;
|
|
||||||
|
|
||||||
// Tipo de bala
|
|
||||||
this->kind = kind;
|
|
||||||
|
|
||||||
// Identificador del dueño del objeto
|
|
||||||
this->owner = owner;
|
|
||||||
|
|
||||||
// Valores especificos según el tipo
|
|
||||||
switch (kind) {
|
|
||||||
case BULLET_UP:
|
|
||||||
// Establece la velocidad inicial
|
|
||||||
velX = 0;
|
|
||||||
|
|
||||||
// Rectangulo con los gráficos del objeto
|
|
||||||
if (!poweredUp) {
|
|
||||||
sprite->setSpriteClip(0 * width, 0, sprite->getWidth(), sprite->getHeight());
|
|
||||||
} else {
|
|
||||||
sprite->setSpriteClip((0 + 3) * width, 0, sprite->getWidth(), sprite->getHeight());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BULLET_LEFT:
|
|
||||||
// Establece la velocidad inicial
|
|
||||||
velX = -2;
|
|
||||||
|
|
||||||
// Rectangulo con los gráficos del objeto
|
|
||||||
if (!poweredUp) {
|
|
||||||
sprite->setSpriteClip(1 * width, 0, sprite->getWidth(), sprite->getHeight());
|
|
||||||
} else {
|
|
||||||
sprite->setSpriteClip((1 + 3) * width, 0, sprite->getWidth(), sprite->getHeight());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case BULLET_RIGHT:
|
|
||||||
// Establece la velocidad inicial
|
|
||||||
velX = 2;
|
|
||||||
|
|
||||||
// Rectangulo con los gráficos del objeto
|
|
||||||
if (!poweredUp) {
|
|
||||||
sprite->setSpriteClip(2 * width, 0, sprite->getWidth(), sprite->getHeight());
|
|
||||||
} else {
|
|
||||||
sprite->setSpriteClip((2 + 3) * width, 0, sprite->getWidth(), sprite->getHeight());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el tamaño del circulo de colisión
|
|
||||||
collider.r = width / 2;
|
|
||||||
|
|
||||||
// Alinea el circulo de colisión con el objeto
|
|
||||||
shiftColliders();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destructor
|
|
||||||
Bullet::~Bullet() {
|
|
||||||
delete sprite;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pinta el objeto en pantalla
|
|
||||||
void Bullet::render() {
|
|
||||||
sprite->render();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actualiza la posición y estado del objeto en horizontal
|
|
||||||
Uint8 Bullet::move() {
|
|
||||||
// Variable con el valor de retorno
|
|
||||||
Uint8 msg = BULLET_MOVE_OK;
|
|
||||||
|
|
||||||
// Mueve el objeto a su nueva posición
|
|
||||||
posX += velX;
|
|
||||||
|
|
||||||
// Si el objeto se sale del area de juego por los laterales
|
|
||||||
if ((posX < PLAY_AREA_LEFT - width) || (posX > PLAY_AREA_RIGHT)) {
|
|
||||||
// Se deshabilita
|
|
||||||
kind = NO_KIND;
|
|
||||||
|
|
||||||
// Mensaje de salida
|
|
||||||
msg = BULLET_MOVE_OUT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mueve el objeto a su nueva posición en vertical
|
|
||||||
posY += int(velY);
|
|
||||||
|
|
||||||
// Si el objeto se sale del area de juego por la parte superior
|
|
||||||
if (posY < PLAY_AREA_TOP - height) {
|
|
||||||
// Se deshabilita
|
|
||||||
kind = NO_KIND;
|
|
||||||
|
|
||||||
// Mensaje de salida
|
|
||||||
msg = BULLET_MOVE_OUT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actualiza la posición del sprite
|
|
||||||
sprite->setPosX(posX);
|
|
||||||
sprite->setPosY(posY);
|
|
||||||
|
|
||||||
// Alinea el circulo de colisión con el objeto
|
|
||||||
shiftColliders();
|
|
||||||
|
|
||||||
return msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprueba si el objeto está habilitado
|
|
||||||
bool Bullet::isEnabled() {
|
|
||||||
if (kind == NO_KIND) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deshabilita el objeto
|
|
||||||
void Bullet::disable() {
|
|
||||||
kind = NO_KIND;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene el valor de la variable
|
|
||||||
int Bullet::getPosX() {
|
|
||||||
return posX;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene el valor de la variable
|
|
||||||
int Bullet::getPosY() {
|
|
||||||
return posY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void Bullet::setPosX(int x) {
|
|
||||||
posX = x;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void Bullet::setPosY(int y) {
|
|
||||||
posY = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene el valor de la variable
|
|
||||||
int Bullet::getVelY() {
|
|
||||||
return velY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene el valor de la variable
|
|
||||||
int Bullet::getKind() {
|
|
||||||
return kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene el valor de la variable
|
|
||||||
int Bullet::getOwner() {
|
|
||||||
return owner;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene el circulo de colisión
|
|
||||||
circle_t &Bullet::getCollider() {
|
|
||||||
return collider;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alinea el circulo de colisión con el objeto
|
|
||||||
void Bullet::shiftColliders() {
|
|
||||||
collider.x = posX + collider.r;
|
|
||||||
collider.y = posY + collider.r;
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
|
|
||||||
#include "utils.h" // for circle_t
|
|
||||||
class Sprite;
|
|
||||||
class Texture;
|
|
||||||
|
|
||||||
// Tipos de bala
|
|
||||||
constexpr int BULLET_UP = 1;
|
|
||||||
constexpr int BULLET_LEFT = 2;
|
|
||||||
constexpr int BULLET_RIGHT = 3;
|
|
||||||
|
|
||||||
// Tipos de retorno de la función move de la bala
|
|
||||||
constexpr int BULLET_MOVE_OK = 0;
|
|
||||||
constexpr int BULLET_MOVE_OUT = 1;
|
|
||||||
|
|
||||||
// Clase Bullet
|
|
||||||
class Bullet {
|
|
||||||
private:
|
|
||||||
// Objetos y punteros
|
|
||||||
Sprite *sprite; // Sprite con los graficos y métodos de pintado
|
|
||||||
|
|
||||||
// Variables
|
|
||||||
int posX; // Posición en el eje X
|
|
||||||
int posY; // Posición en el eje Y
|
|
||||||
Uint8 width; // Ancho del objeto
|
|
||||||
Uint8 height; // Alto del objeto
|
|
||||||
int velX; // Velocidad en el eje X
|
|
||||||
int velY; // Velocidad en el eje Y
|
|
||||||
int kind; // Tipo de objeto
|
|
||||||
int owner; // Identificador del dueño del objeto
|
|
||||||
circle_t collider; // Circulo de colisión del objeto
|
|
||||||
|
|
||||||
// Alinea el circulo de colisión con el objeto
|
|
||||||
void shiftColliders();
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Constructor
|
|
||||||
Bullet(int x, int y, int kind, bool poweredUp, int owner, Texture *texture, SDL_Renderer *renderer);
|
|
||||||
|
|
||||||
// Destructor
|
|
||||||
~Bullet();
|
|
||||||
|
|
||||||
// Pinta el objeto en pantalla
|
|
||||||
void render();
|
|
||||||
|
|
||||||
// Actualiza la posición y estado del objeto
|
|
||||||
Uint8 move();
|
|
||||||
|
|
||||||
// Comprueba si el objeto está habilitado
|
|
||||||
bool isEnabled();
|
|
||||||
|
|
||||||
// Deshabilita el objeto
|
|
||||||
void disable();
|
|
||||||
|
|
||||||
// Obtiene el valor de la variable
|
|
||||||
int getPosX();
|
|
||||||
|
|
||||||
// Obtiene el valor de la variable
|
|
||||||
int getPosY();
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void setPosX(int x);
|
|
||||||
|
|
||||||
// Establece el valor de la variable
|
|
||||||
void setPosY(int y);
|
|
||||||
|
|
||||||
// Obtiene el valor de la variable
|
|
||||||
int getVelY();
|
|
||||||
|
|
||||||
// Obtiene el valor de la variable
|
|
||||||
int getKind();
|
|
||||||
|
|
||||||
// Obtiene el valor de la variable
|
|
||||||
int getOwner();
|
|
||||||
|
|
||||||
// Obtiene el circulo de colisión
|
|
||||||
circle_t &getCollider();
|
|
||||||
};
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
|
|
||||||
#include "lang.h"
|
|
||||||
#include "utils.h"
|
|
||||||
|
|
||||||
// Tamaño de bloque
|
|
||||||
constexpr int BLOCK = 8;
|
|
||||||
constexpr int HALF_BLOCK = BLOCK / 2;
|
|
||||||
|
|
||||||
// Tamaño de la pantalla virtual
|
|
||||||
constexpr int GAMECANVAS_WIDTH = 256;
|
|
||||||
constexpr int GAMECANVAS_HEIGHT = 192;
|
|
||||||
|
|
||||||
// Zona de juego
|
|
||||||
constexpr int PLAY_AREA_TOP = (0 * BLOCK);
|
|
||||||
constexpr int PLAY_AREA_BOTTOM = GAMECANVAS_HEIGHT - (4 * BLOCK);
|
|
||||||
constexpr int PLAY_AREA_LEFT = (0 * BLOCK);
|
|
||||||
constexpr int PLAY_AREA_RIGHT = GAMECANVAS_WIDTH - (0 * BLOCK);
|
|
||||||
constexpr int PLAY_AREA_WIDTH = PLAY_AREA_RIGHT - PLAY_AREA_LEFT;
|
|
||||||
constexpr int PLAY_AREA_HEIGHT = PLAY_AREA_BOTTOM - PLAY_AREA_TOP;
|
|
||||||
constexpr int PLAY_AREA_CENTER_X = PLAY_AREA_LEFT + (PLAY_AREA_WIDTH / 2);
|
|
||||||
constexpr int PLAY_AREA_CENTER_FIRST_QUARTER_X = (PLAY_AREA_WIDTH / 4);
|
|
||||||
constexpr int PLAY_AREA_CENTER_THIRD_QUARTER_X = (PLAY_AREA_WIDTH / 4) * 3;
|
|
||||||
constexpr int PLAY_AREA_CENTER_Y = PLAY_AREA_TOP + (PLAY_AREA_HEIGHT / 2);
|
|
||||||
constexpr int PLAY_AREA_FIRST_QUARTER_Y = PLAY_AREA_HEIGHT / 4;
|
|
||||||
constexpr int PLAY_AREA_THIRD_QUARTER_Y = (PLAY_AREA_HEIGHT / 4) * 3;
|
|
||||||
|
|
||||||
// Anclajes de pantalla
|
|
||||||
constexpr int GAMECANVAS_CENTER_X = GAMECANVAS_WIDTH / 2;
|
|
||||||
constexpr int GAMECANVAS_FIRST_QUARTER_X = GAMECANVAS_WIDTH / 4;
|
|
||||||
constexpr int GAMECANVAS_THIRD_QUARTER_X = (GAMECANVAS_WIDTH / 4) * 3;
|
|
||||||
constexpr int GAMECANVAS_CENTER_Y = GAMECANVAS_HEIGHT / 2;
|
|
||||||
constexpr int GAMECANVAS_FIRST_QUARTER_Y = GAMECANVAS_HEIGHT / 4;
|
|
||||||
constexpr int GAMECANVAS_THIRD_QUARTER_Y = (GAMECANVAS_HEIGHT / 4) * 3;
|
|
||||||
|
|
||||||
// Secciones del programa
|
|
||||||
constexpr int SECTION_PROG_LOGO = 0;
|
|
||||||
constexpr int SECTION_PROG_INTRO = 1;
|
|
||||||
constexpr int SECTION_PROG_TITLE = 2;
|
|
||||||
constexpr int SECTION_PROG_GAME = 3;
|
|
||||||
constexpr int SECTION_PROG_QUIT = 4;
|
|
||||||
|
|
||||||
// Subsecciones
|
|
||||||
constexpr int SUBSECTION_GAME_PLAY_1P = 0;
|
|
||||||
constexpr int SUBSECTION_GAME_PLAY_2P = 1;
|
|
||||||
constexpr int SUBSECTION_GAME_PAUSE = 2;
|
|
||||||
constexpr int SUBSECTION_GAME_GAMEOVER = 3;
|
|
||||||
constexpr int SUBSECTION_TITLE_1 = 3;
|
|
||||||
constexpr int SUBSECTION_TITLE_2 = 4;
|
|
||||||
constexpr int SUBSECTION_TITLE_3 = 5;
|
|
||||||
constexpr int SUBSECTION_TITLE_INSTRUCTIONS = 6;
|
|
||||||
|
|
||||||
// Ningun tipo
|
|
||||||
constexpr int NO_KIND = 0;
|
|
||||||
|
|
||||||
// Colores
|
|
||||||
const color_t bgColor = {0x27, 0x27, 0x36};
|
|
||||||
const color_t noColor = {0xFF, 0xFF, 0xFF};
|
|
||||||
const color_t shdwTxtColor = {0x43, 0x43, 0x4F};
|
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
#include "core/audio/audio.hpp"
|
||||||
|
|
||||||
|
#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).
|
||||||
|
// clang-format off
|
||||||
|
#undef STB_VORBIS_HEADER_ONLY
|
||||||
|
// stb_vorbis (codi de tercers) dispara -Wtautological-compare; el silenciem
|
||||||
|
// només per a aquesta inclusió sense afectar el nostre codi.
|
||||||
|
#pragma GCC diagnostic push
|
||||||
|
#pragma GCC diagnostic ignored "-Wtautological-compare"
|
||||||
|
#include "external/stb_vorbis.h"
|
||||||
|
#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/audio_adapter.hpp" // Para AudioResource::getMusic/getSound
|
||||||
|
#include "core/audio/jail_audio.hpp" // Para Ja namespace
|
||||||
|
#include "game/options.hpp" // Para Options::audio
|
||||||
|
|
||||||
|
// Singleton
|
||||||
|
Audio* Audio::instance = nullptr;
|
||||||
|
|
||||||
|
// Inicializa la instancia única del singleton
|
||||||
|
void Audio::init() { Audio::instance = new Audio(); }
|
||||||
|
|
||||||
|
// Libera la instancia
|
||||||
|
void Audio::destroy() {
|
||||||
|
delete Audio::instance;
|
||||||
|
Audio::instance = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene la instancia
|
||||||
|
auto Audio::get() -> Audio* { return Audio::instance; }
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Audio::Audio() { initSDLAudio(); }
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
Audio::~Audio() {
|
||||||
|
Ja::quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Método principal
|
||||||
|
void Audio::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 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
|
||||||
|
if (music_.state == MusicState::PLAYING && music_.name == name && music_.loop == new_loop) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
if (music_enabled_ && music_.state == MusicState::PLAYING) {
|
||||||
|
Ja::pauseMusic();
|
||||||
|
music_.state = MusicState::PAUSED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continua la música pausada
|
||||||
|
void Audio::resumeMusic() {
|
||||||
|
if (music_enabled_ && music_.state == MusicState::PAUSED) {
|
||||||
|
Ja::resumeMusic();
|
||||||
|
music_.state = MusicState::PLAYING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detiene la música
|
||||||
|
void Audio::stopMusic() {
|
||||||
|
if (music_enabled_) {
|
||||||
|
Ja::stopMusic();
|
||||||
|
music_.state = MusicState::STOPPED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reproduce un sonido por nombre
|
||||||
|
void Audio::playSound(const std::string& name, Group group) const {
|
||||||
|
if (sound_enabled_) {
|
||||||
|
Ja::playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reproduce un sonido por puntero directo
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Realiza un fundido de salida de la música
|
||||||
|
void Audio::fadeOutMusic(int milliseconds) const {
|
||||||
|
if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) {
|
||||||
|
Ja::fadeOutMusic(milliseconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consulta directamente el estado real de la música en jailaudio
|
||||||
|
auto Audio::getRealMusicState() -> MusicState {
|
||||||
|
Ja::MusicState ja_state = Ja::getMusicState();
|
||||||
|
switch (ja_state) {
|
||||||
|
case Ja::MusicState::PLAYING:
|
||||||
|
return MusicState::PLAYING;
|
||||||
|
case Ja::MusicState::PAUSED:
|
||||||
|
return MusicState::PAUSED;
|
||||||
|
case Ja::MusicState::STOPPED:
|
||||||
|
case Ja::MusicState::INVALID:
|
||||||
|
case Ja::MusicState::DISABLED:
|
||||||
|
default:
|
||||||
|
return MusicState::STOPPED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplica la configuración
|
||||||
|
void Audio::applySettings() {
|
||||||
|
enable(Options::audio.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establecer estado general
|
||||||
|
void Audio::enable(bool value) {
|
||||||
|
enabled_ = value;
|
||||||
|
|
||||||
|
setSoundVolume(enabled_ ? Options::audio.sound.volume : MIN_VOLUME);
|
||||||
|
setMusicVolume(enabled_ ? Options::audio.music.volume : MIN_VOLUME);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializa SDL Audio
|
||||||
|
void Audio::initSDLAudio() {
|
||||||
|
if (!SDL_Init(SDL_INIT_AUDIO)) {
|
||||||
|
std::cout << "SDL_AUDIO could not initialize! SDL Error: " << SDL_GetError() << '\n';
|
||||||
|
} else {
|
||||||
|
Ja::init(FREQUENCY, SDL_AUDIO_S16LE, 2);
|
||||||
|
enable(Options::audio.enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cmath> // Para std::lround
|
||||||
|
#include <cstdint> // Para int8_t, uint8_t
|
||||||
|
#include <string> // Para string
|
||||||
|
|
||||||
|
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.0–1.0; la capa de
|
||||||
|
// presentació (menús, notificacions) usa les helpers toPercent/fromPercent
|
||||||
|
// per mostrar 0–100 a l'usuari.
|
||||||
|
class Audio {
|
||||||
|
public:
|
||||||
|
// --- Enums ---
|
||||||
|
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 : 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 (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
|
||||||
|
static void destroy(); // Libera el objeto Audio
|
||||||
|
static auto get() -> Audio*; // Obtiene el puntero al objeto Audio
|
||||||
|
Audio(const Audio&) = delete; // Evitar copia
|
||||||
|
auto operator=(const Audio&) -> Audio& = delete; // Evitar asignación
|
||||||
|
|
||||||
|
static void update(); // Actualización del sistema de audio
|
||||||
|
|
||||||
|
// --- Control de música ---
|
||||||
|
void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional)
|
||||||
|
void playMusic(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(Ja::Sound* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
|
||||||
|
void stopAllSounds() const; // Detener todos los sonidos
|
||||||
|
|
||||||
|
// --- Control de volumen (API interna: float 0.0..1.0) ---
|
||||||
|
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
|
||||||
|
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
|
||||||
|
void applySettings(); // Aplica la configuración
|
||||||
|
|
||||||
|
// --- Configuración de sonidos ---
|
||||||
|
void enableSound() { sound_enabled_ = true; } // Habilitar sonidos
|
||||||
|
void disableSound() { sound_enabled_ = false; } // Deshabilitar sonidos
|
||||||
|
void enableSound(bool value) { sound_enabled_ = value; } // Establecer estado de sonidos
|
||||||
|
void toggleSound() { sound_enabled_ = !sound_enabled_; } // Alternar estado de sonidos
|
||||||
|
|
||||||
|
// --- Configuración de música ---
|
||||||
|
void enableMusic() { music_enabled_ = true; } // Habilitar música
|
||||||
|
void disableMusic() { music_enabled_ = false; } // Deshabilitar música
|
||||||
|
void enableMusic(bool value) { music_enabled_ = value; } // Establecer estado de música
|
||||||
|
void toggleMusic() { music_enabled_ = !music_enabled_; } // Alternar estado de música
|
||||||
|
|
||||||
|
// --- Consultas de estado ---
|
||||||
|
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
|
||||||
|
[[nodiscard]] auto isSoundEnabled() const -> bool { return sound_enabled_; }
|
||||||
|
[[nodiscard]] auto isMusicEnabled() const -> bool { return music_enabled_; }
|
||||||
|
[[nodiscard]] auto getMusicState() const -> MusicState { return music_.state; }
|
||||||
|
[[nodiscard]] static auto getRealMusicState() -> MusicState;
|
||||||
|
[[nodiscard]] auto getCurrentMusicName() const -> const std::string& { return music_.name; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// --- Tipos anidados ---
|
||||||
|
struct Music {
|
||||||
|
MusicState state{MusicState::STOPPED}; // Estado actual de la música
|
||||||
|
std::string name; // Última pista de música reproducida
|
||||||
|
bool loop{false}; // Indica si se reproduce en bucle
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Métodos ---
|
||||||
|
Audio(); // Constructor privado
|
||||||
|
~Audio(); // Destructor privado
|
||||||
|
void initSDLAudio(); // Inicializa SDL Audio
|
||||||
|
|
||||||
|
// --- Variables miembro ---
|
||||||
|
static Audio* instance; // Instancia única de Audio
|
||||||
|
|
||||||
|
Music music_; // Estado de la música
|
||||||
|
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
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#include "core/audio/audio_adapter.hpp"
|
||||||
|
|
||||||
|
#include "core/resources/resource.h"
|
||||||
|
|
||||||
|
namespace AudioResource {
|
||||||
|
auto getMusic(const std::string& name) -> Ja::Music* {
|
||||||
|
return Resource::get()->getMusic(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto getSound(const std::string& name) -> Ja::Sound* {
|
||||||
|
return Resource::get()->getSound(name);
|
||||||
|
}
|
||||||
|
} // namespace AudioResource
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,698 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// --- Includes ---
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#define STB_VORBIS_HEADER_ONLY
|
||||||
|
#include "external/stb_vorbis.h" // Para stb_vorbis_open_memory i streaming
|
||||||
|
|
||||||
|
// Deleter stateless per a buffers reservats amb `SDL_malloc` / `SDL_LoadWAV*`.
|
||||||
|
// Compatible amb `std::unique_ptr<Uint8[], SdlFreeDeleter>` — zero size
|
||||||
|
// overhead gràcies a EBO, igual que un unique_ptr amb default_delete.
|
||||||
|
struct SdlFreeDeleter {
|
||||||
|
void operator()(Uint8* p) const noexcept {
|
||||||
|
if (p != nullptr) { SDL_free(p); }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace Ja {
|
||||||
|
|
||||||
|
// --- Public Enums ---
|
||||||
|
enum class ChannelState : std::uint8_t {
|
||||||
|
INVALID,
|
||||||
|
FREE,
|
||||||
|
PLAYING,
|
||||||
|
PAUSED,
|
||||||
|
DISABLED,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class MusicState : std::uint8_t {
|
||||||
|
INVALID,
|
||||||
|
PLAYING,
|
||||||
|
PAUSED,
|
||||||
|
STOPPED,
|
||||||
|
DISABLED,
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Constants ---
|
||||||
|
inline constexpr int MAX_SIMULTANEOUS_CHANNELS = 20;
|
||||||
|
inline constexpr int MAX_GROUPS = 2;
|
||||||
|
inline constexpr SDL_AudioSpec DEFAULT_SPEC{.format = SDL_AUDIO_S16, .channels = 2, .freq = 48000};
|
||||||
|
|
||||||
|
// --- Struct Definitions ---
|
||||||
|
struct Sound {
|
||||||
|
SDL_AudioSpec spec{DEFAULT_SPEC};
|
||||||
|
Uint32 length{0};
|
||||||
|
// Buffer descomprimit (PCM) propietat del sound. Reservat per SDL_LoadWAV
|
||||||
|
// via SDL_malloc; el deleter `SdlFreeDeleter` allibera amb SDL_free.
|
||||||
|
std::unique_ptr<Uint8[], SdlFreeDeleter> buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
// L'ordre (punters primer, ints després, enum de 8 bits al final) minimitza
|
||||||
|
// el padding a 64-bit (evita avisos de clang-analyzer-optin.performance.Padding).
|
||||||
|
struct Channel {
|
||||||
|
Sound* sound{nullptr};
|
||||||
|
SDL_AudioStream* stream{nullptr};
|
||||||
|
int pos{0};
|
||||||
|
int times{0};
|
||||||
|
int group{0};
|
||||||
|
ChannelState state{ChannelState::FREE};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Music {
|
||||||
|
SDL_AudioSpec spec{DEFAULT_SPEC};
|
||||||
|
|
||||||
|
// OGG comprimit en memòria. Propietat nostra; es copia des del buffer
|
||||||
|
// d'entrada una sola vegada en loadMusic i es descomprimix en chunks
|
||||||
|
// per streaming. Com que stb_vorbis guarda un punter persistent al
|
||||||
|
// `.data()` d'aquest vector, no el podem resize'jar un cop establert
|
||||||
|
// (una reallocation invalidaria el punter que el decoder conserva).
|
||||||
|
std::vector<Uint8> ogg_data;
|
||||||
|
stb_vorbis* vorbis{nullptr}; // handle del decoder, viu tot el cicle del Music
|
||||||
|
|
||||||
|
std::string filename;
|
||||||
|
|
||||||
|
int times{0}; // loops restants (-1 = infinit, 0 = un sol play)
|
||||||
|
SDL_AudioStream* stream{nullptr};
|
||||||
|
MusicState state{MusicState::INVALID};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FadeState {
|
||||||
|
bool active{false};
|
||||||
|
Uint64 start_time{0};
|
||||||
|
int duration_ms{0};
|
||||||
|
float initial_volume{0.0F};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OutgoingMusic {
|
||||||
|
SDL_AudioStream* stream{nullptr};
|
||||||
|
FadeState fade;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Internal Global State (inline, C++17) ---
|
||||||
|
inline Music* current_music{nullptr};
|
||||||
|
inline Channel channels[MAX_SIMULTANEOUS_CHANNELS];
|
||||||
|
|
||||||
|
inline SDL_AudioSpec audio_spec{DEFAULT_SPEC};
|
||||||
|
inline float music_volume{1.0F};
|
||||||
|
inline float sound_volume[MAX_GROUPS];
|
||||||
|
inline bool music_enabled{true};
|
||||||
|
inline bool sound_enabled{true};
|
||||||
|
inline SDL_AudioDeviceID sdl_audio_device{0};
|
||||||
|
|
||||||
|
inline OutgoingMusic outgoing_music;
|
||||||
|
inline FadeState incoming_fade;
|
||||||
|
|
||||||
|
// --- Forward Declarations ---
|
||||||
|
inline void stopMusic();
|
||||||
|
inline void stopChannel(int channel);
|
||||||
|
inline auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
|
||||||
|
inline void crossfadeMusic(Music* music, int crossfade_ms, int loop = -1);
|
||||||
|
|
||||||
|
// --- Music streaming internals ---
|
||||||
|
// Bytes-per-sample per canal (sempre s16)
|
||||||
|
inline constexpr int MUSIC_BYTES_PER_SAMPLE = 2;
|
||||||
|
// Quants shorts decodifiquem per crida a get_samples_short_interleaved.
|
||||||
|
// 8192 shorts = 4096 samples/channel en estèreo ≈ 85ms de so a 48kHz.
|
||||||
|
inline constexpr int MUSIC_CHUNK_SHORTS = 8192;
|
||||||
|
// Umbral d'audio per davant del cursor de reproducció. Mantenim ≥ 0.5 s a
|
||||||
|
// l'SDL_AudioStream per absorbir jitter de frame i evitar underruns.
|
||||||
|
inline constexpr float MUSIC_LOW_WATER_SECONDS = 0.5F;
|
||||||
|
|
||||||
|
// Decodifica un chunk del vorbis i el volca a l'stream. Retorna samples
|
||||||
|
// decodificats per canal (0 = EOF de l'stream vorbis).
|
||||||
|
inline auto feedMusicChunk(Music* music) -> int {
|
||||||
|
if ((music == nullptr) || (music->vorbis == nullptr) || (music->stream == nullptr)) { return 0; }
|
||||||
|
|
||||||
|
short chunk[MUSIC_CHUNK_SHORTS];
|
||||||
|
const int NUM_CHANNELS = music->spec.channels;
|
||||||
|
const int SAMPLES_PER_CHANNEL = stb_vorbis_get_samples_short_interleaved(
|
||||||
|
music->vorbis,
|
||||||
|
NUM_CHANNELS,
|
||||||
|
chunk,
|
||||||
|
MUSIC_CHUNK_SHORTS);
|
||||||
|
if (SAMPLES_PER_CHANNEL <= 0) { return 0; }
|
||||||
|
|
||||||
|
const int BYTES = SAMPLES_PER_CHANNEL * NUM_CHANNELS * MUSIC_BYTES_PER_SAMPLE;
|
||||||
|
SDL_PutAudioStreamData(music->stream, chunk, BYTES);
|
||||||
|
return SAMPLES_PER_CHANNEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reompli l'stream fins que tinga ≥ MUSIC_LOW_WATER_SECONDS bufferats.
|
||||||
|
// En arribar a EOF del vorbis, aplica el loop (times) o deixa drenar.
|
||||||
|
inline void pumpMusic(Music* music) {
|
||||||
|
if ((music == nullptr) || (music->vorbis == nullptr) || (music->stream == nullptr)) { return; }
|
||||||
|
|
||||||
|
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||||
|
const int LOW_WATER_BYTES = static_cast<int>(MUSIC_LOW_WATER_SECONDS * static_cast<float>(BYTES_PER_SECOND));
|
||||||
|
|
||||||
|
while (SDL_GetAudioStreamAvailable(music->stream) < LOW_WATER_BYTES) {
|
||||||
|
const int DECODED = feedMusicChunk(music);
|
||||||
|
if (DECODED > 0) { continue; }
|
||||||
|
|
||||||
|
// EOF: si queden loops, rebobinar; si no, tallar i deixar drenar.
|
||||||
|
if (music->times != 0) {
|
||||||
|
stb_vorbis_seek_start(music->vorbis);
|
||||||
|
if (music->times > 0) { music->times--; }
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-carrega `duration_ms` de so dins l'stream actual abans que l'stream
|
||||||
|
// siga robat per outgoing_music (crossfade o fade-out). Imprescindible amb
|
||||||
|
// streaming: l'stream robat no es pot re-alimentar perquè perd la referència
|
||||||
|
// al seu vorbis decoder. No aplica loop — si el vorbis s'esgota abans, parem.
|
||||||
|
inline void preFillOutgoing(Music* music, int duration_ms) {
|
||||||
|
if ((music == nullptr) || (music->vorbis == nullptr) || (music->stream == nullptr)) { return; }
|
||||||
|
|
||||||
|
const int BYTES_PER_SECOND = music->spec.freq * music->spec.channels * MUSIC_BYTES_PER_SAMPLE;
|
||||||
|
const int NEEDED_BYTES = static_cast<int>((static_cast<std::int64_t>(duration_ms) * BYTES_PER_SECOND) / 1000);
|
||||||
|
|
||||||
|
while (SDL_GetAudioStreamAvailable(music->stream) < NEEDED_BYTES) {
|
||||||
|
const int DECODED = feedMusicChunk(music);
|
||||||
|
if (DECODED <= 0) { break; } // EOF: deixem drenar el que hi haja
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- update() helpers ---
|
||||||
|
inline void updateOutgoingFade() {
|
||||||
|
if ((outgoing_music.stream == nullptr) || !outgoing_music.fade.active) { return; }
|
||||||
|
|
||||||
|
const Uint64 NOW = SDL_GetTicks();
|
||||||
|
const Uint64 ELAPSED = NOW - outgoing_music.fade.start_time;
|
||||||
|
if (ELAPSED >= static_cast<Uint64>(outgoing_music.fade.duration_ms)) {
|
||||||
|
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||||
|
outgoing_music.stream = nullptr;
|
||||||
|
outgoing_music.fade.active = false;
|
||||||
|
} else {
|
||||||
|
const float PERCENT = static_cast<float>(ELAPSED) / static_cast<float>(outgoing_music.fade.duration_ms);
|
||||||
|
SDL_SetAudioStreamGain(outgoing_music.stream, outgoing_music.fade.initial_volume * (1.0F - PERCENT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void updateIncomingFade() {
|
||||||
|
if (!incoming_fade.active) { return; }
|
||||||
|
|
||||||
|
const Uint64 NOW = SDL_GetTicks();
|
||||||
|
const Uint64 ELAPSED = NOW - incoming_fade.start_time;
|
||||||
|
if (ELAPSED >= static_cast<Uint64>(incoming_fade.duration_ms)) {
|
||||||
|
incoming_fade.active = false;
|
||||||
|
SDL_SetAudioStreamGain(current_music->stream, music_volume);
|
||||||
|
} else {
|
||||||
|
const float PERCENT = static_cast<float>(ELAPSED) / static_cast<float>(incoming_fade.duration_ms);
|
||||||
|
SDL_SetAudioStreamGain(current_music->stream, music_volume * PERCENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void updateCurrentMusic() {
|
||||||
|
if (!music_enabled || (current_music == nullptr) || current_music->state != MusicState::PLAYING) { return; }
|
||||||
|
|
||||||
|
updateIncomingFade();
|
||||||
|
|
||||||
|
// Streaming: rellenem l'stream fins al low-water-mark i parem si el
|
||||||
|
// vorbis s'ha esgotat i no queden loops.
|
||||||
|
pumpMusic(current_music);
|
||||||
|
if (current_music->times == 0 && SDL_GetAudioStreamAvailable(current_music->stream) == 0) {
|
||||||
|
stopMusic();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void updateSoundChannels() {
|
||||||
|
if (!sound_enabled) { return; }
|
||||||
|
|
||||||
|
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; ++i) {
|
||||||
|
auto& ch = channels[i];
|
||||||
|
if (ch.state != ChannelState::PLAYING) { continue; }
|
||||||
|
|
||||||
|
if (ch.times != 0) {
|
||||||
|
if (static_cast<Uint32>(SDL_GetAudioStreamAvailable(ch.stream)) < (ch.sound->length / 2)) {
|
||||||
|
SDL_PutAudioStreamData(ch.stream, ch.sound->buffer.get(), ch.sound->length);
|
||||||
|
if (ch.times > 0) { ch.times--; }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (SDL_GetAudioStreamAvailable(ch.stream) == 0) { stopChannel(i); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void update() {
|
||||||
|
updateOutgoingFade();
|
||||||
|
updateCurrentMusic();
|
||||||
|
updateSoundChannels();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void init(int freq, SDL_AudioFormat format, int num_channels) {
|
||||||
|
audio_spec = {.format = format, .channels = num_channels, .freq = freq};
|
||||||
|
if (sdl_audio_device != 0) { SDL_CloseAudioDevice(sdl_audio_device); }
|
||||||
|
sdl_audio_device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audio_spec);
|
||||||
|
if (sdl_audio_device == 0) { std::cout << "Failed to initialize SDL audio!" << '\n'; }
|
||||||
|
for (auto& ch : channels) { ch.state = ChannelState::FREE; }
|
||||||
|
std::ranges::fill(sound_volume, 0.5F);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void quit() {
|
||||||
|
if (outgoing_music.stream != nullptr) {
|
||||||
|
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||||
|
outgoing_music.stream = nullptr;
|
||||||
|
}
|
||||||
|
if (sdl_audio_device != 0) { SDL_CloseAudioDevice(sdl_audio_device); }
|
||||||
|
sdl_audio_device = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Music Functions ---
|
||||||
|
inline auto loadMusic(const Uint8* buffer, Uint32 length) -> Music* {
|
||||||
|
if ((buffer == nullptr) || length == 0) { return nullptr; }
|
||||||
|
|
||||||
|
// Allocem el Music primer per aprofitar el seu `std::vector<Uint8>`
|
||||||
|
// com a propietari del OGG comprimit. stb_vorbis guarda un punter
|
||||||
|
// persistent al buffer; com que ací no el resize'jem, el .data() és
|
||||||
|
// estable durant tot el cicle de vida del music.
|
||||||
|
auto* music = new Music();
|
||||||
|
music->ogg_data.assign(buffer, buffer + length);
|
||||||
|
|
||||||
|
int vorbis_error = 0;
|
||||||
|
music->vorbis = stb_vorbis_open_memory(music->ogg_data.data(),
|
||||||
|
static_cast<int>(length),
|
||||||
|
&vorbis_error,
|
||||||
|
nullptr);
|
||||||
|
if (music->vorbis == nullptr) {
|
||||||
|
std::cout << "loadMusic: stb_vorbis_open_memory failed (error " << vorbis_error << ")" << '\n';
|
||||||
|
delete music;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stb_vorbis_info INFO = stb_vorbis_get_info(music->vorbis);
|
||||||
|
music->spec.channels = INFO.channels;
|
||||||
|
music->spec.freq = static_cast<int>(INFO.sample_rate);
|
||||||
|
music->spec.format = SDL_AUDIO_S16;
|
||||||
|
music->state = MusicState::STOPPED;
|
||||||
|
|
||||||
|
return music;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overload amb filename — els callers l'usen per poder comparar la música
|
||||||
|
// en curs amb getMusicFilename() i no rearrancar-la si ja és la mateixa.
|
||||||
|
inline auto loadMusic(Uint8* buffer, Uint32 length, const char* filename) -> Music* {
|
||||||
|
Music* music = loadMusic(static_cast<const Uint8*>(buffer), length);
|
||||||
|
if ((music != nullptr) && (filename != nullptr)) { music->filename = filename; }
|
||||||
|
return music;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto loadMusic(const char* filename) -> Music* {
|
||||||
|
// Carreguem primer el arxiu en memòria i després el descomprimim.
|
||||||
|
FILE* f = std::fopen(filename, "rb");
|
||||||
|
if (f == nullptr) { return nullptr; }
|
||||||
|
std::fseek(f, 0, SEEK_END);
|
||||||
|
const long FSIZE = std::ftell(f);
|
||||||
|
std::fseek(f, 0, SEEK_SET);
|
||||||
|
if (FSIZE <= 0) {
|
||||||
|
std::fclose(f);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto* buffer = static_cast<Uint8*>(std::malloc(static_cast<size_t>(FSIZE) + 1));
|
||||||
|
if (buffer == nullptr) {
|
||||||
|
std::fclose(f);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (std::fread(buffer, FSIZE, 1, f) != 1) {
|
||||||
|
std::fclose(f);
|
||||||
|
std::free(buffer);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
std::fclose(f);
|
||||||
|
|
||||||
|
Music* music = loadMusic(static_cast<const Uint8*>(buffer), static_cast<Uint32>(FSIZE));
|
||||||
|
if (music != nullptr) { music->filename = filename; }
|
||||||
|
|
||||||
|
std::free(buffer);
|
||||||
|
|
||||||
|
return music;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void playMusic(Music* music, int loop = -1) {
|
||||||
|
if (!music_enabled || (music == nullptr) || (music->vorbis == nullptr)) { return; }
|
||||||
|
|
||||||
|
stopMusic();
|
||||||
|
|
||||||
|
current_music = music;
|
||||||
|
current_music->state = MusicState::PLAYING;
|
||||||
|
current_music->times = loop;
|
||||||
|
|
||||||
|
// Rebobinem l'stream de vorbis al principi. Cobreix tant play-per-primera-
|
||||||
|
// vegada com replays/canvis de track que tornen a la mateixa pista.
|
||||||
|
stb_vorbis_seek_start(current_music->vorbis);
|
||||||
|
|
||||||
|
current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &audio_spec);
|
||||||
|
if (current_music->stream == nullptr) {
|
||||||
|
std::cout << "Failed to create audio stream!" << '\n';
|
||||||
|
current_music->state = MusicState::STOPPED;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SDL_SetAudioStreamGain(current_music->stream, music_volume);
|
||||||
|
|
||||||
|
// Pre-cargem el buffer abans de bindejar per evitar un underrun inicial.
|
||||||
|
pumpMusic(current_music);
|
||||||
|
|
||||||
|
if (!SDL_BindAudioStream(sdl_audio_device, current_music->stream)) {
|
||||||
|
std::cout << "[ERROR] SDL_BindAudioStream failed!" << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto getMusicFilename(const Music* music = nullptr) -> const char* {
|
||||||
|
if (music == nullptr) { music = current_music; }
|
||||||
|
if ((music == nullptr) || music->filename.empty()) { return nullptr; }
|
||||||
|
return music->filename.c_str();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void pauseMusic() {
|
||||||
|
if (!music_enabled) { return; }
|
||||||
|
if ((current_music == nullptr) || current_music->state != MusicState::PLAYING) { return; }
|
||||||
|
|
||||||
|
current_music->state = MusicState::PAUSED;
|
||||||
|
SDL_UnbindAudioStream(current_music->stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void resumeMusic() {
|
||||||
|
if (!music_enabled) { return; }
|
||||||
|
if ((current_music == nullptr) || current_music->state != MusicState::PAUSED) { return; }
|
||||||
|
|
||||||
|
current_music->state = MusicState::PLAYING;
|
||||||
|
SDL_BindAudioStream(sdl_audio_device, current_music->stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void stopMusic() {
|
||||||
|
// Limpiar outgoing crossfade si existe
|
||||||
|
if (outgoing_music.stream != nullptr) {
|
||||||
|
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||||
|
outgoing_music.stream = nullptr;
|
||||||
|
outgoing_music.fade.active = false;
|
||||||
|
}
|
||||||
|
incoming_fade.active = false;
|
||||||
|
|
||||||
|
if ((current_music == nullptr) || current_music->state == MusicState::INVALID || current_music->state == MusicState::STOPPED) { return; }
|
||||||
|
|
||||||
|
current_music->state = MusicState::STOPPED;
|
||||||
|
if (current_music->stream != nullptr) {
|
||||||
|
SDL_DestroyAudioStream(current_music->stream);
|
||||||
|
current_music->stream = nullptr;
|
||||||
|
}
|
||||||
|
// Deixem el handle de vorbis viu — es tanca en deleteMusic.
|
||||||
|
// Rebobinem perquè un futur playMusic comence des del principi.
|
||||||
|
if (current_music->vorbis != nullptr) {
|
||||||
|
stb_vorbis_seek_start(current_music->vorbis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void fadeOutMusic(int milliseconds) {
|
||||||
|
if (!music_enabled) { return; }
|
||||||
|
if ((current_music == nullptr) || current_music->state != MusicState::PLAYING) { return; }
|
||||||
|
|
||||||
|
// Destruir outgoing anterior si existe
|
||||||
|
if (outgoing_music.stream != nullptr) {
|
||||||
|
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||||
|
outgoing_music.stream = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-omplim l'stream amb `milliseconds` de so: un cop robat, ja no
|
||||||
|
// tindrà accés al vorbis decoder i només podrà drenar el que tinga.
|
||||||
|
preFillOutgoing(current_music, milliseconds);
|
||||||
|
|
||||||
|
// Robar el stream del current_music al outgoing
|
||||||
|
outgoing_music.stream = current_music->stream;
|
||||||
|
outgoing_music.fade = {.active = true, .start_time = SDL_GetTicks(), .duration_ms = milliseconds, .initial_volume = music_volume};
|
||||||
|
|
||||||
|
// Dejar current_music sin stream (ya lo tiene outgoing)
|
||||||
|
current_music->stream = nullptr;
|
||||||
|
current_music->state = MusicState::STOPPED;
|
||||||
|
if (current_music->vorbis != nullptr) { stb_vorbis_seek_start(current_music->vorbis); }
|
||||||
|
incoming_fade.active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void crossfadeMusic(Music* music, int crossfade_ms, int loop) {
|
||||||
|
if (!music_enabled || (music == nullptr) || (music->vorbis == nullptr)) { return; }
|
||||||
|
|
||||||
|
// Destruir outgoing anterior si existe (crossfade durante crossfade)
|
||||||
|
if (outgoing_music.stream != nullptr) {
|
||||||
|
SDL_DestroyAudioStream(outgoing_music.stream);
|
||||||
|
outgoing_music.stream = nullptr;
|
||||||
|
outgoing_music.fade.active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Robar el stream de la musica actual al outgoing para el fade-out.
|
||||||
|
// Pre-omplim amb `crossfade_ms` de so perquè no es quede en silenci
|
||||||
|
// abans d'acabar el fade (l'stream robat ja no pot alimentar-se).
|
||||||
|
if ((current_music != nullptr) && current_music->state == MusicState::PLAYING && (current_music->stream != nullptr)) {
|
||||||
|
preFillOutgoing(current_music, crossfade_ms);
|
||||||
|
outgoing_music.stream = current_music->stream;
|
||||||
|
outgoing_music.fade = {.active = true, .start_time = SDL_GetTicks(), .duration_ms = crossfade_ms, .initial_volume = music_volume};
|
||||||
|
current_music->stream = nullptr;
|
||||||
|
current_music->state = MusicState::STOPPED;
|
||||||
|
if (current_music->vorbis != nullptr) { stb_vorbis_seek_start(current_music->vorbis); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iniciar la nueva pista con gain=0 (el fade-in la sube gradualmente)
|
||||||
|
current_music = music;
|
||||||
|
current_music->state = MusicState::PLAYING;
|
||||||
|
current_music->times = loop;
|
||||||
|
|
||||||
|
stb_vorbis_seek_start(current_music->vorbis);
|
||||||
|
current_music->stream = SDL_CreateAudioStream(¤t_music->spec, &audio_spec);
|
||||||
|
if (current_music->stream == nullptr) {
|
||||||
|
std::cout << "Failed to create audio stream for crossfade!" << '\n';
|
||||||
|
current_music->state = MusicState::STOPPED;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SDL_SetAudioStreamGain(current_music->stream, 0.0F);
|
||||||
|
pumpMusic(current_music); // pre-carrega abans de bindejar
|
||||||
|
SDL_BindAudioStream(sdl_audio_device, current_music->stream);
|
||||||
|
|
||||||
|
// Configurar fade-in
|
||||||
|
incoming_fade = {.active = true, .start_time = SDL_GetTicks(), .duration_ms = crossfade_ms, .initial_volume = 0.0F};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto getMusicState() -> MusicState {
|
||||||
|
if (!music_enabled) { return MusicState::DISABLED; }
|
||||||
|
if (current_music == nullptr) { return MusicState::INVALID; }
|
||||||
|
|
||||||
|
return current_music->state;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void deleteMusic(Music* music) {
|
||||||
|
if (music == nullptr) { return; }
|
||||||
|
if (current_music == music) {
|
||||||
|
stopMusic();
|
||||||
|
current_music = nullptr;
|
||||||
|
}
|
||||||
|
if (music->stream != nullptr) { SDL_DestroyAudioStream(music->stream); }
|
||||||
|
if (music->vorbis != nullptr) { stb_vorbis_close(music->vorbis); }
|
||||||
|
// ogg_data (std::vector) i filename (std::string) s'alliberen sols
|
||||||
|
// al destructor de Music.
|
||||||
|
delete music;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto setMusicVolume(float volume) -> float {
|
||||||
|
music_volume = SDL_clamp(volume, 0.0F, 1.0F);
|
||||||
|
if ((current_music != nullptr) && (current_music->stream != nullptr)) {
|
||||||
|
SDL_SetAudioStreamGain(current_music->stream, music_volume);
|
||||||
|
}
|
||||||
|
return music_volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void setMusicPosition(float /*value*/) {
|
||||||
|
// No implementat amb el backend de streaming.
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto getMusicPosition() -> float {
|
||||||
|
return 0.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void enableMusic(bool value) {
|
||||||
|
if (!value && (current_music != nullptr) && (current_music->state == MusicState::PLAYING)) { stopMusic(); }
|
||||||
|
music_enabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sound Functions ---
|
||||||
|
inline auto loadSound(std::uint8_t* buffer, std::uint32_t size) -> Sound* {
|
||||||
|
auto sound = std::make_unique<Sound>();
|
||||||
|
Uint8* raw = nullptr;
|
||||||
|
if (!SDL_LoadWAV_IO(SDL_IOFromMem(buffer, size), true, &sound->spec, &raw, &sound->length)) {
|
||||||
|
std::cout << "Failed to load WAV from memory: " << SDL_GetError() << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
|
||||||
|
return sound.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto loadSound(const char* filename) -> Sound* {
|
||||||
|
auto sound = std::make_unique<Sound>();
|
||||||
|
Uint8* raw = nullptr;
|
||||||
|
if (!SDL_LoadWAV(filename, &sound->spec, &raw, &sound->length)) {
|
||||||
|
std::cout << "Failed to load WAV file: " << SDL_GetError() << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
sound->buffer.reset(raw); // adopta el SDL_malloc'd buffer
|
||||||
|
return sound.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto playSound(Sound* sound, int loop = 0, int group = 0) -> int {
|
||||||
|
if (!sound_enabled || (sound == nullptr)) { return -1; }
|
||||||
|
|
||||||
|
int channel = 0;
|
||||||
|
while (channel < MAX_SIMULTANEOUS_CHANNELS && channels[channel].state != ChannelState::FREE) { channel++; }
|
||||||
|
if (channel == MAX_SIMULTANEOUS_CHANNELS) {
|
||||||
|
// No hi ha canal lliure, reemplacem el primer
|
||||||
|
channel = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return playSoundOnChannel(sound, channel, loop, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto playSoundOnChannel(Sound* sound, int channel, int loop, int group) -> int {
|
||||||
|
if (!sound_enabled || (sound == nullptr)) { return -1; }
|
||||||
|
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return -1; }
|
||||||
|
|
||||||
|
stopChannel(channel);
|
||||||
|
|
||||||
|
channels[channel].sound = sound;
|
||||||
|
channels[channel].times = loop;
|
||||||
|
channels[channel].pos = 0;
|
||||||
|
channels[channel].group = group;
|
||||||
|
channels[channel].state = ChannelState::PLAYING;
|
||||||
|
channels[channel].stream = SDL_CreateAudioStream(&channels[channel].sound->spec, &audio_spec);
|
||||||
|
|
||||||
|
if (channels[channel].stream == nullptr) {
|
||||||
|
std::cout << "Failed to create audio stream for sound!" << '\n';
|
||||||
|
channels[channel].state = ChannelState::FREE;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_PutAudioStreamData(channels[channel].stream, channels[channel].sound->buffer.get(), channels[channel].sound->length);
|
||||||
|
SDL_SetAudioStreamGain(channels[channel].stream, sound_volume[group]);
|
||||||
|
SDL_BindAudioStream(sdl_audio_device, channels[channel].stream);
|
||||||
|
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void deleteSound(Sound* sound) {
|
||||||
|
if (sound == nullptr) { return; }
|
||||||
|
for (int i = 0; i < MAX_SIMULTANEOUS_CHANNELS; i++) {
|
||||||
|
if (channels[i].sound == sound) { stopChannel(i); }
|
||||||
|
}
|
||||||
|
// buffer es destrueix automàticament via RAII (SdlFreeDeleter).
|
||||||
|
delete sound;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void pauseChannel(int channel) {
|
||||||
|
if (!sound_enabled) { return; }
|
||||||
|
|
||||||
|
if (channel == -1) {
|
||||||
|
for (auto& ch : channels) {
|
||||||
|
if (ch.state == ChannelState::PLAYING) {
|
||||||
|
ch.state = ChannelState::PAUSED;
|
||||||
|
SDL_UnbindAudioStream(ch.stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) {
|
||||||
|
if (channels[channel].state == ChannelState::PLAYING) {
|
||||||
|
channels[channel].state = ChannelState::PAUSED;
|
||||||
|
SDL_UnbindAudioStream(channels[channel].stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void resumeChannel(int channel) {
|
||||||
|
if (!sound_enabled) { return; }
|
||||||
|
|
||||||
|
if (channel == -1) {
|
||||||
|
for (auto& ch : channels) {
|
||||||
|
if (ch.state == ChannelState::PAUSED) {
|
||||||
|
ch.state = ChannelState::PLAYING;
|
||||||
|
SDL_BindAudioStream(sdl_audio_device, ch.stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) {
|
||||||
|
if (channels[channel].state == ChannelState::PAUSED) {
|
||||||
|
channels[channel].state = ChannelState::PLAYING;
|
||||||
|
SDL_BindAudioStream(sdl_audio_device, channels[channel].stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void stopChannel(int channel) {
|
||||||
|
if (channel == -1) {
|
||||||
|
for (auto& ch : channels) {
|
||||||
|
if (ch.state != ChannelState::FREE) {
|
||||||
|
if (ch.stream != nullptr) { SDL_DestroyAudioStream(ch.stream); }
|
||||||
|
ch.stream = nullptr;
|
||||||
|
ch.state = ChannelState::FREE;
|
||||||
|
ch.pos = 0;
|
||||||
|
ch.sound = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (channel >= 0 && channel < MAX_SIMULTANEOUS_CHANNELS) {
|
||||||
|
if (channels[channel].state != ChannelState::FREE) {
|
||||||
|
if (channels[channel].stream != nullptr) { SDL_DestroyAudioStream(channels[channel].stream); }
|
||||||
|
channels[channel].stream = nullptr;
|
||||||
|
channels[channel].state = ChannelState::FREE;
|
||||||
|
channels[channel].pos = 0;
|
||||||
|
channels[channel].sound = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto getChannelState(int channel) -> ChannelState {
|
||||||
|
if (!sound_enabled) { return ChannelState::DISABLED; }
|
||||||
|
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return ChannelState::INVALID; }
|
||||||
|
|
||||||
|
return channels[channel].state;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto setSoundVolume(float volume, int group = -1) -> float {
|
||||||
|
const float V = SDL_clamp(volume, 0.0F, 1.0F);
|
||||||
|
|
||||||
|
if (group == -1) {
|
||||||
|
std::ranges::fill(sound_volume, V);
|
||||||
|
} else if (group >= 0 && group < MAX_GROUPS) {
|
||||||
|
sound_volume[group] = V;
|
||||||
|
} else {
|
||||||
|
return V;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar volum als canals actius.
|
||||||
|
for (auto& ch : channels) {
|
||||||
|
if ((ch.state == ChannelState::PLAYING) || (ch.state == ChannelState::PAUSED)) {
|
||||||
|
if (group == -1 || ch.group == group) {
|
||||||
|
if (ch.stream != nullptr) {
|
||||||
|
SDL_SetAudioStreamGain(ch.stream, sound_volume[ch.group]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return V;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void enableSound(bool value) {
|
||||||
|
if (!value) {
|
||||||
|
stopChannel(-1);
|
||||||
|
}
|
||||||
|
sound_enabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline auto setVolume(float volume) -> float {
|
||||||
|
const float V = setMusicVolume(volume);
|
||||||
|
setSoundVolume(V, -1);
|
||||||
|
return V;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Ja
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
#include "core/input/global_inputs.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "core/input/input.h"
|
||||||
|
#include "core/locale/lang.h"
|
||||||
|
#include "core/rendering/notifications.hpp"
|
||||||
|
#include "core/rendering/screen.h"
|
||||||
|
#include "game/options.hpp"
|
||||||
|
#include "utils/defines.hpp"
|
||||||
|
#include "version.h"
|
||||||
|
|
||||||
|
namespace GlobalInputs {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Índexs de Lang per a les notificacions de hotkey
|
||||||
|
constexpr int LANG_ZOOM = 96;
|
||||||
|
constexpr int LANG_FULLSCREEN = 97;
|
||||||
|
constexpr int LANG_WINDOW = 98;
|
||||||
|
constexpr int LANG_SHADER = 99;
|
||||||
|
constexpr int LANG_PRESET = 100;
|
||||||
|
constexpr int LANG_EXIT_CONFIRM = 101;
|
||||||
|
|
||||||
|
// Patró de doble pulsació: la primera pulsació d'EXIT mostra una
|
||||||
|
// notificació en vermell i obre una finestra de confirmació; una
|
||||||
|
// segona pulsació dins la finestra activa `quit_requested`. La
|
||||||
|
// finestra coincideix amb la durada del missatge perquè usuari i
|
||||||
|
// sistema sempre estiguin sincronitzats.
|
||||||
|
Uint32 exit_window_until_ticks = 0;
|
||||||
|
bool quit_requested = false;
|
||||||
|
|
||||||
|
void notifyZoom() {
|
||||||
|
const std::string MSG = Lang::get()->getText(LANG_ZOOM) + " " + std::to_string(Options::window.zoom) + "x";
|
||||||
|
Notifications::show(MSG, Notifications::Palette::INFO, Notifications::STANDARD_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifyFullscreen() {
|
||||||
|
const int IDX = Options::video.fullscreen ? LANG_FULLSCREEN : LANG_WINDOW;
|
||||||
|
Notifications::show(Lang::get()->getText(IDX), Notifications::Palette::INFO, Notifications::STANDARD_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifyShaderEnabled() {
|
||||||
|
const std::string STATE = Screen::isShaderEnabled() ? "ON" : "OFF";
|
||||||
|
const std::string MSG = Lang::get()->getText(LANG_SHADER) + " " + STATE;
|
||||||
|
Notifications::show(MSG, Notifications::Palette::TOGGLE, Notifications::STANDARD_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifyShaderType() {
|
||||||
|
const bool IS_CRTPI = Options::video.shader.current_shader == Rendering::ShaderType::CRTPI;
|
||||||
|
const std::string MSG = Lang::get()->getText(LANG_SHADER) + " " + (IS_CRTPI ? "CRTPI" : "POSTFX");
|
||||||
|
Notifications::show(MSG, Notifications::Palette::CHOICE, Notifications::STANDARD_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifyPreset() {
|
||||||
|
const std::string MSG = Lang::get()->getText(LANG_PRESET) + " " + Screen::get()->getCurrentPresetName();
|
||||||
|
Notifications::show(MSG, Notifications::Palette::SUCCESS, Notifications::STANDARD_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifyVersion() {
|
||||||
|
// Format: "<APP_NAME> v<VERSION> (<GIT_HASH>)"
|
||||||
|
const std::string MSG = std::string(Version::APP_NAME) + " v" + Texts::VERSION + " (" + Version::GIT_HASH + ")";
|
||||||
|
Notifications::show(MSG, Notifications::Palette::TOGGLE, Notifications::LONG_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifyVSync() {
|
||||||
|
const std::string STATE = Options::video.vsync ? "ON" : "OFF";
|
||||||
|
const std::string MSG = std::string("VSync ") + STATE;
|
||||||
|
Notifications::show(MSG, Notifications::Palette::TOGGLE, Notifications::STANDARD_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void notifyPresentationMode() {
|
||||||
|
const std::string MSG = std::string("Mode ") + Screen::getPresentationModeName();
|
||||||
|
Notifications::show(MSG, Notifications::Palette::CHOICE, Notifications::STANDARD_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onExit() {
|
||||||
|
const Uint32 NOW = SDL_GetTicks();
|
||||||
|
if (NOW < exit_window_until_ticks) {
|
||||||
|
quit_requested = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
exit_window_until_ticks = NOW + Notifications::CONFIRM_MS;
|
||||||
|
Notifications::show(Lang::get()->getText(LANG_EXIT_CONFIRM), Notifications::Palette::DANGER, Notifications::CONFIRM_MS);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto handle() -> bool {
|
||||||
|
if (Screen::get() == nullptr || Input::get() == nullptr) { return false; }
|
||||||
|
|
||||||
|
if (Input::get()->checkInput(Input::Action::EXIT, Input::Repeat::OFF)) {
|
||||||
|
onExit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Input::get()->checkInput(Input::Action::WINDOW_FULLSCREEN, Input::Repeat::OFF)) {
|
||||||
|
Screen::get()->toggleVideoMode();
|
||||||
|
notifyFullscreen();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Input::get()->checkInput(Input::Action::WINDOW_DEC_ZOOM, Input::Repeat::OFF)) {
|
||||||
|
if (Screen::get()->decWindowZoom()) {
|
||||||
|
notifyZoom();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Input::get()->checkInput(Input::Action::WINDOW_INC_ZOOM, Input::Repeat::OFF)) {
|
||||||
|
if (Screen::get()->incWindowZoom()) {
|
||||||
|
notifyZoom();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Input::get()->checkInput(Input::Action::TOGGLE_SHADER, Input::Repeat::OFF)) {
|
||||||
|
Screen::get()->toggleShaderEnabled();
|
||||||
|
notifyShaderEnabled();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Input::get()->checkInput(Input::Action::SHOW_VERSION, Input::Repeat::OFF)) {
|
||||||
|
notifyVersion();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Input::get()->checkInput(Input::Action::TOGGLE_VSYNC, Input::Repeat::OFF)) {
|
||||||
|
Screen::get()->toggleVSync();
|
||||||
|
notifyVSync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Input::get()->checkInput(Input::Action::NEXT_PRESENTATION_MODE, Input::Repeat::OFF)) {
|
||||||
|
Screen::get()->nextPresentationMode();
|
||||||
|
notifyPresentationMode();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Input::get()->checkInput(Input::Action::TOGGLE_FPS, Input::Repeat::OFF)) {
|
||||||
|
Screen::get()->toggleFps();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// F5/F6 només actuen quan el post-procesado està actiu.
|
||||||
|
if (Screen::isShaderEnabled()) {
|
||||||
|
if (Input::get()->checkInput(Input::Action::TOGGLE_SHADER_TYPE, Input::Repeat::OFF)) {
|
||||||
|
Screen::get()->toggleActiveShader();
|
||||||
|
notifyShaderType();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Input::get()->checkInput(Input::Action::NEXT_SHADER_PRESET, Input::Repeat::OFF)) {
|
||||||
|
if (Screen::get()->nextPreset()) {
|
||||||
|
notifyPreset();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto wantsQuit() -> bool {
|
||||||
|
return quit_requested;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace GlobalInputs
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace GlobalInputs {
|
||||||
|
// Gestiona els atalls globals disponibles en qualsevol escena: zoom de
|
||||||
|
// finestra (F1/F2), fullscreen (F3), toggle shader (F4), tipus de shader
|
||||||
|
// POSTFX↔CRTPI (F5), següent preset (F6) i la confirmació d'eixida amb
|
||||||
|
// ESC (Action::EXIT) en dues pulsacions. Cada hotkey emet una
|
||||||
|
// notificació localitzada. Retorna true si ha consumit alguna tecla (per
|
||||||
|
// si la capa cridant vol suprimir-la del processament específic de
|
||||||
|
// l'escena).
|
||||||
|
auto handle() -> bool;
|
||||||
|
|
||||||
|
// True si la doble pulsació d'ESC s'ha confirmat. Director consulta açò
|
||||||
|
// a iterate() per a posar `section_->name = SECTION_PROG_QUIT`.
|
||||||
|
[[nodiscard]] auto wantsQuit() -> bool;
|
||||||
|
} // namespace GlobalInputs
|
||||||
@@ -0,0 +1,410 @@
|
|||||||
|
#include "core/input/input.h"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm> // for ranges::any_of
|
||||||
|
#include <iostream> // for basic_ostream, operator<<, cout, basi...
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instancia única
|
||||||
|
Input *Input::instance = nullptr;
|
||||||
|
|
||||||
|
// Singleton API
|
||||||
|
void Input::init(const std::string &game_controller_db_path) {
|
||||||
|
Input::instance = new Input(game_controller_db_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Input::destroy() {
|
||||||
|
delete Input::instance;
|
||||||
|
Input::instance = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Input::get() -> Input * {
|
||||||
|
return Input::instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Input::Input(std::string file)
|
||||||
|
: db_path_(std::move(file)) {
|
||||||
|
// Inicializa las variables
|
||||||
|
KeyBindings kb;
|
||||||
|
kb.scancode = 0;
|
||||||
|
kb.active = false;
|
||||||
|
key_bindings_.resize(static_cast<std::size_t>(Action::NUMBER_OF_INPUTS), kb);
|
||||||
|
|
||||||
|
GameControllerBindings gcb;
|
||||||
|
gcb.button = SDL_GAMEPAD_BUTTON_INVALID;
|
||||||
|
gcb.active = false;
|
||||||
|
game_controller_bindings_.resize(static_cast<std::size_t>(Action::NUMBER_OF_INPUTS), gcb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
Input::~Input() {
|
||||||
|
for (auto *pad : connected_controllers_) {
|
||||||
|
if (pad != nullptr) {
|
||||||
|
SDL_CloseGamepad(pad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connected_controllers_.clear();
|
||||||
|
connected_controller_ids_.clear();
|
||||||
|
controller_names_.clear();
|
||||||
|
num_gamepads_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualiza el estado del objeto
|
||||||
|
void Input::update() {
|
||||||
|
if (disabled_until_ == Disable::KEY_PRESSED && !checkAnyInput()) {
|
||||||
|
enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asigna inputs a teclas
|
||||||
|
void Input::bindKey(Action input, SDL_Scancode code) {
|
||||||
|
key_bindings_[static_cast<std::size_t>(input)].scancode = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asigna inputs a botones del mando
|
||||||
|
void Input::bindGameControllerButton(Action input, SDL_GamepadButton button) {
|
||||||
|
game_controller_bindings_[static_cast<std::size_t>(input)].button = button;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si un input esta activo
|
||||||
|
auto Input::checkInput(Action input, Repeat repeat, Device device, int index) -> bool {
|
||||||
|
if (!enabled_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device == Device::ANY) {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success_keyboard = false;
|
||||||
|
if (device == Device::KEYBOARD || device == Device::ANY) {
|
||||||
|
success_keyboard = checkKeyboardInput(input, repeat);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success_game_controller = false;
|
||||||
|
if ((device == Device::GAMECONTROLLER || device == Device::ANY) && gameControllerFound() && index >= 0 && index < (int)connected_controllers_.size()) {
|
||||||
|
success_game_controller = checkGameControllerInput(input, repeat, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success_keyboard || success_game_controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper de checkInput: comprueba el estado de una tecla
|
||||||
|
auto Input::checkKeyboardInput(Action input, Repeat repeat) -> bool {
|
||||||
|
const auto IDX = static_cast<std::size_t>(input);
|
||||||
|
const bool *key_states = SDL_GetKeyboardState(nullptr);
|
||||||
|
const bool IS_DOWN = key_states[key_bindings_[IDX].scancode];
|
||||||
|
|
||||||
|
if (repeat == Repeat::ON) {
|
||||||
|
return IS_DOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modo edge-trigger: éxito sólo en el frame en que la tecla pasa de up a down
|
||||||
|
const bool PRESS_EDGE = IS_DOWN && !key_bindings_[IDX].active;
|
||||||
|
key_bindings_[IDX].active = IS_DOWN;
|
||||||
|
return PRESS_EDGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper de checkInput: comprueba el estado de un botón de mando
|
||||||
|
auto Input::checkGameControllerInput(Action input, Repeat repeat, int index) -> bool {
|
||||||
|
const auto IDX = static_cast<std::size_t>(input);
|
||||||
|
const bool IS_DOWN = SDL_GetGamepadButton(connected_controllers_[index], game_controller_bindings_[IDX].button);
|
||||||
|
|
||||||
|
if (repeat == Repeat::ON) {
|
||||||
|
return IS_DOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modo edge-trigger: éxito sólo en el frame en que el botón pasa de up a down
|
||||||
|
const bool PRESS_EDGE = IS_DOWN && !game_controller_bindings_[IDX].active;
|
||||||
|
game_controller_bindings_[IDX].active = IS_DOWN;
|
||||||
|
return PRESS_EDGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si hay almenos un input "humano" activo (moviment, ACCEPT/CANCEL,
|
||||||
|
// FIRE_*). Exclou les accions reservades a hotkeys globals (EXIT, PAUSE,
|
||||||
|
// WINDOW_*, *SHADER*) perque prémer F1-F12 o ESC no s'ha de comptar com
|
||||||
|
// "qualsevol tecla" — ningu vol saltar una intro per modificar el zoom.
|
||||||
|
auto Input::checkAnyInput(Device device, int index) -> bool {
|
||||||
|
if (device == Device::ANY) {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto is_skippable = [](Action a) {
|
||||||
|
switch (a) {
|
||||||
|
case Action::UP:
|
||||||
|
case Action::DOWN:
|
||||||
|
case Action::LEFT:
|
||||||
|
case Action::RIGHT:
|
||||||
|
case Action::ACCEPT:
|
||||||
|
case Action::CANCEL:
|
||||||
|
case Action::FIRE_LEFT:
|
||||||
|
case Action::FIRE_CENTER:
|
||||||
|
case Action::FIRE_RIGHT:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (device == Device::KEYBOARD || device == Device::ANY) {
|
||||||
|
const bool *key_states = SDL_GetKeyboardState(nullptr);
|
||||||
|
for (std::size_t i = 0; i < key_bindings_.size(); ++i) {
|
||||||
|
if (is_skippable(static_cast<Action>(i)) && key_states[key_bindings_[i].scancode]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameControllerFound() && index >= 0 && index < (int)connected_controllers_.size()) {
|
||||||
|
if (device == Device::GAMECONTROLLER || device == Device::ANY) {
|
||||||
|
for (std::size_t i = 0; i < game_controller_bindings_.size(); ++i) {
|
||||||
|
if (is_skippable(static_cast<Action>(i)) && SDL_GetGamepadButton(connected_controllers_[index], game_controller_bindings_[i].button)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construye el nombre visible de un mando.
|
||||||
|
// Recorta des del primer '(' o '[' (per a evitar coses tipus
|
||||||
|
// "Retroid Controller (vendor: 1001) ...") i talla a 25 caràcters.
|
||||||
|
auto Input::buildControllerName(SDL_Gamepad *pad, int pad_index) -> std::string {
|
||||||
|
(void)pad_index;
|
||||||
|
const char *pad_name = SDL_GetGamepadName(pad);
|
||||||
|
std::string name = (pad_name != nullptr) ? pad_name : "Unknown";
|
||||||
|
const auto POS = name.find_first_of("([");
|
||||||
|
if (POS != std::string::npos) {
|
||||||
|
name.erase(POS);
|
||||||
|
}
|
||||||
|
while (!name.empty() && name.back() == ' ') {
|
||||||
|
name.pop_back();
|
||||||
|
}
|
||||||
|
if (name.size() > 25) {
|
||||||
|
name.resize(25);
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Busca si hay un mando conectado. Cierra y limpia el estado previo para
|
||||||
|
// que la función sea idempotente si se invoca más de una vez.
|
||||||
|
auto Input::discoverGameController() -> bool {
|
||||||
|
resetGameControllerState();
|
||||||
|
ensureGamepadSubsystem();
|
||||||
|
|
||||||
|
int num_joysticks = 0;
|
||||||
|
SDL_JoystickID *joysticks = SDL_GetJoysticks(&num_joysticks);
|
||||||
|
if (joysticks == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int gamepad_count = 0;
|
||||||
|
for (int i = 0; i < num_joysticks; ++i) {
|
||||||
|
if (SDL_IsGamepad(joysticks[i])) {
|
||||||
|
gamepad_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verbose_) {
|
||||||
|
std::cout << "\nChecking for game controllers...\n";
|
||||||
|
std::cout << num_joysticks << " joysticks found, " << gamepad_count << " are gamepads\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
if (gamepad_count > 0) {
|
||||||
|
found = true;
|
||||||
|
int pad_index = 0;
|
||||||
|
for (int i = 0; i < num_joysticks; i++) {
|
||||||
|
if (!SDL_IsGamepad(joysticks[i])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (openGamepad(joysticks[i], pad_index)) {
|
||||||
|
pad_index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SDL_SetGamepadEventsEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_free(joysticks);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper de discoverGameController: cierra mandos previos y limpia vectores paralelos
|
||||||
|
void Input::resetGameControllerState() {
|
||||||
|
for (auto *pad : connected_controllers_) {
|
||||||
|
if (pad != nullptr) {
|
||||||
|
SDL_CloseGamepad(pad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connected_controllers_.clear();
|
||||||
|
connected_controller_ids_.clear();
|
||||||
|
controller_names_.clear();
|
||||||
|
num_gamepads_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper de discoverGameController: inicializa el subsystem de gamepad y carga el mapping
|
||||||
|
void Input::ensureGamepadSubsystem() {
|
||||||
|
if (SDL_WasInit(SDL_INIT_GAMEPAD) != SDL_INIT_GAMEPAD) {
|
||||||
|
SDL_InitSubSystem(SDL_INIT_GAMEPAD);
|
||||||
|
}
|
||||||
|
if (SDL_AddGamepadMappingsFromFile(db_path_.c_str()) < 0 && verbose_) {
|
||||||
|
std::cout << "Error, could not load " << db_path_.c_str() << " file: " << SDL_GetError() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper de discoverGameController: abre y registra un mando. Devuelve true si tuvo éxito.
|
||||||
|
auto Input::openGamepad(SDL_JoystickID joystick_id, int pad_index) -> bool {
|
||||||
|
installWebStandardMapping(joystick_id);
|
||||||
|
SDL_Gamepad *pad = SDL_OpenGamepad(joystick_id);
|
||||||
|
if (pad == nullptr) {
|
||||||
|
if (verbose_) {
|
||||||
|
std::cout << "SDL_GetError() = " << SDL_GetError() << '\n';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string NAME = buildControllerName(pad, pad_index);
|
||||||
|
connected_controllers_.push_back(pad);
|
||||||
|
connected_controller_ids_.push_back(joystick_id);
|
||||||
|
controller_names_.push_back(NAME);
|
||||||
|
num_gamepads_++;
|
||||||
|
if (verbose_) {
|
||||||
|
std::cout << NAME << '\n';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Procesa un evento SDL_EVENT_GAMEPAD_ADDED
|
||||||
|
auto Input::handleGamepadAdded(SDL_JoystickID jid, std::string &out_name) -> bool {
|
||||||
|
if (!SDL_IsGamepad(jid)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si el mando ya está registrado no hace nada (ej. evento retroactivo tras el scan inicial)
|
||||||
|
if (std::ranges::any_of(connected_controller_ids_, [jid](SDL_JoystickID existing) { return existing == jid; })) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
installWebStandardMapping(jid);
|
||||||
|
SDL_Gamepad *pad = SDL_OpenGamepad(jid);
|
||||||
|
if (pad == nullptr) {
|
||||||
|
if (verbose_) {
|
||||||
|
std::cout << "Failed to open gamepad " << jid << ": " << SDL_GetError() << '\n';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int PAD_INDEX = (int)connected_controllers_.size();
|
||||||
|
const std::string NAME = buildControllerName(pad, PAD_INDEX);
|
||||||
|
connected_controllers_.push_back(pad);
|
||||||
|
connected_controller_ids_.push_back(jid);
|
||||||
|
controller_names_.push_back(NAME);
|
||||||
|
num_gamepads_++;
|
||||||
|
|
||||||
|
if (verbose_) {
|
||||||
|
std::cout << "Gamepad connected: " << NAME << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
out_name = NAME;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Procesa un evento SDL_EVENT_GAMEPAD_REMOVED
|
||||||
|
auto Input::handleGamepadRemoved(SDL_JoystickID jid, std::string &out_name) -> bool {
|
||||||
|
for (size_t i = 0; i < connected_controller_ids_.size(); ++i) {
|
||||||
|
if (connected_controller_ids_[i] != jid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_name = controller_names_[i];
|
||||||
|
if (connected_controllers_[i] != nullptr) {
|
||||||
|
SDL_CloseGamepad(connected_controllers_[i]);
|
||||||
|
}
|
||||||
|
connected_controllers_.erase(connected_controllers_.begin() + i);
|
||||||
|
connected_controller_ids_.erase(connected_controller_ids_.begin() + i);
|
||||||
|
controller_names_.erase(controller_names_.begin() + i);
|
||||||
|
num_gamepads_--;
|
||||||
|
num_gamepads_ = std::max(num_gamepads_, 0);
|
||||||
|
|
||||||
|
if (verbose_) {
|
||||||
|
std::cout << "Gamepad disconnected: " << out_name << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si hay algun mando conectado
|
||||||
|
auto Input::gameControllerFound() const -> bool {
|
||||||
|
return num_gamepads_ > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obten el nombre de un mando de juego
|
||||||
|
auto Input::getControllerName(int index) -> std::string {
|
||||||
|
if (num_gamepads_ > 0) {
|
||||||
|
return controller_names_[index];
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obten el numero de mandos conectados
|
||||||
|
auto Input::getNumControllers() const -> int {
|
||||||
|
return num_gamepads_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece si ha de mostrar mensajes
|
||||||
|
void Input::setVerbose(bool value) {
|
||||||
|
verbose_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deshabilita las entradas durante un periodo de tiempo
|
||||||
|
void Input::disableUntil(Disable value) {
|
||||||
|
disabled_until_ = value;
|
||||||
|
enabled_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hablita las entradas
|
||||||
|
void Input::enable() {
|
||||||
|
enabled_ = true;
|
||||||
|
disabled_until_ = Disable::NOT_DISABLED;
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint> // for uint8_t
|
||||||
|
#include <string> // for string, basic_string
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
class Input {
|
||||||
|
public:
|
||||||
|
enum class Repeat : std::uint8_t {
|
||||||
|
OFF,
|
||||||
|
ON
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class Device : std::uint8_t {
|
||||||
|
KEYBOARD,
|
||||||
|
GAMECONTROLLER,
|
||||||
|
ANY
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class Disable : std::uint8_t {
|
||||||
|
NOT_DISABLED,
|
||||||
|
FOREVER,
|
||||||
|
KEY_PRESSED
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class Action : std::uint8_t {
|
||||||
|
// Inputs obligatorios
|
||||||
|
INVALID,
|
||||||
|
UP,
|
||||||
|
DOWN,
|
||||||
|
LEFT,
|
||||||
|
RIGHT,
|
||||||
|
PAUSE,
|
||||||
|
EXIT,
|
||||||
|
ACCEPT,
|
||||||
|
CANCEL,
|
||||||
|
|
||||||
|
// Inputs personalizados
|
||||||
|
FIRE_LEFT,
|
||||||
|
FIRE_CENTER,
|
||||||
|
FIRE_RIGHT,
|
||||||
|
WINDOW_FULLSCREEN,
|
||||||
|
WINDOW_INC_ZOOM,
|
||||||
|
WINDOW_DEC_ZOOM,
|
||||||
|
|
||||||
|
// GPU / shaders (hotkeys provisionales hasta que haya menú de opciones)
|
||||||
|
NEXT_SHADER_PRESET,
|
||||||
|
TOGGLE_SHADER,
|
||||||
|
TOGGLE_SHADER_TYPE,
|
||||||
|
|
||||||
|
// Diagnostic / video toggles
|
||||||
|
SHOW_VERSION,
|
||||||
|
TOGGLE_VSYNC,
|
||||||
|
NEXT_PRESENTATION_MODE,
|
||||||
|
TOGGLE_FPS,
|
||||||
|
|
||||||
|
// Centinela final (usar para sizing)
|
||||||
|
NUMBER_OF_INPUTS
|
||||||
|
};
|
||||||
|
|
||||||
|
// Singleton API
|
||||||
|
static void init(const std::string &game_controller_db_path); // Crea la instancia
|
||||||
|
static void destroy(); // Libera la instancia
|
||||||
|
static auto get() -> Input *; // Obtiene el puntero a la instancia
|
||||||
|
|
||||||
|
~Input(); // Destructor
|
||||||
|
|
||||||
|
void update(); // Actualiza el estado del objeto
|
||||||
|
void bindKey(Action input, SDL_Scancode code); // Asigna inputs a teclas
|
||||||
|
void bindGameControllerButton(Action input, SDL_GamepadButton button); // Asigna inputs a botones del mando
|
||||||
|
|
||||||
|
auto checkInput(Action input, Repeat repeat = Repeat::ON, Device device = Device::ANY, int index = 0) -> bool; // Comprueba si un input esta activo
|
||||||
|
auto checkAnyInput(Device device = Device::ANY, int index = 0) -> bool; // Comprueba si hay almenos un input activo
|
||||||
|
|
||||||
|
auto discoverGameController() -> bool; // Busca si hay un mando conectado
|
||||||
|
|
||||||
|
// Procesa un evento SDL_EVENT_GAMEPAD_ADDED. Devuelve true si el mando se ha añadido
|
||||||
|
// (no estaba ya registrado) y escribe el nombre visible en outName.
|
||||||
|
auto handleGamepadAdded(SDL_JoystickID jid, std::string &out_name) -> bool;
|
||||||
|
|
||||||
|
// Procesa un evento SDL_EVENT_GAMEPAD_REMOVED. Devuelve true si se ha encontrado y
|
||||||
|
// eliminado, y escribe el nombre visible en outName.
|
||||||
|
auto handleGamepadRemoved(SDL_JoystickID jid, std::string &out_name) -> bool;
|
||||||
|
|
||||||
|
[[nodiscard]] auto gameControllerFound() const -> bool; // Comprueba si hay algun mando conectado
|
||||||
|
[[nodiscard]] auto getNumControllers() const -> int; // Obten el numero de mandos conectados
|
||||||
|
auto getControllerName(int index) -> std::string; // Obten el nombre de un mando de juego
|
||||||
|
|
||||||
|
void setVerbose(bool value); // Establece si ha de mostrar mensajes
|
||||||
|
void disableUntil(Disable value); // Deshabilita las entradas durante un periodo de tiempo
|
||||||
|
void enable(); // Hablita las entradas
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct KeyBindings {
|
||||||
|
Uint8 scancode; // Scancode asociado
|
||||||
|
bool active; // Indica si está activo
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GameControllerBindings {
|
||||||
|
SDL_GamepadButton button; // GameControllerButton asociado
|
||||||
|
bool active; // Indica si está activo
|
||||||
|
};
|
||||||
|
|
||||||
|
// Objetos y punteros
|
||||||
|
std::vector<SDL_Gamepad *> connected_controllers_; // Vector con todos los mandos conectados
|
||||||
|
std::vector<SDL_JoystickID> connected_controller_ids_; // Instance IDs paralelos para mapear eventos
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
std::vector<KeyBindings> key_bindings_; // Vector con las teclas asociadas a los inputs predefinidos
|
||||||
|
std::vector<GameControllerBindings> game_controller_bindings_; // Vector con las teclas asociadas a los inputs predefinidos
|
||||||
|
std::vector<std::string> controller_names_; // Vector con los nombres de los mandos
|
||||||
|
int num_gamepads_{0}; // Numero de mandos conectados
|
||||||
|
std::string db_path_; // Ruta al archivo gamecontrollerdb.txt
|
||||||
|
bool verbose_{true}; // Indica si ha de mostrar mensajes
|
||||||
|
Disable disabled_until_{Disable::NOT_DISABLED}; // Tiempo que esta deshabilitado
|
||||||
|
bool enabled_{true}; // Indica si está habilitado
|
||||||
|
|
||||||
|
static Input *instance; // Instancia única
|
||||||
|
|
||||||
|
explicit Input(std::string file); // Constructor privado (usar Input::init)
|
||||||
|
|
||||||
|
// Construye el nombre visible de un mando (name truncado + sufijo #N)
|
||||||
|
static auto buildControllerName(SDL_Gamepad *pad, int pad_index) -> std::string;
|
||||||
|
|
||||||
|
// Helpers de checkInput
|
||||||
|
auto checkKeyboardInput(Action input, Repeat repeat) -> bool;
|
||||||
|
auto checkGameControllerInput(Action input, Repeat repeat, int index) -> bool;
|
||||||
|
|
||||||
|
// Helpers de discoverGameController
|
||||||
|
void resetGameControllerState();
|
||||||
|
void ensureGamepadSubsystem();
|
||||||
|
auto openGamepad(SDL_JoystickID joystick_id, int pad_index) -> bool;
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#include "core/input/mouse.hpp"
|
||||||
|
|
||||||
|
namespace Mouse {
|
||||||
|
Uint32 cursor_hide_time = 3000; // Tiempo en milisegundos para ocultar el cursor por inactividad
|
||||||
|
Uint32 last_mouse_move_time = 0; // Última vez que el ratón se movió
|
||||||
|
bool cursor_visible = true; // Estado del cursor
|
||||||
|
|
||||||
|
void handleEvent(const SDL_Event &event, bool fullscreen) {
|
||||||
|
if (event.type == SDL_EVENT_MOUSE_MOTION) {
|
||||||
|
last_mouse_move_time = SDL_GetTicks();
|
||||||
|
if (!cursor_visible && !fullscreen) {
|
||||||
|
SDL_ShowCursor();
|
||||||
|
cursor_visible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateCursorVisibility(bool fullscreen) {
|
||||||
|
// En pantalla completa el cursor siempre está oculto
|
||||||
|
if (fullscreen) {
|
||||||
|
if (cursor_visible) {
|
||||||
|
SDL_HideCursor();
|
||||||
|
cursor_visible = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// En modo ventana, lo oculta tras el periodo de inactividad
|
||||||
|
const Uint32 CURRENT_TIME = SDL_GetTicks();
|
||||||
|
if (cursor_visible && (CURRENT_TIME - last_mouse_move_time > cursor_hide_time)) {
|
||||||
|
SDL_HideCursor();
|
||||||
|
cursor_visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace Mouse
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
namespace Mouse {
|
||||||
|
extern Uint32 cursor_hide_time; // Tiempo en milisegundos para ocultar el cursor por inactividad
|
||||||
|
extern Uint32 last_mouse_move_time; // Última vez que el ratón se movió
|
||||||
|
extern bool cursor_visible; // Estado del cursor
|
||||||
|
|
||||||
|
// Procesa un evento de ratón. En pantalla completa ignora el movimiento
|
||||||
|
// para no volver a mostrar el cursor.
|
||||||
|
void handleEvent(const SDL_Event &event, bool fullscreen);
|
||||||
|
|
||||||
|
// Actualiza la visibilidad del cursor. En modo ventana lo oculta
|
||||||
|
// después de cursorHideTime ms sin movimiento. En pantalla completa
|
||||||
|
// lo mantiene siempre oculto.
|
||||||
|
void updateCursorVisibility(bool fullscreen);
|
||||||
|
} // namespace Mouse
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#include "core/locale/lang.h"
|
||||||
|
|
||||||
|
#include <fstream> // for basic_ifstream, basic_istream, ifstream
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include "core/resources/asset.h" // for Asset
|
||||||
|
#include "core/resources/resource_helper.h"
|
||||||
|
|
||||||
|
// Instancia única
|
||||||
|
Lang *Lang::instance = nullptr;
|
||||||
|
|
||||||
|
// Singleton API
|
||||||
|
void Lang::init() {
|
||||||
|
Lang::instance = new Lang();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lang::destroy() {
|
||||||
|
delete Lang::instance;
|
||||||
|
Lang::instance = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Lang::get() -> Lang * {
|
||||||
|
return Lang::instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Lang::Lang() = default;
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
Lang::~Lang() = default;
|
||||||
|
|
||||||
|
// Inicializa los textos del juego en el idioma seleccionado
|
||||||
|
auto Lang::setLang(Code lang) -> bool {
|
||||||
|
std::string file;
|
||||||
|
|
||||||
|
switch (lang) {
|
||||||
|
case Code::ES_ES:
|
||||||
|
file = Asset::get()->get("es_ES.txt");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Code::EN_UK:
|
||||||
|
file = Asset::get()->get("en_UK.txt");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Code::BA_BA:
|
||||||
|
file = Asset::get()->get("ba_BA.txt");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
file = Asset::get()->get("en_UK.txt");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto &text_string : text_strings_) {
|
||||||
|
text_string = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lee el fichero via ResourceHelper (pack o filesystem)
|
||||||
|
auto bytes = ResourceHelper::loadFile(file);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
|
||||||
|
std::stringstream ss(content);
|
||||||
|
std::string line;
|
||||||
|
int index = 0;
|
||||||
|
while (std::getline(ss, line)) {
|
||||||
|
// Normaliza CRLF: en Windows els fitxers es llegeixen en binari i
|
||||||
|
// getline només talla pel \n, deixant un \r residual que faria que les
|
||||||
|
// línies en blanc no semblen buides (i sobreescriguen més enllà de
|
||||||
|
// mTextStrings, corrompent el heap).
|
||||||
|
if (!line.empty() && line.back() == '\r') {
|
||||||
|
line.pop_back();
|
||||||
|
}
|
||||||
|
// Almacena solo las lineas que no empiezan por # o no esten vacias
|
||||||
|
const bool NOT_COMMENT = line.substr(0, 1) != "#";
|
||||||
|
const bool NOT_EMPTY = !line.empty();
|
||||||
|
if (NOT_COMMENT && NOT_EMPTY) {
|
||||||
|
if (index >= MAX_TEXT_STRINGS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
text_strings_[index] = line;
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene la cadena de texto del indice
|
||||||
|
auto Lang::getText(int index) -> std::string {
|
||||||
|
return text_strings_[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devuelve el siguiente idioma del ciclo (wrap-around)
|
||||||
|
auto Lang::nextLanguage(Code c) -> Code {
|
||||||
|
const int NEXT = (static_cast<int>(c) + 1) % MAX_LANGUAGES;
|
||||||
|
return static_cast<Code>(NEXT);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint> // for uint8_t
|
||||||
|
#include <string> // for string, basic_string
|
||||||
|
|
||||||
|
// Clase Lang
|
||||||
|
class Lang {
|
||||||
|
public:
|
||||||
|
// Códigos de idioma (basados en la convención IETF de los ficheros de locale)
|
||||||
|
enum class Code : std::uint8_t {
|
||||||
|
ES_ES = 0,
|
||||||
|
BA_BA = 1,
|
||||||
|
EN_UK = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr int MAX_LANGUAGES = 3; // Número total de idiomas disponibles
|
||||||
|
|
||||||
|
// Singleton API
|
||||||
|
static void init(); // Crea la instancia
|
||||||
|
static void destroy(); // Libera la instancia
|
||||||
|
static auto get() -> Lang *; // Obtiene el puntero a la instancia
|
||||||
|
|
||||||
|
~Lang(); // Destructor
|
||||||
|
|
||||||
|
auto setLang(Code lang) -> bool; // Inicializa los textos del juego en el idioma seleccionado
|
||||||
|
auto getText(int index) -> std::string; // Obtiene la cadena de texto del indice
|
||||||
|
|
||||||
|
static auto nextLanguage(Code c) -> Code; // Devuelve el siguiente idioma del ciclo
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr int MAX_TEXT_STRINGS = 110;
|
||||||
|
|
||||||
|
std::string text_strings_[MAX_TEXT_STRINGS]; // Vector con los textos
|
||||||
|
|
||||||
|
static Lang *instance; // Instancia única
|
||||||
|
|
||||||
|
Lang(); // Constructor privado (usar Lang::init)
|
||||||
|
};
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
#include "core/rendering/animatedsprite.h"
|
||||||
|
|
||||||
|
#include <iostream> // for cout
|
||||||
|
#include <sstream> // for basic_stringstream
|
||||||
|
|
||||||
|
#include "core/rendering/texture.h" // for Texture
|
||||||
|
#include "core/resources/resource_helper.h" // for loadFile (pack + filesystem fallback)
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Normalitza CRLF: fitxers .ani amb terminadors de Windows fan que
|
||||||
|
// line == "[animation]" no faci match i el parser entri en bucle
|
||||||
|
// infinit / no carregui cap animació.
|
||||||
|
void stripCr(std::string &s) {
|
||||||
|
if (!s.empty() && s.back() == '\r') {
|
||||||
|
s.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseFramesList(const std::string &value, Animation &buffer, int frame_width, int frame_height, int frames_per_row, int max_tiles) {
|
||||||
|
std::stringstream ss(value);
|
||||||
|
std::string tmp;
|
||||||
|
SDL_Rect rect = {0, 0, frame_width, frame_height};
|
||||||
|
while (getline(ss, tmp, ',')) {
|
||||||
|
const int NUM_TILE = std::stoi(tmp) > max_tiles ? 0 : std::stoi(tmp);
|
||||||
|
rect.x = (NUM_TILE % frames_per_row) * frame_width;
|
||||||
|
rect.y = (NUM_TILE / frames_per_row) * frame_height;
|
||||||
|
buffer.frames.push_back(rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseAnimationField(const std::string &line, int pos, Animation &buffer, int frame_width, int frame_height, int frames_per_row, int max_tiles, const std::string &filename) {
|
||||||
|
const std::string KEY = line.substr(0, pos);
|
||||||
|
const std::string VALUE = line.substr(pos + 1, line.length());
|
||||||
|
if (KEY == "name") {
|
||||||
|
buffer.name = VALUE;
|
||||||
|
} else if (KEY == "speed") {
|
||||||
|
buffer.speed = std::stoi(VALUE);
|
||||||
|
// Time-based: el valor del .ani s'expressa en "ticks per frame
|
||||||
|
// d'animació" (assumint 60 Hz). El camp `speed` (int) es manté per al
|
||||||
|
// fallback frame-based; el nou `step_duration_s` (float) és el que
|
||||||
|
// gasta animate(dt).
|
||||||
|
buffer.step_duration_s = static_cast<float>(buffer.speed) / 60.0F;
|
||||||
|
} else if (KEY == "loop") {
|
||||||
|
buffer.loop = std::stoi(VALUE);
|
||||||
|
} else if (KEY == "frames") {
|
||||||
|
parseFramesList(VALUE, buffer, frame_width, frame_height, frames_per_row, max_tiles);
|
||||||
|
} else {
|
||||||
|
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << KEY.c_str() << "\"" << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parseAnimationBlock(std::istream &file, int frame_width, int frame_height, int frames_per_row, int max_tiles, const std::string &filename) -> Animation {
|
||||||
|
Animation buffer;
|
||||||
|
buffer.speed = 0;
|
||||||
|
buffer.step_duration_s = 0.0F;
|
||||||
|
buffer.loop = -1;
|
||||||
|
buffer.counter = 0;
|
||||||
|
buffer.current_frame = 0;
|
||||||
|
buffer.completed = false;
|
||||||
|
buffer.time_accumulator_s = 0.0F;
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
do {
|
||||||
|
if (!std::getline(file, line)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
stripCr(line);
|
||||||
|
int pos = line.find('=');
|
||||||
|
if (pos != (int)std::string::npos) {
|
||||||
|
parseAnimationField(line, pos, buffer, frame_width, frame_height, frames_per_row, max_tiles, filename);
|
||||||
|
}
|
||||||
|
} while (line != "[/animation]");
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseGlobalField(const std::string &line, int pos, int &frames_per_row, int &frame_width, int &frame_height, int &max_tiles, const Texture *texture, const std::string &filename) {
|
||||||
|
const std::string KEY = line.substr(0, pos);
|
||||||
|
const std::string VALUE = line.substr(pos + 1, line.length());
|
||||||
|
if (KEY == "framesPerRow") {
|
||||||
|
frames_per_row = std::stoi(VALUE);
|
||||||
|
} else if (KEY == "frameWidth") {
|
||||||
|
frame_width = std::stoi(VALUE);
|
||||||
|
} else if (KEY == "frameHeight") {
|
||||||
|
frame_height = std::stoi(VALUE);
|
||||||
|
} else {
|
||||||
|
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << KEY.c_str() << "\"" << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frames_per_row == 0 && frame_width > 0) {
|
||||||
|
frames_per_row = texture->getWidth() / frame_width;
|
||||||
|
}
|
||||||
|
if (max_tiles == 0 && frame_width > 0 && frame_height > 0) {
|
||||||
|
const int W = texture->getWidth() / frame_width;
|
||||||
|
const int H = texture->getHeight() / frame_height;
|
||||||
|
max_tiles = W * H;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Parser compartido: lee un istream con el formato .ani
|
||||||
|
static auto parseAnimationStream(std::istream &file, Texture *texture, const std::string &filename, bool verbose) -> AnimatedSpriteData {
|
||||||
|
AnimatedSpriteData as;
|
||||||
|
as.texture = texture;
|
||||||
|
int frames_per_row = 0;
|
||||||
|
int frame_width = 0;
|
||||||
|
int frame_height = 0;
|
||||||
|
int max_tiles = 0;
|
||||||
|
std::string line;
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
std::cout << "Animation loaded: " << filename << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
while (std::getline(file, line)) {
|
||||||
|
stripCr(line);
|
||||||
|
if (line == "[animation]") {
|
||||||
|
as.animations.push_back(parseAnimationBlock(file, frame_width, frame_height, frames_per_row, max_tiles, filename));
|
||||||
|
} else {
|
||||||
|
int pos = line.find('=');
|
||||||
|
if (pos != (int)std::string::npos) {
|
||||||
|
parseGlobalField(line, pos, frames_per_row, frame_width, frame_height, max_tiles, texture, filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return as;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carga la animación desde un fichero (vía ResourceHelper: pack si està inicialitzat, filesystem si no)
|
||||||
|
auto loadAnimationFromFile(Texture *texture, const std::string &file_path, bool verbose) -> AnimatedSpriteData {
|
||||||
|
const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1);
|
||||||
|
auto bytes = ResourceHelper::loadFile(file_path);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
if (verbose) {
|
||||||
|
std::cout << "Warning: Unable to open " << FILE_NAME.c_str() << " file" << '\n';
|
||||||
|
}
|
||||||
|
AnimatedSpriteData as;
|
||||||
|
as.texture = texture;
|
||||||
|
return as;
|
||||||
|
}
|
||||||
|
return loadAnimationFromMemory(texture, bytes, FILE_NAME, verbose);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carga la animación desde bytes en memoria
|
||||||
|
auto loadAnimationFromMemory(Texture *texture, const std::vector<uint8_t> &bytes, const std::string &name_for_logs, bool verbose) -> AnimatedSpriteData {
|
||||||
|
if (bytes.empty()) {
|
||||||
|
AnimatedSpriteData as;
|
||||||
|
as.texture = texture;
|
||||||
|
return as;
|
||||||
|
}
|
||||||
|
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
|
||||||
|
std::stringstream ss(content);
|
||||||
|
return parseAnimationStream(ss, texture, name_for_logs, verbose);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
AnimatedSprite::AnimatedSprite(Texture *texture, SDL_Renderer *renderer, const std::string &file, const std::vector<std::string> *buffer)
|
||||||
|
: current_animation_(0) {
|
||||||
|
// Copia los punteros
|
||||||
|
setTexture(texture);
|
||||||
|
setRenderer(renderer);
|
||||||
|
|
||||||
|
// Carga las animaciones
|
||||||
|
if (!file.empty()) {
|
||||||
|
AnimatedSpriteData as = loadAnimationFromFile(texture, file);
|
||||||
|
|
||||||
|
// Copia los datos de las animaciones
|
||||||
|
animation_.insert(animation_.end(), as.animations.begin(), as.animations.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (buffer != nullptr) {
|
||||||
|
loadFromVector(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
AnimatedSprite::AnimatedSprite(SDL_Renderer *renderer, AnimatedSpriteData *data)
|
||||||
|
: current_animation_(0) {
|
||||||
|
// Copia los punteros
|
||||||
|
setTexture(data->texture);
|
||||||
|
setRenderer(renderer);
|
||||||
|
|
||||||
|
// Copia los datos de las animaciones
|
||||||
|
this->animation_.insert(this->animation_.end(), data->animations.begin(), data->animations.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
AnimatedSprite::~AnimatedSprite() {
|
||||||
|
for (auto &a : animation_) {
|
||||||
|
a.frames.clear();
|
||||||
|
}
|
||||||
|
animation_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el indice de la animación a partir del nombre
|
||||||
|
auto AnimatedSprite::getIndex(const std::string &name) -> int {
|
||||||
|
int index = -1;
|
||||||
|
|
||||||
|
for (const auto &a : animation_) {
|
||||||
|
index++;
|
||||||
|
if (a.name == name) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "** Warning: could not find \"" << name.c_str() << "\" animation" << '\n';
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avança l'acumulador i calcula el frame actual a partir de `step_duration_s`.
|
||||||
|
void AnimatedSprite::animate(float dt_s) {
|
||||||
|
Animation &anim = animation_[current_animation_];
|
||||||
|
if (!enabled_ || anim.step_duration_s <= 0.0F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
anim.time_accumulator_s += dt_s;
|
||||||
|
anim.current_frame = static_cast<int>(anim.time_accumulator_s / anim.step_duration_s);
|
||||||
|
|
||||||
|
if (anim.current_frame >= (int)anim.frames.size()) {
|
||||||
|
if (anim.loop == -1) {
|
||||||
|
anim.current_frame = anim.frames.size();
|
||||||
|
anim.completed = true;
|
||||||
|
} else {
|
||||||
|
anim.time_accumulator_s = 0.0F;
|
||||||
|
anim.current_frame = anim.loop;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSpriteClip(anim.frames[anim.current_frame]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el numero de frames de la animación actual
|
||||||
|
auto AnimatedSprite::getNumFrames() -> int {
|
||||||
|
return (int)animation_[current_animation_].frames.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el frame actual de la animación
|
||||||
|
void AnimatedSprite::setCurrentFrame(int num) {
|
||||||
|
// Descarta valores fuera de rango
|
||||||
|
if (num >= (int)animation_[current_animation_].frames.size()) {
|
||||||
|
num = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambia el valor de la variable
|
||||||
|
animation_[current_animation_].current_frame = num;
|
||||||
|
animation_[current_animation_].counter = 0;
|
||||||
|
|
||||||
|
// Escoge el frame correspondiente de la animación
|
||||||
|
setSpriteClip(animation_[current_animation_].frames[animation_[current_animation_].current_frame]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor del contador
|
||||||
|
void AnimatedSprite::setAnimationCounter(const std::string &name, int num) {
|
||||||
|
animation_[getIndex(name)].counter = num;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la velocidad de una animación
|
||||||
|
void AnimatedSprite::setAnimationSpeed(const std::string &name, int speed) {
|
||||||
|
animation_[getIndex(name)].counter = speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la velocidad de una animación
|
||||||
|
void AnimatedSprite::setAnimationSpeed(int index, int speed) {
|
||||||
|
animation_[index].counter = speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece si la animación se reproduce en bucle
|
||||||
|
void AnimatedSprite::setAnimationLoop(const std::string &name, int loop) {
|
||||||
|
animation_[getIndex(name)].loop = loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece si la animación se reproduce en bucle
|
||||||
|
void AnimatedSprite::setAnimationLoop(int index, int loop) {
|
||||||
|
animation_[index].loop = loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void AnimatedSprite::setAnimationCompleted(const std::string &name, bool value) {
|
||||||
|
animation_[getIndex(name)].completed = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OLD - Establece el valor de la variable
|
||||||
|
void AnimatedSprite::setAnimationCompleted(int index, bool value) {
|
||||||
|
animation_[index].completed = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si ha terminado la animación
|
||||||
|
auto AnimatedSprite::animationIsCompleted() -> bool {
|
||||||
|
return animation_[current_animation_].completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devuelve el rectangulo de una animación y frame concreto
|
||||||
|
auto AnimatedSprite::getAnimationClip(const std::string &name, Uint8 index) -> SDL_Rect {
|
||||||
|
return animation_[getIndex(name)].frames[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devuelve el rectangulo de una animación y frame concreto
|
||||||
|
auto AnimatedSprite::getAnimationClip(int index_a, Uint8 index_f) -> SDL_Rect {
|
||||||
|
return animation_[index_a].frames[index_f];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carga la animación desde un vector (reutiliza parseAnimationStream via stringstream)
|
||||||
|
auto AnimatedSprite::loadFromVector(const std::vector<std::string> *source) -> bool {
|
||||||
|
std::stringstream ss;
|
||||||
|
for (const auto &line : *source) {
|
||||||
|
ss << line << '\n';
|
||||||
|
}
|
||||||
|
AnimatedSpriteData as = parseAnimationStream(ss, texture_, "", false);
|
||||||
|
animation_.insert(animation_.end(), as.animations.begin(), as.animations.end());
|
||||||
|
|
||||||
|
// El primer frame lleva frame_width/frame_height en .w/.h — los usamos como rect por defecto
|
||||||
|
if (!as.animations.empty() && !as.animations.front().frames.empty()) {
|
||||||
|
const auto &first = as.animations.front().frames.front();
|
||||||
|
setRect({0, 0, first.w, first.h});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la animacion actual
|
||||||
|
void AnimatedSprite::setCurrentAnimation(const std::string &name) {
|
||||||
|
const int NEW_ANIMATION = getIndex(name);
|
||||||
|
if (current_animation_ != NEW_ANIMATION) {
|
||||||
|
current_animation_ = NEW_ANIMATION;
|
||||||
|
animation_[current_animation_].current_frame = 0;
|
||||||
|
animation_[current_animation_].counter = 0;
|
||||||
|
animation_[current_animation_].completed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la animacion actual
|
||||||
|
void AnimatedSprite::setCurrentAnimation(int index) {
|
||||||
|
const int NEW_ANIMATION = index;
|
||||||
|
if (current_animation_ != NEW_ANIMATION) {
|
||||||
|
current_animation_ = NEW_ANIMATION;
|
||||||
|
animation_[current_animation_].current_frame = 0;
|
||||||
|
animation_[current_animation_].counter = 0;
|
||||||
|
animation_[current_animation_].completed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// animate(dt) + MovingSprite::update(dt) (move + rotació)
|
||||||
|
void AnimatedSprite::update(float dt_s) {
|
||||||
|
animate(dt_s);
|
||||||
|
MovingSprite::update(dt_s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el rectangulo para un frame de una animación
|
||||||
|
void AnimatedSprite::setAnimationFrames(Uint8 index_animation, Uint8 /*index_frame*/, int x, int y, int w, int h) {
|
||||||
|
animation_[index_animation].frames.push_back({x, y, w, h});
|
||||||
|
}
|
||||||
|
|
||||||
|
// OLD - Establece el contador para todas las animaciones
|
||||||
|
void AnimatedSprite::setAnimationCounter(int value) {
|
||||||
|
for (auto &a : animation_) {
|
||||||
|
a.counter = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reinicia la animación
|
||||||
|
void AnimatedSprite::resetAnimation() {
|
||||||
|
animation_[current_animation_].current_frame = 0;
|
||||||
|
animation_[current_animation_].counter = 0;
|
||||||
|
animation_[current_animation_].completed = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string> // for string, basic_string
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include "core/rendering/movingsprite.h" // for MovingSprite
|
||||||
|
class Texture;
|
||||||
|
|
||||||
|
struct Animation {
|
||||||
|
std::string name; // Nombre de la animacion
|
||||||
|
std::vector<SDL_Rect> frames; // Cada uno de los frames que componen la animación
|
||||||
|
int speed; // Velocidad de la animación (frame-based: ticks per frame)
|
||||||
|
float step_duration_s; // Time-based: segons per frame d'animació (derivat de speed al parse: speed/60)
|
||||||
|
int loop; // Indica a que frame vuelve la animación al terminar. -1 para que no vuelva
|
||||||
|
bool completed; // Indica si ha finalizado la animación
|
||||||
|
int current_frame; // Frame actual
|
||||||
|
int counter; // Contador per a les animacions (frame-based)
|
||||||
|
float time_accumulator_s; // Acumulador de temps (time-based)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AnimatedSpriteData {
|
||||||
|
std::vector<Animation> animations; // Vector con las diferentes animaciones
|
||||||
|
Texture *texture; // Textura con los graficos para el sprite
|
||||||
|
};
|
||||||
|
|
||||||
|
// Carga la animación desde un fichero
|
||||||
|
auto loadAnimationFromFile(Texture *texture, const std::string &file_path, bool verbose = false) -> AnimatedSpriteData;
|
||||||
|
|
||||||
|
// Carga la animación desde bytes en memoria
|
||||||
|
auto loadAnimationFromMemory(Texture *texture, const std::vector<uint8_t> &bytes, const std::string &name_for_logs = "", bool verbose = false) -> AnimatedSpriteData;
|
||||||
|
|
||||||
|
class AnimatedSprite : public MovingSprite {
|
||||||
|
public:
|
||||||
|
explicit AnimatedSprite(Texture *texture = nullptr, SDL_Renderer *renderer = nullptr, const std::string &file = "", const std::vector<std::string> *buffer = nullptr); // Constructor
|
||||||
|
AnimatedSprite(SDL_Renderer *renderer, AnimatedSpriteData *data);
|
||||||
|
|
||||||
|
~AnimatedSprite() override; // Destructor
|
||||||
|
|
||||||
|
void animate(float dt_s); // Calcula el frame correspondiente a la animación actual
|
||||||
|
auto getNumFrames() -> int; // Obtiene el numero de frames de la animación actual
|
||||||
|
void setCurrentFrame(int num); // Establece el frame actual de la animación
|
||||||
|
void setAnimationCounter(const std::string &name, int num); // Establece el valor del contador
|
||||||
|
|
||||||
|
void setAnimationSpeed(const std::string &name, int speed); // Establece la velocidad de una animación
|
||||||
|
void setAnimationSpeed(int index, int speed);
|
||||||
|
|
||||||
|
void setAnimationLoop(const std::string &name, int loop); // Establece el frame al que vuelve la animación al finalizar
|
||||||
|
void setAnimationLoop(int index, int loop);
|
||||||
|
|
||||||
|
void setAnimationCompleted(const std::string &name, bool value); // Establece el valor de la variable
|
||||||
|
void setAnimationCompleted(int index, bool value);
|
||||||
|
|
||||||
|
auto animationIsCompleted() -> bool; // Comprueba si ha terminado la animación
|
||||||
|
|
||||||
|
auto getAnimationClip(const std::string &name = "default", Uint8 index = 0) -> SDL_Rect; // Devuelve el rectangulo de una animación y frame concreto
|
||||||
|
auto getAnimationClip(int index_a = 0, Uint8 index_f = 0) -> SDL_Rect;
|
||||||
|
|
||||||
|
auto getIndex(const std::string &name) -> int; // Obtiene el indice de la animación a partir del nombre
|
||||||
|
auto loadFromVector(const std::vector<std::string> *source) -> bool; // Carga la animación desde un vector
|
||||||
|
|
||||||
|
void setCurrentAnimation(const std::string &name = "default"); // Establece la animacion actual
|
||||||
|
void setCurrentAnimation(int index = 0);
|
||||||
|
|
||||||
|
void update(float dt_s) override; // Actualiza las variables del objeto
|
||||||
|
|
||||||
|
void setAnimationFrames(Uint8 index_animation, Uint8 index_frame, int x, int y, int w, int h); // OLD - Establece el rectangulo para un frame de una animación
|
||||||
|
void setAnimationCounter(int value); // OLD - Establece el contador para todas las animaciones
|
||||||
|
|
||||||
|
void resetAnimation(); // Reinicia la animación
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Variables
|
||||||
|
std::vector<Animation> animation_; // Vector con las diferentes animaciones
|
||||||
|
int current_animation_; // Animacion activa
|
||||||
|
};
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
#include "core/rendering/fade.h"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdlib> // for rand
|
||||||
|
#include <iostream> // for char_traits, basic_ostream, operator<<
|
||||||
|
|
||||||
|
#include "game/defaults.hpp" // for GAMECANVAS_HEIGHT, GAMECANVAS_WIDTH
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Fade::Fade(SDL_Renderer *renderer)
|
||||||
|
: renderer_(renderer) {
|
||||||
|
backbuffer_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT);
|
||||||
|
if (backbuffer_ != nullptr) {
|
||||||
|
SDL_SetTextureScaleMode(backbuffer_, SDL_SCALEMODE_NEAREST);
|
||||||
|
}
|
||||||
|
if (backbuffer_ == nullptr) {
|
||||||
|
std::cout << "Error: textTexture could not be created!\nSDL Error: " << SDL_GetError() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
Fade::~Fade() {
|
||||||
|
SDL_DestroyTexture(backbuffer_);
|
||||||
|
backbuffer_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializa las variables
|
||||||
|
void Fade::init(Uint8 r, Uint8 g, Uint8 b) {
|
||||||
|
fade_type_ = Type::CENTER;
|
||||||
|
enabled_ = false;
|
||||||
|
finished_ = false;
|
||||||
|
counter_ = 0;
|
||||||
|
elapsed_s_ = 0.0F;
|
||||||
|
r_ = r;
|
||||||
|
g_ = g;
|
||||||
|
b_ = b;
|
||||||
|
r_original_ = r;
|
||||||
|
g_original_ = g;
|
||||||
|
b_original_ = b;
|
||||||
|
last_square_ticks_ = 0;
|
||||||
|
squares_drawn_ = 0;
|
||||||
|
fullscreen_done_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pinta una transición en pantalla
|
||||||
|
void Fade::render() {
|
||||||
|
if (enabled_ && !finished_) {
|
||||||
|
switch (fade_type_) {
|
||||||
|
case Type::FULLSCREEN:
|
||||||
|
renderFadeFullscreen();
|
||||||
|
break;
|
||||||
|
case Type::CENTER:
|
||||||
|
renderFadeCenter();
|
||||||
|
break;
|
||||||
|
case Type::RANDOM_SQUARE:
|
||||||
|
renderFadeRandomSquare();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finished_) {
|
||||||
|
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, 255);
|
||||||
|
SDL_RenderClear(renderer_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper de render: tipo FULLSCREEN
|
||||||
|
void Fade::renderFadeFullscreen() {
|
||||||
|
if (fullscreen_done_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int ALPHA = counter_ * 4;
|
||||||
|
if (ALPHA >= 255) {
|
||||||
|
fullscreen_done_ = true;
|
||||||
|
|
||||||
|
// Deja todos los buffers del mismo color
|
||||||
|
SDL_SetRenderTarget(renderer_, backbuffer_);
|
||||||
|
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, 255);
|
||||||
|
SDL_RenderClear(renderer_);
|
||||||
|
|
||||||
|
SDL_SetRenderTarget(renderer_, nullptr);
|
||||||
|
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, 255);
|
||||||
|
SDL_RenderClear(renderer_);
|
||||||
|
|
||||||
|
finished_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dibujamos sobre el renderizador
|
||||||
|
SDL_SetRenderTarget(renderer_, nullptr);
|
||||||
|
|
||||||
|
// Copia el backbuffer con la imagen que había al renderizador
|
||||||
|
SDL_RenderTexture(renderer_, backbuffer_, nullptr, nullptr);
|
||||||
|
|
||||||
|
SDL_FRect f_rect1 = {0, 0, (float)GAMECANVAS_WIDTH, (float)GAMECANVAS_HEIGHT};
|
||||||
|
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, ALPHA);
|
||||||
|
SDL_RenderFillRect(renderer_, &f_rect1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper de render: tipo CENTER
|
||||||
|
void Fade::renderFadeCenter() {
|
||||||
|
SDL_FRect f_r1 = {0, 0, (float)GAMECANVAS_WIDTH, 0};
|
||||||
|
SDL_FRect f_r2 = {0, 0, (float)GAMECANVAS_WIDTH, 0};
|
||||||
|
|
||||||
|
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, 64);
|
||||||
|
|
||||||
|
for (int i = 0; i < counter_; i++) {
|
||||||
|
f_r1.h = f_r2.h = (float)(i * 4);
|
||||||
|
f_r2.y = (float)(GAMECANVAS_HEIGHT - (i * 4));
|
||||||
|
|
||||||
|
SDL_RenderFillRect(renderer_, &f_r1);
|
||||||
|
SDL_RenderFillRect(renderer_, &f_r2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((counter_ * 4) > GAMECANVAS_HEIGHT) {
|
||||||
|
finished_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper de render: tipo RANDOM_SQUARE
|
||||||
|
void Fade::renderFadeRandomSquare() {
|
||||||
|
const Uint32 NOW = SDL_GetTicks();
|
||||||
|
if (squares_drawn_ < 50 && NOW - last_square_ticks_ >= 100) {
|
||||||
|
last_square_ticks_ = NOW;
|
||||||
|
|
||||||
|
SDL_FRect f_rs = {0, 0, 32, 32};
|
||||||
|
|
||||||
|
// Crea un color al azar
|
||||||
|
const Uint8 R = 255 * (rand() % 2);
|
||||||
|
const Uint8 G = 255 * (rand() % 2);
|
||||||
|
const Uint8 B = 255 * (rand() % 2);
|
||||||
|
SDL_SetRenderDrawColor(renderer_, R, G, B, 64);
|
||||||
|
|
||||||
|
// Dibujamos sobre el backbuffer
|
||||||
|
SDL_SetRenderTarget(renderer_, backbuffer_);
|
||||||
|
|
||||||
|
f_rs.x = (float)(rand() % (GAMECANVAS_WIDTH - 32));
|
||||||
|
f_rs.y = (float)(rand() % (GAMECANVAS_HEIGHT - 32));
|
||||||
|
SDL_RenderFillRect(renderer_, &f_rs);
|
||||||
|
|
||||||
|
// Volvemos a usar el renderizador de forma normal
|
||||||
|
SDL_SetRenderTarget(renderer_, nullptr);
|
||||||
|
|
||||||
|
squares_drawn_++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copiamos el backbuffer al renderizador
|
||||||
|
SDL_RenderTexture(renderer_, backbuffer_, nullptr, nullptr);
|
||||||
|
|
||||||
|
if (squares_drawn_ >= 50) {
|
||||||
|
finished_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualiza les variables internes. `counter_` (Uint16, frames a la cadència
|
||||||
|
// de referència 60Hz) es deriva de `elapsed_s_` perquè els helpers de
|
||||||
|
// `render()` (renderFadeFullscreen / Center / RandomSquare) segueixin
|
||||||
|
// llegint-lo igual que abans.
|
||||||
|
void Fade::update(float dt_s) {
|
||||||
|
if (!enabled_) { return; }
|
||||||
|
elapsed_s_ += dt_s;
|
||||||
|
constexpr float FADE_STEPS_PER_S = 60.0F;
|
||||||
|
counter_ = static_cast<Uint16>(elapsed_s_ * FADE_STEPS_PER_S);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activa el fade
|
||||||
|
void Fade::activateFade() {
|
||||||
|
enabled_ = true;
|
||||||
|
finished_ = false;
|
||||||
|
counter_ = 0;
|
||||||
|
elapsed_s_ = 0.0F;
|
||||||
|
squares_drawn_ = 0;
|
||||||
|
last_square_ticks_ = 0;
|
||||||
|
fullscreen_done_ = false;
|
||||||
|
r_ = r_original_;
|
||||||
|
g_ = g_original_;
|
||||||
|
b_ = b_original_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si está activo
|
||||||
|
auto Fade::isEnabled() const -> bool {
|
||||||
|
return enabled_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si ha terminado la transicion
|
||||||
|
auto Fade::hasEnded() const -> bool {
|
||||||
|
return finished_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el tipo de fade
|
||||||
|
void Fade::setFadeType(Type fade_type) {
|
||||||
|
fade_type_ = fade_type;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// Clase Fade
|
||||||
|
class Fade {
|
||||||
|
public:
|
||||||
|
enum class Type : std::uint8_t {
|
||||||
|
FULLSCREEN,
|
||||||
|
CENTER,
|
||||||
|
RANDOM_SQUARE
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit Fade(SDL_Renderer *renderer); // Constructor
|
||||||
|
~Fade(); // Destructor
|
||||||
|
|
||||||
|
void init(Uint8 r, Uint8 g, Uint8 b); // Inicializa las variables
|
||||||
|
void render(); // Pinta una transición en pantalla
|
||||||
|
void update(float dt_s); // Actualiza las variables internas
|
||||||
|
void activateFade(); // Activa el fade
|
||||||
|
|
||||||
|
[[nodiscard]] auto hasEnded() const -> bool; // Comprueba si ha terminado la transicion
|
||||||
|
[[nodiscard]] auto isEnabled() const -> bool; // Comprueba si está activo
|
||||||
|
|
||||||
|
void setFadeType(Type fade_type); // Establece el tipo de fade
|
||||||
|
|
||||||
|
private:
|
||||||
|
void renderFadeFullscreen(); // Helper de render: tipo FULLSCREEN
|
||||||
|
void renderFadeCenter(); // Helper de render: tipo CENTER
|
||||||
|
void renderFadeRandomSquare(); // Helper de render: tipo RANDOM_SQUARE
|
||||||
|
|
||||||
|
SDL_Renderer *renderer_ = nullptr; // El renderizador de la ventana
|
||||||
|
SDL_Texture *backbuffer_ = nullptr; // Textura para usar como backbuffer
|
||||||
|
Type fade_type_{Type::FULLSCREEN}; // Tipo de fade a realizar
|
||||||
|
Uint16 counter_ = 0; // Contador intern (frame-based)
|
||||||
|
float elapsed_s_ = 0.0F; // Acumulador de temps (time-based)
|
||||||
|
bool enabled_ = false; // Indica si el fade está activo
|
||||||
|
bool finished_ = false; // Indica si ha terminado la transición
|
||||||
|
Uint8 r_ = 0, g_ = 0, b_ = 0; // Colores para el fade
|
||||||
|
Uint8 r_original_ = 0, g_original_ = 0, b_original_ = 0; // Colores originales para RANDOM_SQUARE
|
||||||
|
Uint32 last_square_ticks_ = 0; // Ticks del último cuadrado dibujado (RANDOM_SQUARE)
|
||||||
|
Uint16 squares_drawn_ = 0; // Número de cuadrados dibujados (RANDOM_SQUARE)
|
||||||
|
bool fullscreen_done_ = false; // Indica si el fade fullscreen ha terminado la fase de fundido
|
||||||
|
};
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
#include "core/rendering/movingsprite.h"
|
||||||
|
|
||||||
|
#include "core/rendering/texture.h" // for Texture
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
MovingSprite::MovingSprite(float x, float y, int w, int h, float velx, float vely, float accelx, float accely, Texture *texture, SDL_Renderer *renderer)
|
||||||
|
: Sprite(0, 0, w, h, texture, renderer),
|
||||||
|
x_(x),
|
||||||
|
y_(y),
|
||||||
|
x_prev_(x),
|
||||||
|
y_prev_(y),
|
||||||
|
vx_(velx),
|
||||||
|
vy_(vely),
|
||||||
|
ax_(accelx),
|
||||||
|
ay_(accely) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reinicia todas las variables
|
||||||
|
void MovingSprite::clear() {
|
||||||
|
x_ = 0.0F;
|
||||||
|
y_ = 0.0F;
|
||||||
|
|
||||||
|
vx_ = 0.0F;
|
||||||
|
vy_ = 0.0F;
|
||||||
|
|
||||||
|
ax_ = 0.0F;
|
||||||
|
ay_ = 0.0F;
|
||||||
|
|
||||||
|
zoom_w_ = 1.0F;
|
||||||
|
zoom_h_ = 1.0F;
|
||||||
|
|
||||||
|
angle_ = 0.0;
|
||||||
|
rotate_enabled_ = false;
|
||||||
|
center_ = nullptr;
|
||||||
|
rotate_speed_ = 0;
|
||||||
|
rotate_amount_ = 0.0;
|
||||||
|
|
||||||
|
current_flip_ = SDL_FLIP_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mueve el sprite. vx_/vy_ en px/s, ax_/ay_ en px/s². Integració d'Euler
|
||||||
|
// senzilla — suficient per a moviments sense col·lisions sensibles.
|
||||||
|
void MovingSprite::move(float dt_s) {
|
||||||
|
if (enabled_) {
|
||||||
|
x_prev_ = x_;
|
||||||
|
y_prev_ = y_;
|
||||||
|
|
||||||
|
x_ += vx_ * dt_s;
|
||||||
|
y_ += vy_ * dt_s;
|
||||||
|
|
||||||
|
vx_ += ax_ * dt_s;
|
||||||
|
vy_ += ay_ * dt_s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Muestra el sprite por pantalla
|
||||||
|
void MovingSprite::render() {
|
||||||
|
if (enabled_) {
|
||||||
|
texture_->render(renderer_, (int)x_, (int)y_, &sprite_clip_, zoom_w_, zoom_h_, angle_, center_, current_flip_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
// cppcheck-suppress duplInheritedMember ; shadow intencional: vegeu movingsprite.h
|
||||||
|
auto MovingSprite::getPosX() const -> float {
|
||||||
|
return x_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
// cppcheck-suppress duplInheritedMember ; shadow intencional: vegeu movingsprite.h
|
||||||
|
auto MovingSprite::getPosY() const -> float {
|
||||||
|
return y_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto MovingSprite::getVelX() const -> float {
|
||||||
|
return vx_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto MovingSprite::getVelY() const -> float {
|
||||||
|
return vy_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto MovingSprite::getAccelX() const -> float {
|
||||||
|
return ax_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto MovingSprite::getAccelY() const -> float {
|
||||||
|
return ay_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto MovingSprite::getZoomW() const -> float {
|
||||||
|
return zoom_w_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto MovingSprite::getZoomH() const -> float {
|
||||||
|
return zoom_h_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto MovingSprite::getAngle() const -> double {
|
||||||
|
return angle_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la posición y el tamaño del objeto
|
||||||
|
void MovingSprite::setRect(SDL_Rect rect) {
|
||||||
|
x_ = (float)rect.x;
|
||||||
|
y_ = (float)rect.y;
|
||||||
|
w_ = rect.w;
|
||||||
|
h_ = rect.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void MovingSprite::setPosX(float value) {
|
||||||
|
x_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void MovingSprite::setPosY(float value) {
|
||||||
|
y_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void MovingSprite::setVelX(float value) {
|
||||||
|
vx_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void MovingSprite::setVelY(float value) {
|
||||||
|
vy_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void MovingSprite::setAccelX(float value) {
|
||||||
|
ax_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void MovingSprite::setAccelY(float value) {
|
||||||
|
ay_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void MovingSprite::setZoomW(float value) {
|
||||||
|
zoom_w_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void MovingSprite::setZoomH(float value) {
|
||||||
|
zoom_h_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void MovingSprite::setAngle(double value) {
|
||||||
|
angle_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Incrementa el valor de la variable
|
||||||
|
void MovingSprite::incAngle(double value) {
|
||||||
|
angle_ += value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrementa el valor de la variable
|
||||||
|
void MovingSprite::decAngle(double value) {
|
||||||
|
angle_ -= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto MovingSprite::getRotate() const -> bool {
|
||||||
|
return rotate_enabled_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto MovingSprite::getRotateSpeed() const -> Uint16 {
|
||||||
|
return rotate_speed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void MovingSprite::setRotate(bool value) {
|
||||||
|
rotate_enabled_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void MovingSprite::setRotateSpeed(int value) {
|
||||||
|
if (value < 1) {
|
||||||
|
rotate_speed_ = 1;
|
||||||
|
} else {
|
||||||
|
rotate_speed_ = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void MovingSprite::setRotateAmount(double value) {
|
||||||
|
rotate_amount_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void MovingSprite::disableRotate() {
|
||||||
|
rotate_enabled_ = false;
|
||||||
|
angle_ = (double)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualiza les variables internes (move + rotació integrada). La rotació
|
||||||
|
// frame-based original era `incAngle(rotate_amount_)` cada `rotate_speed_`
|
||||||
|
// frames a 60Hz, equivalent a velocitat angular constant
|
||||||
|
// = rotate_amount_ * 60 / rotate_speed_ graus/s.
|
||||||
|
void MovingSprite::update(float dt_s) {
|
||||||
|
move(dt_s);
|
||||||
|
if (enabled_ && rotate_enabled_) {
|
||||||
|
const double ANGULAR_VELOCITY_DEG_PER_S = rotate_amount_ * 60.0 / static_cast<double>(rotate_speed_);
|
||||||
|
incAngle(ANGULAR_VELOCITY_DEG_PER_S * dt_s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambia el sentido de la rotación
|
||||||
|
void MovingSprite::switchRotate() {
|
||||||
|
rotate_amount_ *= -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void MovingSprite::setFlip(SDL_FlipMode flip) {
|
||||||
|
current_flip_ = flip;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gira el sprite horizontalmente
|
||||||
|
void MovingSprite::flip() {
|
||||||
|
current_flip_ = (current_flip_ == SDL_FLIP_HORIZONTAL) ? SDL_FLIP_NONE : SDL_FLIP_HORIZONTAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto MovingSprite::getFlip() -> SDL_FlipMode {
|
||||||
|
return current_flip_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devuelve el rectangulo donde está el sprite
|
||||||
|
auto MovingSprite::getRect() -> SDL_Rect {
|
||||||
|
const SDL_Rect RECT = {(int)x_, (int)y_, w_, h_};
|
||||||
|
return RECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deshace el último movimiento
|
||||||
|
void MovingSprite::undoMove() {
|
||||||
|
x_ = x_prev_;
|
||||||
|
y_ = y_prev_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deshace el último movimiento en el eje X
|
||||||
|
void MovingSprite::undoMoveX() {
|
||||||
|
x_ = x_prev_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deshace el último movimiento en el eje Y
|
||||||
|
void MovingSprite::undoMoveY() {
|
||||||
|
y_ = y_prev_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pone a cero las velocidades de desplacamiento
|
||||||
|
void MovingSprite::clearVel() {
|
||||||
|
vx_ = vy_ = 0.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devuelve el incremento en el eje X en pixels
|
||||||
|
auto MovingSprite::getIncX() const -> int {
|
||||||
|
return (int)x_ - (int)x_prev_;
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include "core/rendering/sprite.h" // for Sprite
|
||||||
|
class Texture;
|
||||||
|
|
||||||
|
// Clase MovingSprite. Añade posicion y velocidad en punto flotante
|
||||||
|
class MovingSprite : public Sprite {
|
||||||
|
public:
|
||||||
|
explicit MovingSprite(float x = 0, float y = 0, int w = 0, int h = 0, float velx = 0, float vely = 0, float accelx = 0, float accely = 0, Texture *texture = nullptr, SDL_Renderer *renderer = nullptr); // Constructor
|
||||||
|
|
||||||
|
void move(float dt_s); // Mueve el sprite (vx/vy/ax/ay en px/s i px/s^2)
|
||||||
|
virtual void update(float dt_s); // Actualiza les variables internes (move + rotació integrada)
|
||||||
|
void clear(); // Reinicia todas las variables
|
||||||
|
void render() override; // Muestra el sprite por pantalla
|
||||||
|
|
||||||
|
// cppcheck-suppress duplInheritedMember ; shadow intencional: Sprite::getPosX retorna int (sprites estàtics), MovingSprite::getPosX retorna float (sub-pixel). No s'accedeix via Sprite*: la jerarquia de joc treballa amb el tipus concret
|
||||||
|
[[nodiscard]] auto getPosX() const -> float; // Obten el valor de la variable
|
||||||
|
// cppcheck-suppress duplInheritedMember ; shadow intencional: vegeu nota a getPosX
|
||||||
|
[[nodiscard]] auto getPosY() const -> float; // Obten el valor de la variable
|
||||||
|
|
||||||
|
[[nodiscard]] auto getVelX() const -> float; // Obten el valor de la variable
|
||||||
|
[[nodiscard]] auto getVelY() const -> float; // Obten el valor de la variable
|
||||||
|
[[nodiscard]] auto getAccelX() const -> float; // Obten el valor de la variable
|
||||||
|
[[nodiscard]] auto getAccelY() const -> float; // Obten el valor de la variable
|
||||||
|
[[nodiscard]] auto getZoomW() const -> float; // Obten el valor de la variable
|
||||||
|
[[nodiscard]] auto getZoomH() const -> float; // Obten el valor de la variable
|
||||||
|
[[nodiscard]] auto getAngle() const -> double; // Obten el valor de la variable
|
||||||
|
[[nodiscard]] auto getRotate() const -> bool; // Obtiene el valor de la variable
|
||||||
|
[[nodiscard]] auto getRotateSpeed() const -> Uint16; // Obtiene el valor de la variable
|
||||||
|
|
||||||
|
void setRect(SDL_Rect rect) override; // Establece la posición y el tamaño del objeto
|
||||||
|
void setPosX(float value); // Establece el valor de la variable
|
||||||
|
void setPosY(float value); // Establece el valor de la variable
|
||||||
|
void setVelX(float value); // Establece el valor de la variable
|
||||||
|
void setVelY(float value); // Establece el valor de la variable
|
||||||
|
void setAccelX(float value); // Establece el valor de la variable
|
||||||
|
void setAccelY(float value); // Establece el valor de la variable
|
||||||
|
void setZoomW(float value); // Establece el valor de la variable
|
||||||
|
void setZoomH(float value); // Establece el valor de la variable
|
||||||
|
void setAngle(double value); // Establece el valor de la variable
|
||||||
|
void incAngle(double value); // Incrementa el valor de la variable
|
||||||
|
void decAngle(double value); // Decrementa el valor de la variable
|
||||||
|
|
||||||
|
void setRotate(bool value); // Establece el valor de la variable
|
||||||
|
void setRotateSpeed(int value); // Establece el valor de la variable
|
||||||
|
void setRotateAmount(double value); // Establece el valor de la variable
|
||||||
|
void disableRotate(); // Quita el efecto de rotación y deja el sprite en su angulo inicial.
|
||||||
|
void switchRotate(); // Cambia el sentido de la rotación
|
||||||
|
|
||||||
|
void setFlip(SDL_FlipMode flip); // Establece el valor de la variable
|
||||||
|
void flip(); // Gira el sprite horizontalmente
|
||||||
|
auto getFlip() -> SDL_FlipMode; // Obtiene el valor de la variable
|
||||||
|
|
||||||
|
auto getRect() -> SDL_Rect override; // Devuelve el rectangulo donde está el sprite
|
||||||
|
|
||||||
|
void undoMove(); // Deshace el último movimiento
|
||||||
|
void undoMoveX(); // Deshace el último movimiento en el eje X
|
||||||
|
void undoMoveY(); // Deshace el último movimiento en el eje Y
|
||||||
|
void clearVel(); // Pone a cero las velocidades de desplacamiento
|
||||||
|
|
||||||
|
[[nodiscard]] auto getIncX() const -> int; // Devuelve el incremento en el eje X en pixels
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// cppcheck-suppress duplInheritedMember ; shadow intencional: Sprite::x_ és int (posició entera per a sprites estàtics), MovingSprite::x_ és float (sub-pixel per a entitats mòbils). No s'accedeix via punter a Sprite*
|
||||||
|
float x_; // Posición en el eje X (sub-pixel; Sprite::x_ es int)
|
||||||
|
// cppcheck-suppress duplInheritedMember ; shadow intencional: vegeu nota a x_
|
||||||
|
float y_; // Posición en el eje Y (sub-pixel; Sprite::y_ es int)
|
||||||
|
|
||||||
|
float x_prev_; // Posición anterior en el eje X
|
||||||
|
float y_prev_; // Posición anterior en el eje Y
|
||||||
|
|
||||||
|
float vx_; // Velocidad en el eje X. Cantidad de pixeles a desplazarse
|
||||||
|
float vy_; // Velocidad en el eje Y. Cantidad de pixeles a desplazarse
|
||||||
|
|
||||||
|
float ax_; // Aceleración en el eje X. Variación de la velocidad
|
||||||
|
float ay_; // Aceleración en el eje Y. Variación de la velocidad
|
||||||
|
|
||||||
|
float zoom_w_{1}; // Zoom aplicado a la anchura
|
||||||
|
float zoom_h_{1}; // Zoom aplicado a la altura
|
||||||
|
|
||||||
|
double angle_{0.0}; // Angulo para dibujarlo
|
||||||
|
bool rotate_enabled_{false}; // Indica si ha de rotar
|
||||||
|
int rotate_speed_{0}; // Velocidad de giro (frames per pas de rotació al ritme de referència 60Hz)
|
||||||
|
double rotate_amount_{0.0}; // Cantidad de grados a girar en cada pas
|
||||||
|
SDL_Point *center_{nullptr}; // Centro de rotación
|
||||||
|
SDL_FlipMode current_flip_{SDL_FLIP_NONE}; // Indica como se voltea el sprite
|
||||||
|
};
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#include "core/rendering/notifications.hpp"
|
||||||
|
|
||||||
|
#include "core/rendering/screen.h"
|
||||||
|
#include "utils/utils.h"
|
||||||
|
|
||||||
|
namespace Notifications {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Paleta semi-saturada: a mig cami entre pastel i color "pur". Manté
|
||||||
|
// contrast del outline (foscor) sense diluir el matís.
|
||||||
|
// (Color no és literal type ⇒ const, no constexpr.)
|
||||||
|
const Color INFO_COLOR{0xF0, 0xD0, 0x40}; // groc
|
||||||
|
const Color TOGGLE_COLOR{0x60, 0xC0, 0xF0}; // cian
|
||||||
|
const Color CHOICE_COLOR{0xD0, 0x60, 0xD0}; // magenta
|
||||||
|
const Color SUCCESS_COLOR{0x70, 0xD0, 0x70}; // verd
|
||||||
|
const Color DANGER_COLOR{0xF0, 0x60, 0x60}; // vermell
|
||||||
|
|
||||||
|
// Factor de foscor per a l'outline (~40% de la lluminositat del
|
||||||
|
// color base): manté el matís i queda prou fosc per a contrastar
|
||||||
|
// amb el text pastel sobre el fons del joc.
|
||||||
|
constexpr float OUTLINE_FACTOR = 0.40F;
|
||||||
|
|
||||||
|
auto baseColor(Palette p) -> Color {
|
||||||
|
switch (p) {
|
||||||
|
case Palette::INFO:
|
||||||
|
return INFO_COLOR;
|
||||||
|
case Palette::TOGGLE:
|
||||||
|
return TOGGLE_COLOR;
|
||||||
|
case Palette::CHOICE:
|
||||||
|
return CHOICE_COLOR;
|
||||||
|
case Palette::SUCCESS:
|
||||||
|
return SUCCESS_COLOR;
|
||||||
|
case Palette::DANGER:
|
||||||
|
return DANGER_COLOR;
|
||||||
|
}
|
||||||
|
return INFO_COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto darken(Color c, float factor) -> Color {
|
||||||
|
return Color{
|
||||||
|
static_cast<Uint8>(static_cast<float>(c.r) * factor),
|
||||||
|
static_cast<Uint8>(static_cast<float>(c.g) * factor),
|
||||||
|
static_cast<Uint8>(static_cast<float>(c.b) * factor),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void show(const std::string &text, Palette palette, Uint32 duration_ms) {
|
||||||
|
if (Screen::get() == nullptr) { return; }
|
||||||
|
const Color BASE = baseColor(palette);
|
||||||
|
const Color OUTLINE = darken(BASE, OUTLINE_FACTOR);
|
||||||
|
Screen::get()->notify(text, BASE, OUTLINE, duration_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Notifications
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// Notificacions overlay centralitzades. Cada call site tria una entrada de
|
||||||
|
// la `Palette` semàntica i una durada; el color base (pastel) i el seu
|
||||||
|
// outline (versió fosca derivada) viuen en un sol lloc — `notifications.cpp`.
|
||||||
|
//
|
||||||
|
// Per a tunejar l'estètica només cal editar les constants del .cpp.
|
||||||
|
|
||||||
|
namespace Notifications {
|
||||||
|
|
||||||
|
enum class Palette : std::uint8_t {
|
||||||
|
INFO, // pastel groc — zoom, finestra/fullscreen
|
||||||
|
TOGGLE, // pastel cian — activacions on/off (shader)
|
||||||
|
CHOICE, // pastel magenta — selecció entre opcions (tipus shader)
|
||||||
|
SUCCESS, // pastel verd — acceptat / connectat (preset, mando added)
|
||||||
|
DANGER, // pastel roig — confirmacions perilloses / desconnexions
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr Uint32 STANDARD_MS = 1500; // Hotkeys "normals"
|
||||||
|
constexpr Uint32 CONFIRM_MS = 2000; // Doble pulsació d'ESC
|
||||||
|
constexpr Uint32 LONG_MS = 2500; // Esdeveniments rellevants (mando)
|
||||||
|
|
||||||
|
// Mostra una notificació. L'outline es deriva automàticament del color
|
||||||
|
// base com a versió fosca (~25% de lluminositat).
|
||||||
|
void show(const std::string &text, Palette palette, Uint32 duration_ms);
|
||||||
|
|
||||||
|
} // namespace Notifications
|
||||||
@@ -0,0 +1,875 @@
|
|||||||
|
#include "core/rendering/screen.h"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <algorithm> // for max, min
|
||||||
|
#include <cmath> // for lround
|
||||||
|
#include <cstring> // for memcpy
|
||||||
|
#include <iostream> // for basic_ostream, operator<<, cout, endl
|
||||||
|
#include <string> // for basic_string, char_traits, string
|
||||||
|
|
||||||
|
#include "core/input/mouse.hpp" // for Mouse::cursorVisible, Mouse::lastMouseMoveTime
|
||||||
|
#include "core/rendering/text.h" // for Text
|
||||||
|
#include "core/resources/resource.h"
|
||||||
|
#include "game/defaults.hpp" // for GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT
|
||||||
|
#include "game/options.hpp" // for Options::video, Options::settings
|
||||||
|
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp" // for Rendering::SDL3GPUShader
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
#include <emscripten.h>
|
||||||
|
#include <emscripten/html5.h>
|
||||||
|
|
||||||
|
// --- Fix per a fullscreen/resize en Emscripten ---
|
||||||
|
//
|
||||||
|
// SDL3 + Emscripten no emet de forma fiable SDL_EVENT_WINDOW_LEAVE_FULLSCREEN
|
||||||
|
// (libsdl-org/SDL#13300) ni SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED /
|
||||||
|
// SDL_EVENT_DISPLAY_ORIENTATION (libsdl-org/SDL#11389). Quan l'usuari ix de
|
||||||
|
// fullscreen amb Esc o rota el mòbil, el canvas HTML torna al tamany correcte
|
||||||
|
// però l'estat intern de SDL creu que segueix en fullscreen amb la resolució
|
||||||
|
// anterior i el viewport queda desencuadrat.
|
||||||
|
//
|
||||||
|
// Solució: registrar callbacks natius d'Emscripten, diferir la feina un tick
|
||||||
|
// del event loop (el canvas encara no està estable en el moment del callback)
|
||||||
|
// i cridar setVideoMode() amb el flag de fullscreen actualitzat. La crida
|
||||||
|
// interna a SDL_SetWindowFullscreen(false) és la peça que realment fa eixir
|
||||||
|
// SDL del seu estat intern de fullscreen — sense això res més funciona.
|
||||||
|
namespace {
|
||||||
|
Screen *g_screen_instance = nullptr;
|
||||||
|
|
||||||
|
void deferredCanvasResize(void * /*userData*/) {
|
||||||
|
if (g_screen_instance) {
|
||||||
|
g_screen_instance->handleCanvasResized();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EM_BOOL onEmFullscreenChange(int /*eventType*/, const EmscriptenFullscreenChangeEvent *event, void * /*userData*/) {
|
||||||
|
if (g_screen_instance && event) {
|
||||||
|
g_screen_instance->syncFullscreenFlagFromBrowser(event->isFullscreen != 0);
|
||||||
|
}
|
||||||
|
emscripten_async_call(deferredCanvasResize, nullptr, 0);
|
||||||
|
return EM_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
EM_BOOL onEmOrientationChange(int /*eventType*/, const EmscriptenOrientationChangeEvent * /*event*/, void * /*userData*/) {
|
||||||
|
emscripten_async_call(deferredCanvasResize, nullptr, 0);
|
||||||
|
return EM_FALSE;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
#endif // __EMSCRIPTEN__
|
||||||
|
|
||||||
|
// Instancia única
|
||||||
|
Screen *Screen::instance = nullptr;
|
||||||
|
|
||||||
|
// Singleton API
|
||||||
|
void Screen::init(SDL_Window *window, SDL_Renderer *renderer) {
|
||||||
|
Screen::instance = new Screen(window, renderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Screen::destroy() {
|
||||||
|
delete Screen::instance;
|
||||||
|
Screen::instance = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Screen::get() -> Screen * {
|
||||||
|
return Screen::instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Screen::Screen(SDL_Window *window, SDL_Renderer *renderer)
|
||||||
|
: border_color_{0x00, 0x00, 0x00} {
|
||||||
|
// Inicializa variables
|
||||||
|
this->window_ = window;
|
||||||
|
this->renderer_ = renderer;
|
||||||
|
|
||||||
|
game_canvas_width_ = GAMECANVAS_WIDTH;
|
||||||
|
game_canvas_height_ = GAMECANVAS_HEIGHT;
|
||||||
|
|
||||||
|
// Establece el modo de video (fullscreen/ventana + logical presentation)
|
||||||
|
// ANTES de crear la textura — SDL3 GPU necesita la logical presentation
|
||||||
|
// del renderer ya aplicada al swapchain quan es reclama la ventana per a GPU.
|
||||||
|
// Mirror del pattern de jaildoctors_dilemma (que usa exactament 256×192 i
|
||||||
|
// funciona) on `initSDLVideo` configura la presentation abans de crear cap
|
||||||
|
// textura.
|
||||||
|
setVideoMode(Options::video.fullscreen);
|
||||||
|
|
||||||
|
// Força al window manager a completar el resize/posicionat abans de passar
|
||||||
|
// la ventana al dispositiu GPU. Sense açò en Linux/X11 hi ha un race
|
||||||
|
// condition que deixa el swapchain en estat inestable i fa crashear el
|
||||||
|
// driver Vulkan en `SDL_CreateGPUGraphicsPipeline`.
|
||||||
|
SDL_SyncWindow(window);
|
||||||
|
|
||||||
|
// Crea la textura donde se dibujan los graficos del juego.
|
||||||
|
// ARGB8888 per simplificar el readback cap al pipeline SDL3 GPU.
|
||||||
|
game_canvas_ = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, game_canvas_width_, game_canvas_height_);
|
||||||
|
if (game_canvas_ != nullptr) {
|
||||||
|
SDL_SetTextureScaleMode(game_canvas_, Options::video.scale_mode);
|
||||||
|
}
|
||||||
|
if (game_canvas_ == nullptr) {
|
||||||
|
if (Options::settings.console) {
|
||||||
|
std::cout << "gameCanvas could not be created!\nSDL Error: " << SDL_GetError() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
// Buffer de readback del gameCanvas (lo dimensionamos una vez)
|
||||||
|
pixel_buffer_.resize(static_cast<size_t>(game_canvas_width_) * static_cast<size_t>(game_canvas_height_));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Renderiza una vez la textura vacía al renderer abans d'inicialitzar els
|
||||||
|
// shaders: jaildoctors_dilemma ho fa així i evita que el driver Vulkan
|
||||||
|
// crashegi en la creació del pipeline gràfic. `initShaders()` es crida
|
||||||
|
// després des de `Director` amb el swapchain ja estable.
|
||||||
|
SDL_RenderTexture(renderer, game_canvas_, nullptr, nullptr);
|
||||||
|
|
||||||
|
// Estado inicial de las notificaciones. El Text real se enlaza después vía
|
||||||
|
// `initNotifications()` quan `Resource` ja estigui inicialitzat. Dividim
|
||||||
|
// això del constructor perquè `initShaders()` (GPU) ha de cridar-se ABANS
|
||||||
|
// de carregar recursos: si el SDL_Renderer ha fet abans moltes
|
||||||
|
// allocacions (carrega de textures), el driver Vulkan crasheja quan
|
||||||
|
// després es reclama la ventana per al dispositiu GPU.
|
||||||
|
notification_text_ = nullptr;
|
||||||
|
notification_message_ = "";
|
||||||
|
notification_text_color_ = {0xFF, 0xFF, 0xFF};
|
||||||
|
notification_outline_color_ = {0x00, 0x00, 0x00};
|
||||||
|
notification_end_time_ = 0;
|
||||||
|
notification_y_ = 2;
|
||||||
|
|
||||||
|
// Registra callbacks natius d'Emscripten per a fullscreen/orientation
|
||||||
|
registerEmscriptenEventCallbacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enllaça el Text de les notificacions amb el recurs compartit de `Resource`.
|
||||||
|
// S'ha de cridar després de `Resource::init(...)`.
|
||||||
|
void Screen::initNotifications() {
|
||||||
|
notification_text_ = Resource::get()->getText("8bithud");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
Screen::~Screen() {
|
||||||
|
// notificationText es propiedad de Resource — no liberar.
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
shutdownShaders();
|
||||||
|
#endif
|
||||||
|
SDL_DestroyTexture(game_canvas_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpia la pantalla
|
||||||
|
void Screen::clean(Color color) {
|
||||||
|
SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, 0xFF);
|
||||||
|
SDL_RenderClear(renderer_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepara para empezar a dibujar en la textura de juego
|
||||||
|
void Screen::start() {
|
||||||
|
SDL_SetRenderTarget(renderer_, game_canvas_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vuelca el contenido del renderizador en pantalla
|
||||||
|
void Screen::blit() {
|
||||||
|
updateFps();
|
||||||
|
|
||||||
|
// Dibuja la notificación activa i, si toca, l'overlay de FPS sobre el gameCanvas
|
||||||
|
SDL_SetRenderTarget(renderer_, game_canvas_);
|
||||||
|
renderNotification();
|
||||||
|
renderFps();
|
||||||
|
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
// Si el backend GPU està viu i accelerat, passem sempre per ell (tant amb
|
||||||
|
// shaders com sense). Seguim el mateix pattern que aee_plus: quan shader
|
||||||
|
// està desactivat, forcem POSTFX + params a zero només per a aquest frame
|
||||||
|
// i restaurem el shader actiu, així CRTPI no aplica les seues scanlines
|
||||||
|
// quan l'usuari ho ha desactivat.
|
||||||
|
if (shader_backend_ && shader_backend_->isHardwareAccelerated()) {
|
||||||
|
SDL_Surface *surface = SDL_RenderReadPixels(renderer_, nullptr);
|
||||||
|
if (surface != nullptr) {
|
||||||
|
if (surface->format == SDL_PIXELFORMAT_ARGB8888) {
|
||||||
|
std::memcpy(pixel_buffer_.data(), surface->pixels, pixel_buffer_.size() * sizeof(Uint32));
|
||||||
|
} else {
|
||||||
|
SDL_Surface *converted = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_ARGB8888);
|
||||||
|
if (converted != nullptr) {
|
||||||
|
std::memcpy(pixel_buffer_.data(), converted->pixels, pixel_buffer_.size() * sizeof(Uint32));
|
||||||
|
SDL_DestroySurface(converted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SDL_DestroySurface(surface);
|
||||||
|
}
|
||||||
|
SDL_SetRenderTarget(renderer_, nullptr);
|
||||||
|
|
||||||
|
if (Options::video.shader.enabled) {
|
||||||
|
// Ruta normal: shader amb els seus params.
|
||||||
|
shader_backend_->uploadPixels(pixel_buffer_.data(), game_canvas_width_, game_canvas_height_);
|
||||||
|
shader_backend_->render();
|
||||||
|
} else {
|
||||||
|
// Shader off: POSTFX amb params zero (passa-per-aquí). CRTPI no
|
||||||
|
// val perque sempre aplica els seus efectes interns; salvem i
|
||||||
|
// restaurem el shader actiu.
|
||||||
|
const auto PREV_SHADER = shader_backend_->getActiveShader();
|
||||||
|
if (PREV_SHADER != Rendering::ShaderType::POSTFX) {
|
||||||
|
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
|
||||||
|
}
|
||||||
|
shader_backend_->setPostFXParams(Rendering::PostFXParams{});
|
||||||
|
shader_backend_->uploadPixels(pixel_buffer_.data(), game_canvas_width_, game_canvas_height_);
|
||||||
|
shader_backend_->render();
|
||||||
|
if (PREV_SHADER != Rendering::ShaderType::POSTFX) {
|
||||||
|
shader_backend_->setActiveShader(PREV_SHADER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Vuelve a dejar el renderizador en modo normal
|
||||||
|
SDL_SetRenderTarget(renderer_, nullptr);
|
||||||
|
|
||||||
|
// Borra el contenido previo
|
||||||
|
SDL_SetRenderDrawColor(renderer_, border_color_.r, border_color_.g, border_color_.b, 0xFF);
|
||||||
|
SDL_RenderClear(renderer_);
|
||||||
|
|
||||||
|
// Copia la textura de juego en el renderizador en la posición adecuada
|
||||||
|
SDL_FRect fdest = {(float)dest_.x, (float)dest_.y, (float)dest_.w, (float)dest_.h};
|
||||||
|
SDL_RenderTexture(renderer_, game_canvas_, nullptr, &fdest);
|
||||||
|
|
||||||
|
// Muestra por pantalla el renderizador
|
||||||
|
SDL_RenderPresent(renderer_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Video y ventana
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Establece el modo de video
|
||||||
|
void Screen::setVideoMode(bool fullscreen) {
|
||||||
|
applyFullscreen(fullscreen);
|
||||||
|
if (fullscreen) {
|
||||||
|
applyFullscreenLayout();
|
||||||
|
} else {
|
||||||
|
applyWindowedLayout();
|
||||||
|
}
|
||||||
|
applyLogicalPresentation(fullscreen);
|
||||||
|
|
||||||
|
// En SDL3 + Vulkan sobre Windows, després de SDL_SetWindowSize la render-
|
||||||
|
// target texture (gameCanvas) queda en un estat on SDL_RenderClear funciona
|
||||||
|
// però SDL_RenderTexture* no dibuixa res: el frame següent només mostra el
|
||||||
|
// fons net, els sprites desapareixen. Title se'n surt sense voler perquè
|
||||||
|
// createTiledBackground() crea/destrueix una textura target nova, i això
|
||||||
|
// reinicialitza l'estat intern del renderer. Recreem gameCanvas aquí
|
||||||
|
// mateix per garantir el mateix efecte en qualsevol escena.
|
||||||
|
if (game_canvas_ != nullptr) {
|
||||||
|
SDL_DestroyTexture(game_canvas_);
|
||||||
|
game_canvas_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, game_canvas_width_, game_canvas_height_);
|
||||||
|
if (game_canvas_ != nullptr) {
|
||||||
|
SDL_SetTextureScaleMode(game_canvas_, Options::video.scale_mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambia entre pantalla completa y ventana
|
||||||
|
void Screen::toggleVideoMode() {
|
||||||
|
setVideoMode(!Options::video.fullscreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduce el zoom de la ventana
|
||||||
|
auto Screen::decWindowZoom() -> bool {
|
||||||
|
if (Options::video.fullscreen) { return false; }
|
||||||
|
const int PREV = Options::window.zoom;
|
||||||
|
Options::window.zoom = std::max(Options::window.zoom - 1, WINDOW_ZOOM_MIN);
|
||||||
|
if (Options::window.zoom == PREV) { return false; }
|
||||||
|
setVideoMode(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aumenta el zoom de la ventana
|
||||||
|
auto Screen::incWindowZoom() -> bool {
|
||||||
|
if (Options::video.fullscreen) { return false; }
|
||||||
|
const int PREV = Options::window.zoom;
|
||||||
|
Options::window.zoom = std::min(Options::window.zoom + 1, Options::window.max_zoom);
|
||||||
|
if (Options::window.zoom == PREV) { return false; }
|
||||||
|
setVideoMode(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el zoom de la ventana directamente
|
||||||
|
auto Screen::setWindowZoom(int zoom) -> bool {
|
||||||
|
if (Options::video.fullscreen) { return false; }
|
||||||
|
if (zoom < WINDOW_ZOOM_MIN || zoom > Options::window.max_zoom) { return false; }
|
||||||
|
if (zoom == Options::window.zoom) { return false; }
|
||||||
|
Options::window.zoom = zoom;
|
||||||
|
setVideoMode(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detecta el zoom màxim windowed segons la resolució del display actual.
|
||||||
|
void Screen::detectMaxZoom() {
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
// En WASM el tamany del canvas el fixa el browser; el zoom no aplica.
|
||||||
|
return;
|
||||||
|
#else
|
||||||
|
int num_displays = 0;
|
||||||
|
SDL_DisplayID *displays = SDL_GetDisplays(&num_displays);
|
||||||
|
if (displays == nullptr || num_displays == 0) {
|
||||||
|
if (displays != nullptr) { SDL_free(displays); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto *dm = SDL_GetCurrentDisplayMode(displays[0]);
|
||||||
|
if (dm != nullptr) {
|
||||||
|
const int MAX_W = dm->w / GAMECANVAS_WIDTH;
|
||||||
|
const int MAX_H = (dm->h - WINDOWS_DECORATIONS) / GAMECANVAS_HEIGHT;
|
||||||
|
const int DETECTED = std::max(WINDOW_ZOOM_MIN, std::min(MAX_W, MAX_H));
|
||||||
|
Options::window.max_zoom = DETECTED;
|
||||||
|
Options::window.zoom = std::clamp(Options::window.zoom, WINDOW_ZOOM_MIN, DETECTED);
|
||||||
|
if (Options::settings.console) {
|
||||||
|
std::cout << "Display " << dm->w << "x" << dm->h
|
||||||
|
<< " → max windowed zoom = " << DETECTED << "x\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SDL_free(displays);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estableix el mode de presentacio del canvas i reaplica el layout
|
||||||
|
void Screen::setPresentationMode(Options::PresentationMode mode) {
|
||||||
|
if (Options::video.presentation_mode == mode) { return; }
|
||||||
|
Options::video.presentation_mode = mode;
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
if (shader_backend_) {
|
||||||
|
shader_backend_->setPresentationMode(static_cast<Rendering::ShaderBackend::PresentationMode>(mode));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
setVideoMode(Options::video.fullscreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cicla integer_scale -> letterbox -> stretched -> overscan -> integer_scale
|
||||||
|
void Screen::nextPresentationMode() {
|
||||||
|
setPresentationMode(Options::nextPresentationMode(Options::video.presentation_mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nom curt del mode actual (per a notificacions). Static perque no necessita
|
||||||
|
// estat d'instancia: nomes consulta Options::video.
|
||||||
|
auto Screen::getPresentationModeName() -> const char * {
|
||||||
|
return Options::presentationModeToString(Options::video.presentation_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el V-Sync
|
||||||
|
void Screen::setVSync(bool enabled) {
|
||||||
|
Options::video.vsync = enabled;
|
||||||
|
SDL_SetRenderVSync(renderer_, enabled ? 1 : SDL_RENDERER_VSYNC_DISABLED);
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
if (shader_backend_) {
|
||||||
|
shader_backend_->setVSync(enabled);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alterna el V-Sync
|
||||||
|
void Screen::toggleVSync() {
|
||||||
|
setVSync(!Options::video.vsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambia el color del borde
|
||||||
|
void Screen::setBorderColor(Color color) {
|
||||||
|
border_color_ = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Helpers privados de setVideoMode
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// SDL_SetWindowFullscreen + visibilidad del cursor
|
||||||
|
void Screen::applyFullscreen(bool fullscreen) {
|
||||||
|
SDL_SetWindowFullscreen(window_, fullscreen);
|
||||||
|
if (fullscreen) {
|
||||||
|
SDL_HideCursor();
|
||||||
|
Mouse::cursor_visible = false;
|
||||||
|
} else {
|
||||||
|
SDL_ShowCursor();
|
||||||
|
Mouse::cursor_visible = true;
|
||||||
|
Mouse::last_mouse_move_time = SDL_GetTicks();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcula windowWidth/Height/dest para el modo ventana y aplica SDL_SetWindowSize
|
||||||
|
void Screen::applyWindowedLayout() {
|
||||||
|
window_width_ = game_canvas_width_;
|
||||||
|
window_height_ = game_canvas_height_;
|
||||||
|
dest_ = {.x = 0, .y = 0, .w = game_canvas_width_, .h = game_canvas_height_};
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
windowWidth *= WASM_RENDER_SCALE;
|
||||||
|
windowHeight *= WASM_RENDER_SCALE;
|
||||||
|
dest.w *= WASM_RENDER_SCALE;
|
||||||
|
dest.h *= WASM_RENDER_SCALE;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Modifica el tamaño de la ventana
|
||||||
|
SDL_SetWindowSize(window_, window_width_ * Options::window.zoom, window_height_ * Options::window.zoom);
|
||||||
|
// Sense aquesta sincronia, en Windows + Vulkan el swapchain del SDL3 GPU
|
||||||
|
// es queda en estat out-of-date després del resize i SDL_AcquireGPU-
|
||||||
|
// SwapchainTexture deixa de tornar una textura vàlida → finestra negra.
|
||||||
|
// En Linux Mesa el driver ho tolera, però el patró segur (igual que
|
||||||
|
// jaildoctors_dilemma) és esperar que el WM completi el resize abans de
|
||||||
|
// reposicionar i continuar amb el render.
|
||||||
|
SDL_SyncWindow(window_);
|
||||||
|
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el tamaño de la ventana en fullscreen y calcula el rect del juego
|
||||||
|
void Screen::applyFullscreenLayout() {
|
||||||
|
SDL_GetWindowSize(window_, &window_width_, &window_height_);
|
||||||
|
computeFullscreenGameRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcula el rectangle dest segons el PresentationMode actiu.
|
||||||
|
// INTEGER_SCALE: x sencera maxima (1x, 2x, 3x...) centrada amb barres.
|
||||||
|
// LETTERBOX: mante aspect ratio, ajusta al menor dels eixos, barres.
|
||||||
|
// STRETCHED: omple tota la finestra deformant la relacio d'aspecte.
|
||||||
|
// OVERSCAN: mante aspect ratio omplint la finestra (retalla el sobrant).
|
||||||
|
void Screen::computeFullscreenGameRect() {
|
||||||
|
const float CANVAS_RATIO = static_cast<float>(game_canvas_width_) / static_cast<float>(game_canvas_height_);
|
||||||
|
const float WINDOW_RATIO = static_cast<float>(window_width_) / static_cast<float>(window_height_);
|
||||||
|
|
||||||
|
switch (Options::video.presentation_mode) {
|
||||||
|
case Options::PresentationMode::INTEGER_SCALE: {
|
||||||
|
int scale = 0;
|
||||||
|
while (((game_canvas_width_ * (scale + 1)) <= window_width_) && ((game_canvas_height_ * (scale + 1)) <= window_height_)) {
|
||||||
|
scale++;
|
||||||
|
}
|
||||||
|
dest_.w = game_canvas_width_ * scale;
|
||||||
|
dest_.h = game_canvas_height_ * scale;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Options::PresentationMode::LETTERBOX: {
|
||||||
|
if (WINDOW_RATIO >= CANVAS_RATIO) {
|
||||||
|
dest_.h = window_height_;
|
||||||
|
dest_.w = static_cast<int>(std::lround(window_height_ * CANVAS_RATIO));
|
||||||
|
} else {
|
||||||
|
dest_.w = window_width_;
|
||||||
|
dest_.h = static_cast<int>(std::lround(window_width_ / CANVAS_RATIO));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Options::PresentationMode::STRETCHED: {
|
||||||
|
dest_.w = window_width_;
|
||||||
|
dest_.h = window_height_;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Options::PresentationMode::OVERSCAN: {
|
||||||
|
// Mante aspect: dimensiona al major dels eixos (l'altre desborda).
|
||||||
|
if (WINDOW_RATIO >= CANVAS_RATIO) {
|
||||||
|
dest_.w = window_width_;
|
||||||
|
dest_.h = static_cast<int>(std::lround(window_width_ / CANVAS_RATIO));
|
||||||
|
} else {
|
||||||
|
dest_.h = window_height_;
|
||||||
|
dest_.w = static_cast<int>(std::lround(window_height_ * CANVAS_RATIO));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dest_.x = (window_width_ - dest_.w) / 2;
|
||||||
|
dest_.y = (window_height_ - dest_.h) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplica la logical presentation segons el PresentationMode actiu (ruta SDL_Renderer fallback).
|
||||||
|
// La ruta GPU calcula el viewport ella mateixa via computeViewport().
|
||||||
|
void Screen::applyLogicalPresentation(bool fullscreen) {
|
||||||
|
SDL_RendererLogicalPresentation lp = SDL_LOGICAL_PRESENTATION_LETTERBOX;
|
||||||
|
switch (Options::video.presentation_mode) {
|
||||||
|
case Options::PresentationMode::INTEGER_SCALE:
|
||||||
|
lp = SDL_LOGICAL_PRESENTATION_INTEGER_SCALE;
|
||||||
|
break;
|
||||||
|
case Options::PresentationMode::LETTERBOX:
|
||||||
|
lp = SDL_LOGICAL_PRESENTATION_LETTERBOX;
|
||||||
|
break;
|
||||||
|
case Options::PresentationMode::STRETCHED:
|
||||||
|
lp = SDL_LOGICAL_PRESENTATION_STRETCH;
|
||||||
|
break;
|
||||||
|
case Options::PresentationMode::OVERSCAN:
|
||||||
|
lp = SDL_LOGICAL_PRESENTATION_OVERSCAN;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
SDL_SetRenderLogicalPresentation(renderer_, window_width_, window_height_, lp);
|
||||||
|
Options::video.fullscreen = fullscreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Notificaciones
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Muestra una notificación en la línea superior durante durationMs
|
||||||
|
void Screen::notify(const std::string &text, Color text_color, Color outline_color, Uint32 duration_ms) {
|
||||||
|
notification_message_ = text;
|
||||||
|
notification_text_color_ = text_color;
|
||||||
|
notification_outline_color_ = outline_color;
|
||||||
|
notification_end_time_ = SDL_GetTicks() + duration_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limpia la notificación actual
|
||||||
|
void Screen::clearNotification() {
|
||||||
|
notification_end_time_ = 0;
|
||||||
|
notification_message_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dibuja la notificación activa (si la hay) sobre el gameCanvas. La Y es
|
||||||
|
// el `notification_y_` configurat, desplacat cap avall si en overscan part
|
||||||
|
// de la franja superior queda fora de pantalla.
|
||||||
|
void Screen::renderNotification() {
|
||||||
|
if (notification_text_ == nullptr || SDL_GetTicks() >= notification_end_time_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notification_text_->writeDX(Text::FLAG_CENTER | Text::FLAG_COLOR | Text::FLAG_STROKE,
|
||||||
|
game_canvas_width_ / 2,
|
||||||
|
notification_y_ + safeNotificationY(),
|
||||||
|
notification_message_,
|
||||||
|
1,
|
||||||
|
notification_text_color_,
|
||||||
|
1,
|
||||||
|
notification_outline_color_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alterna la visibilitat de l'overlay de FPS. No persisteix a config.
|
||||||
|
void Screen::toggleFps() {
|
||||||
|
fps_visible_ = !fps_visible_;
|
||||||
|
if (fps_visible_) {
|
||||||
|
fps_window_start_ticks_ = SDL_GetTicks();
|
||||||
|
fps_frame_count_ = 0;
|
||||||
|
fps_value_ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Screen::isFpsVisible() const -> bool {
|
||||||
|
return fps_visible_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acumula frames i recalcula el FPS cada segon real (no afectat per dt del joc).
|
||||||
|
// Cridat des de blit() una vegada per frame.
|
||||||
|
void Screen::updateFps() {
|
||||||
|
if (!fps_visible_) { return; }
|
||||||
|
++fps_frame_count_;
|
||||||
|
const Uint32 NOW = SDL_GetTicks();
|
||||||
|
const Uint32 ELAPSED = NOW - fps_window_start_ticks_;
|
||||||
|
if (ELAPSED >= 1000) {
|
||||||
|
fps_value_ = static_cast<int>((static_cast<Uint64>(fps_frame_count_) * 1000) / ELAPSED);
|
||||||
|
fps_frame_count_ = 0;
|
||||||
|
fps_window_start_ticks_ = NOW;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dibuixa "NN FPS" a dalt a la dreta del canvas, mateixa Y que les notificacions
|
||||||
|
// (incloent l'ajust per overscan) i amb la mateixa font 8bithud.
|
||||||
|
void Screen::renderFps() {
|
||||||
|
if (!fps_visible_ || notification_text_ == nullptr) { return; }
|
||||||
|
constexpr int RIGHT_MARGIN = 2;
|
||||||
|
const Color FPS_COLOR{0x60, 0xD0, 0x70}; // verd (mateix to que SUCCESS de notificacions)
|
||||||
|
const Color FPS_OUTLINE{0x26, 0x53, 0x2C}; // ~40% darken del verd
|
||||||
|
const std::string MSG = std::to_string(fps_value_) + " FPS";
|
||||||
|
const int TEXT_W = notification_text_->lenght(MSG, 1);
|
||||||
|
const int X = game_canvas_width_ - TEXT_W - RIGHT_MARGIN;
|
||||||
|
const int Y = notification_y_ + safeNotificationY();
|
||||||
|
notification_text_->writeDX(Text::FLAG_COLOR | Text::FLAG_STROKE,
|
||||||
|
X,
|
||||||
|
Y,
|
||||||
|
MSG,
|
||||||
|
1,
|
||||||
|
FPS_COLOR,
|
||||||
|
1,
|
||||||
|
FPS_OUTLINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Y minima del canvas visible. Solo no zero quan estem en overscan i l'aspect
|
||||||
|
// de finestra obliga a escalar mes ample que alt (el canvas vertical desborda
|
||||||
|
// i la franja superior es retalla). En cas contrari (qualsevol altre mode, o
|
||||||
|
// overscan amb retall horitzontal nomes), retorna 0.
|
||||||
|
auto Screen::safeNotificationY() const -> int {
|
||||||
|
if (Options::video.presentation_mode != Options::PresentationMode::OVERSCAN) { return 0; }
|
||||||
|
if (window_width_ <= 0 || window_height_ <= 0 || game_canvas_height_ <= 0) { return 0; }
|
||||||
|
const float CANVAS_RATIO = static_cast<float>(game_canvas_width_) / static_cast<float>(game_canvas_height_);
|
||||||
|
const float WINDOW_RATIO = static_cast<float>(window_width_) / static_cast<float>(window_height_);
|
||||||
|
if (WINDOW_RATIO < CANVAS_RATIO) { return 0; } // retall horitzontal nomes
|
||||||
|
const float OVERSCAN_SCALE = static_cast<float>(window_width_) / static_cast<float>(game_canvas_width_);
|
||||||
|
const float VH = static_cast<float>(game_canvas_height_) * OVERSCAN_SCALE;
|
||||||
|
return static_cast<int>(std::ceil((VH - static_cast<float>(window_height_)) / (2.0F * OVERSCAN_SCALE)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Emscripten — fix per a fullscreen/resize (veure el bloc de comentaris al
|
||||||
|
// principi del fitxer i l'anonymous namespace amb els callbacks natius).
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void Screen::handleCanvasResized() {
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
// La crida a SDL_SetWindowFullscreen + SDL_SetRenderLogicalPresentation
|
||||||
|
// que fa setVideoMode és l'única manera de resincronitzar l'estat intern
|
||||||
|
// de SDL amb el canvas HTML real.
|
||||||
|
setVideoMode(Options::video.fullscreen);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Screen::syncFullscreenFlagFromBrowser(bool is_fullscreen) {
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
Options::video.fullscreen = isFullscreen;
|
||||||
|
#else
|
||||||
|
(void)is_fullscreen;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Screen::registerEmscriptenEventCallbacks() {
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
// IMPORTANT: 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. Això portava a cridar setVideoMode per cada scroll, que
|
||||||
|
// re-aplicava la logical presentation i corrompia el viewport intern de SDL.
|
||||||
|
g_screen_instance = this;
|
||||||
|
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onEmFullscreenChange);
|
||||||
|
emscripten_set_orientationchange_callback(nullptr, EM_TRUE, onEmOrientationChange);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// GPU / shaders (SDL3 GPU post-procesado). En builds con NO_SHADERS (Emscripten)
|
||||||
|
// las operaciones son no-op; la ruta clásica sigue siendo la única disponible.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
// Aplica al backend el shader actiu + els seus presets PostFX i CrtPi.
|
||||||
|
// Només s'ha de cridar quan `videoShaderEnabled=true` (en cas contrari el
|
||||||
|
// blit() ja força POSTFX+zero params per a desactivar els efectes sense
|
||||||
|
// tocar els paràmetres emmagatzemats).
|
||||||
|
void Screen::applyShaderParams() {
|
||||||
|
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shader_backend_->setActiveShader(Options::video.shader.current_shader);
|
||||||
|
applyCurrentPostFXPreset();
|
||||||
|
applyCurrentCrtPiPreset();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void Screen::initShaders() {
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
if (!shader_backend_) {
|
||||||
|
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
|
||||||
|
const std::string FALLBACK_DRIVER = "none";
|
||||||
|
shader_backend_->setPreferredDriver(
|
||||||
|
Options::video.gpu.acceleration ? Options::video.gpu.preferred_driver : FALLBACK_DRIVER);
|
||||||
|
}
|
||||||
|
if (!shader_backend_->isHardwareAccelerated()) {
|
||||||
|
const bool OK = shader_backend_->init(window_, game_canvas_, "", "");
|
||||||
|
if (Options::settings.console) {
|
||||||
|
std::cout << "Screen::initShaders: SDL3GPUShader::init() = " << (OK ? "OK" : "FAILED") << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shader_backend_->isHardwareAccelerated()) {
|
||||||
|
shader_backend_->setPresentationMode(static_cast<Rendering::ShaderBackend::PresentationMode>(Options::video.presentation_mode));
|
||||||
|
shader_backend_->setVSync(Options::video.vsync);
|
||||||
|
|
||||||
|
// Resol els índexs de preset a partir del nom emmagatzemat al config.
|
||||||
|
// Si el nom no existeix (preset esborrat del YAML), es queda en 0.
|
||||||
|
for (int i = 0; i < static_cast<int>(Options::postfx_presets.size()); ++i) {
|
||||||
|
if (Options::postfx_presets[i].name == Options::video.shader.current_postfx_preset_name) {
|
||||||
|
Options::current_postfx_preset = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < static_cast<int>(Options::crtpi_presets.size()); ++i) {
|
||||||
|
if (Options::crtpi_presets[i].name == Options::video.shader.current_crtpi_preset_name) {
|
||||||
|
Options::current_crtpi_preset = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyShaderParams(); // aplica preset del shader actiu
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Screen::shutdownShaders() {
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
// Només es crida des del destructor de Screen. Els toggles runtime NO la
|
||||||
|
// poden cridar: destruir + recrear el dispositiu SDL3 GPU amb la ventana
|
||||||
|
// ja reclamada és inestable (Vulkan/Radeon crasheja en el següent claim).
|
||||||
|
if (shader_backend_) {
|
||||||
|
shader_backend_->cleanup();
|
||||||
|
shader_backend_.reset();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Screen::isGpuAccelerated() const -> bool {
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
return shader_backend_ && shader_backend_->isHardwareAccelerated();
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Screen::setShaderEnabled(bool enabled) {
|
||||||
|
if (Options::video.shader.enabled == enabled) { return; }
|
||||||
|
Options::video.shader.enabled = enabled;
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
if (enabled) {
|
||||||
|
applyShaderParams(); // restaura preset del shader actiu
|
||||||
|
}
|
||||||
|
// Si enabled=false, blit() forçarà POSTFX+zero per frame — no cal tocar
|
||||||
|
// res ara.
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Screen::toggleShaderEnabled() {
|
||||||
|
setShaderEnabled(!Options::video.shader.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Screen::isShaderEnabled() -> bool {
|
||||||
|
return Options::video.shader.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
void Screen::setActiveShader(Rendering::ShaderType type) {
|
||||||
|
Options::video.shader.current_shader = type;
|
||||||
|
if (Options::video.shader.enabled) {
|
||||||
|
applyShaderParams();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Screen::getActiveShader() -> Rendering::ShaderType {
|
||||||
|
return Options::video.shader.current_shader;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void Screen::toggleActiveShader() {
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
const Rendering::ShaderType NEXT = getActiveShader() == Rendering::ShaderType::POSTFX
|
||||||
|
? Rendering::ShaderType::CRTPI
|
||||||
|
: Rendering::ShaderType::POSTFX;
|
||||||
|
setActiveShader(NEXT);
|
||||||
|
#else
|
||||||
|
Options::video.shader.current_shader = Options::video.shader.current_shader == Rendering::ShaderType::POSTFX
|
||||||
|
? Rendering::ShaderType::CRTPI
|
||||||
|
: Rendering::ShaderType::POSTFX;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Presets de shaders
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
void Screen::applyCurrentPostFXPreset() {
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return; }
|
||||||
|
if (Options::postfx_presets.empty()) { return; }
|
||||||
|
if (Options::current_postfx_preset < 0 || Options::current_postfx_preset >= static_cast<int>(Options::postfx_presets.size())) {
|
||||||
|
Options::current_postfx_preset = 0;
|
||||||
|
}
|
||||||
|
const auto &preset = Options::postfx_presets[Options::current_postfx_preset];
|
||||||
|
Rendering::PostFXParams p;
|
||||||
|
p.vignette = preset.vignette;
|
||||||
|
p.scanlines = preset.scanlines;
|
||||||
|
p.chroma_min = preset.chroma_min;
|
||||||
|
p.chroma_max = preset.chroma_max;
|
||||||
|
p.mask = preset.mask;
|
||||||
|
p.gamma = preset.gamma;
|
||||||
|
p.curvature = preset.curvature;
|
||||||
|
p.bleeding = preset.bleeding;
|
||||||
|
p.flicker = preset.flicker;
|
||||||
|
p.scan_dark_ratio = preset.scan_dark_ratio;
|
||||||
|
p.scan_dark_floor = preset.scan_dark_floor;
|
||||||
|
p.scan_edge_soft = preset.scan_edge_soft;
|
||||||
|
shader_backend_->setPostFXParams(p);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Screen::applyCurrentCrtPiPreset() {
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return; }
|
||||||
|
if (Options::crtpi_presets.empty()) { return; }
|
||||||
|
if (Options::current_crtpi_preset < 0 || Options::current_crtpi_preset >= static_cast<int>(Options::crtpi_presets.size())) {
|
||||||
|
Options::current_crtpi_preset = 0;
|
||||||
|
}
|
||||||
|
const auto &preset = Options::crtpi_presets[Options::current_crtpi_preset];
|
||||||
|
Rendering::CrtPiParams p;
|
||||||
|
p.scanline_weight = preset.scanline_weight;
|
||||||
|
p.scanline_gap_brightness = preset.scanline_gap_brightness;
|
||||||
|
p.bloom_factor = preset.bloom_factor;
|
||||||
|
p.input_gamma = preset.input_gamma;
|
||||||
|
p.output_gamma = preset.output_gamma;
|
||||||
|
p.mask_brightness = preset.mask_brightness;
|
||||||
|
p.curvature_x = preset.curvature_x;
|
||||||
|
p.curvature_y = preset.curvature_y;
|
||||||
|
p.mask_type = preset.mask_type;
|
||||||
|
p.enable_scanlines = preset.enable_scanlines;
|
||||||
|
p.enable_multisample = preset.enable_multisample;
|
||||||
|
p.enable_gamma = preset.enable_gamma;
|
||||||
|
p.enable_curvature = preset.enable_curvature;
|
||||||
|
p.enable_sharper = preset.enable_sharper;
|
||||||
|
shader_backend_->setCrtPiParams(p);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Screen::getCurrentPresetName() const -> const char * {
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return "---"; }
|
||||||
|
if (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX) {
|
||||||
|
if (Options::current_postfx_preset >= 0 && Options::current_postfx_preset < static_cast<int>(Options::postfx_presets.size())) {
|
||||||
|
return Options::postfx_presets[Options::current_postfx_preset].name.c_str();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (Options::current_crtpi_preset >= 0 && Options::current_crtpi_preset < static_cast<int>(Options::crtpi_presets.size())) {
|
||||||
|
return Options::crtpi_presets[Options::current_crtpi_preset].name.c_str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return "---";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Screen::nextPreset() -> bool {
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return false; }
|
||||||
|
if (!Options::video.shader.enabled) { return false; }
|
||||||
|
|
||||||
|
if (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX) {
|
||||||
|
if (Options::postfx_presets.empty()) { return false; }
|
||||||
|
const int N = static_cast<int>(Options::postfx_presets.size());
|
||||||
|
Options::current_postfx_preset = (Options::current_postfx_preset + 1) % N;
|
||||||
|
Options::video.shader.current_postfx_preset_name =
|
||||||
|
Options::postfx_presets[Options::current_postfx_preset].name;
|
||||||
|
applyCurrentPostFXPreset();
|
||||||
|
} else {
|
||||||
|
if (Options::crtpi_presets.empty()) { return false; }
|
||||||
|
const int N = static_cast<int>(Options::crtpi_presets.size());
|
||||||
|
Options::current_crtpi_preset = (Options::current_crtpi_preset + 1) % N;
|
||||||
|
Options::video.shader.current_crtpi_preset_name =
|
||||||
|
Options::crtpi_presets[Options::current_crtpi_preset].name;
|
||||||
|
applyCurrentCrtPiPreset();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Screen::prevPreset() -> bool {
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) { return false; }
|
||||||
|
if (!Options::video.shader.enabled) { return false; }
|
||||||
|
|
||||||
|
if (Options::video.shader.current_shader == Rendering::ShaderType::POSTFX) {
|
||||||
|
if (Options::postfx_presets.empty()) { return false; }
|
||||||
|
const int N = static_cast<int>(Options::postfx_presets.size());
|
||||||
|
Options::current_postfx_preset = (Options::current_postfx_preset - 1 + N) % N;
|
||||||
|
Options::video.shader.current_postfx_preset_name =
|
||||||
|
Options::postfx_presets[Options::current_postfx_preset].name;
|
||||||
|
applyCurrentPostFXPreset();
|
||||||
|
} else {
|
||||||
|
if (Options::crtpi_presets.empty()) { return false; }
|
||||||
|
const int N = static_cast<int>(Options::crtpi_presets.size());
|
||||||
|
Options::current_crtpi_preset = (Options::current_crtpi_preset - 1 + N) % N;
|
||||||
|
Options::video.shader.current_crtpi_preset_name =
|
||||||
|
Options::crtpi_presets[Options::current_crtpi_preset].name;
|
||||||
|
applyCurrentCrtPiPreset();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <memory> // for unique_ptr
|
||||||
|
#include <string> // for string
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include "game/options.hpp" // for Options::PresentationMode
|
||||||
|
#include "utils/utils.h" // for Color
|
||||||
|
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
#include "core/rendering/shader_backend.hpp" // for Rendering::ShaderType
|
||||||
|
namespace Rendering {
|
||||||
|
class ShaderBackend;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class Text;
|
||||||
|
|
||||||
|
class Screen {
|
||||||
|
public:
|
||||||
|
// Constantes
|
||||||
|
static constexpr int WINDOW_ZOOM_MIN = 1;
|
||||||
|
// Pixels reservats per a la barra de títol/decoracions a l'hora de
|
||||||
|
// calcular el zoom màxim windowed (mateix valor que CCAE/jaildoctors).
|
||||||
|
static constexpr int WINDOWS_DECORATIONS = 35;
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
// En WASM el tamaño de ventana está fijado a 1x, así que escalamos el
|
||||||
|
// renderizado por 3 aprovechando el modo NEAREST de la textura del juego
|
||||||
|
// para que los píxeles salgan nítidos.
|
||||||
|
static constexpr int WASM_RENDER_SCALE = 3;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Singleton API
|
||||||
|
static void init(SDL_Window *window, SDL_Renderer *renderer); // Crea la instancia
|
||||||
|
static void destroy(); // Libera la instancia
|
||||||
|
static auto get() -> Screen *; // Obtiene el puntero a la instancia
|
||||||
|
|
||||||
|
// Detecta el zoom màxim windowed segons la resolució del display actual.
|
||||||
|
// Cal cridar-la després de SDL_Init(VIDEO) i abans de crear la finestra.
|
||||||
|
// Escriu a `Options::window.max_zoom` i clampa `Options::window.zoom`.
|
||||||
|
// En Emscripten és no-op (el tamany del canvas el controla el browser).
|
||||||
|
static void detectMaxZoom();
|
||||||
|
|
||||||
|
// Destructor (público por requisitos de `delete` desde destroy())
|
||||||
|
~Screen();
|
||||||
|
|
||||||
|
// Render loop
|
||||||
|
void clean(Color color = {0x00, 0x00, 0x00}); // Limpia la pantalla
|
||||||
|
void start(); // Prepara para empezar a dibujar en la textura de juego
|
||||||
|
void blit(); // Vuelca el contenido del renderizador en pantalla
|
||||||
|
|
||||||
|
// Video y ventana
|
||||||
|
void setVideoMode(bool fullscreen); // Establece el modo de video
|
||||||
|
void toggleVideoMode(); // Cambia entre pantalla completa y ventana
|
||||||
|
void handleCanvasResized(); // En Emscripten, reaplica setVideoMode tras un cambio del navegador (salida de fullscreen con Esc, rotación). No-op fuera de Emscripten
|
||||||
|
static void syncFullscreenFlagFromBrowser(bool is_fullscreen); // Sincroniza el flag interno de fullscreen con el estado real del navegador. Debe llamarse antes de diferir handleCanvasResized. No-op fuera de Emscripten
|
||||||
|
void nextPresentationMode(); // Cicla integer_scale -> letterbox -> stretched -> overscan
|
||||||
|
void setPresentationMode(Options::PresentationMode mode); // Estableix el mode de presentacio del canvas
|
||||||
|
[[nodiscard]] static auto getPresentationModeName() -> const char *; // Nom curt del mode actual (per a notificacions)
|
||||||
|
void toggleVSync(); // Alterna el V-Sync
|
||||||
|
void setVSync(bool enabled); // Establece el V-Sync
|
||||||
|
auto decWindowZoom() -> bool; // Reduce el zoom de la ventana (devuelve true si cambió)
|
||||||
|
auto incWindowZoom() -> bool; // Aumenta el zoom de la ventana (devuelve true si cambió)
|
||||||
|
auto setWindowZoom(int zoom) -> bool; // Establece el zoom de la ventana (devuelve true si cambió)
|
||||||
|
|
||||||
|
// Borde
|
||||||
|
void setBorderColor(Color color); // Cambia el color del borde
|
||||||
|
|
||||||
|
// Notificaciones
|
||||||
|
void initNotifications(); // Enllaça el Text de notificacions amb `Resource`. A cridar després de `Resource::init(...)`.
|
||||||
|
void notify(const std::string &text, Color text_color, Color outline_color, Uint32 duration_ms); // Muestra una notificación en la línea superior del canvas durante durationMs. Sobrescribe cualquier notificación activa (sin apilación).
|
||||||
|
void clearNotification(); // Limpia la notificación actual
|
||||||
|
|
||||||
|
// FPS overlay (debug, no persistent)
|
||||||
|
void toggleFps(); // Alterna la visibilitat de l'overlay de FPS
|
||||||
|
[[nodiscard]] auto isFpsVisible() const -> bool; // Estat actual
|
||||||
|
|
||||||
|
// GPU / shaders (post-procesado). En builds con NO_SHADERS (Emscripten) son no-op.
|
||||||
|
void initShaders(); // Crea el backend GPU si no existe y lo inicializa
|
||||||
|
void shutdownShaders(); // Libera el backend GPU
|
||||||
|
[[nodiscard]] auto isGpuAccelerated() const -> bool; // true si el backend existe y reporta hardware OK
|
||||||
|
void setShaderEnabled(bool enabled); // Activa o desactiva el post-procesado (persiste)
|
||||||
|
void toggleShaderEnabled(); // Alterna post-procesado
|
||||||
|
[[nodiscard]] static auto isShaderEnabled() -> bool; // Estado actual (lee options)
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
void setActiveShader(Rendering::ShaderType type); // POSTFX o CRTPI
|
||||||
|
[[nodiscard]] static auto getActiveShader() -> Rendering::ShaderType;
|
||||||
|
#endif
|
||||||
|
void toggleActiveShader(); // Alterna POSTFX ↔ CRTPI
|
||||||
|
|
||||||
|
// Presets de shaders (PostFX/CrtPi). Operen sobre el shader actiu.
|
||||||
|
// Retornen false si GPU off / shaders off / llista buida (igual que a aee_plus).
|
||||||
|
auto nextPreset() -> bool;
|
||||||
|
auto prevPreset() -> bool;
|
||||||
|
[[nodiscard]] auto getCurrentPresetName() const -> const char *;
|
||||||
|
void applyCurrentPostFXPreset(); // Escriu el preset PostFX actiu al backend
|
||||||
|
void applyCurrentCrtPiPreset(); // Escriu el preset CrtPi actiu al backend
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Constructor privado (usar Screen::init)
|
||||||
|
Screen(SDL_Window *window, SDL_Renderer *renderer);
|
||||||
|
|
||||||
|
// Instancia única
|
||||||
|
static Screen *instance;
|
||||||
|
|
||||||
|
// Helpers internos de setVideoMode
|
||||||
|
void applyFullscreen(bool fullscreen); // SDL_SetWindowFullscreen + visibilidad del cursor
|
||||||
|
void applyWindowedLayout(); // Calcula windowWidth/Height/dest + SDL_SetWindowSize + SDL_SetWindowPosition
|
||||||
|
void applyFullscreenLayout(); // SDL_GetWindowSize + delegación a computeFullscreenGameRect
|
||||||
|
void computeFullscreenGameRect(); // Calcula dest en fullscreen (integerScale / keepAspect / stretched)
|
||||||
|
void applyLogicalPresentation(bool fullscreen); // SDL_SetRenderLogicalPresentation + persistencia a options
|
||||||
|
|
||||||
|
// Emscripten
|
||||||
|
void registerEmscriptenEventCallbacks(); // Registra los callbacks nativos de Emscripten para fullscreenchange y orientationchange. No-op fuera de Emscripten
|
||||||
|
|
||||||
|
// Notificaciones
|
||||||
|
void renderNotification(); // Dibuja la notificación activa (si la hay) sobre el gameCanvas
|
||||||
|
[[nodiscard]] auto safeNotificationY() const -> int; // Y minima dins del canvas que segueix sent visible en overscan (segons aspect ratio finestra/canvas)
|
||||||
|
|
||||||
|
// FPS overlay
|
||||||
|
void updateFps(); // Acumula temps i recalcula fps cada segon (a cridar des de blit)
|
||||||
|
void renderFps(); // Dibuixa "NN FPS" a dalt a la dreta del canvas
|
||||||
|
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
// Aplica els paràmetres actuals del shader al backend segons options
|
||||||
|
// (pass-through si `videoShaderEnabled==false`, preset per defecte si true).
|
||||||
|
void applyShaderParams();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Objetos y punteros
|
||||||
|
SDL_Window *window_; // Ventana de la aplicación
|
||||||
|
SDL_Renderer *renderer_; // El renderizador de la ventana
|
||||||
|
SDL_Texture *game_canvas_; // Textura para completar la ventana de juego hasta la pantalla completa
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
int window_width_; // Ancho de la pantalla o ventana
|
||||||
|
int window_height_; // Alto de la pantalla o ventana
|
||||||
|
int game_canvas_width_; // Resolución interna del juego. Es el ancho de la textura donde se dibuja el juego
|
||||||
|
int game_canvas_height_; // Resolución interna del juego. Es el alto de la textura donde se dibuja el juego
|
||||||
|
SDL_Rect dest_; // Coordenadas donde se va a dibujar la textura del juego sobre la pantalla o ventana
|
||||||
|
Color border_color_; // Color del borde añadido a la textura de juego para rellenar la pantalla
|
||||||
|
|
||||||
|
// Notificaciones - una sola activa, sin apilación ni animaciones
|
||||||
|
Text *notification_text_; // Fuente 8bithud dedicada a las notificaciones
|
||||||
|
std::string notification_message_; // Texto a mostrar
|
||||||
|
Color notification_text_color_; // Color del texto
|
||||||
|
Color notification_outline_color_; // Color del outline
|
||||||
|
Uint32 notification_end_time_; // SDL_GetTicks() hasta el cual se muestra
|
||||||
|
int notification_y_; // Fila vertical en el canvas virtual
|
||||||
|
|
||||||
|
// FPS overlay (debug, no persistent)
|
||||||
|
bool fps_visible_{false}; // F10 toggle
|
||||||
|
int fps_value_{0}; // Ultim valor calculat (frames per segon)
|
||||||
|
int fps_frame_count_{0}; // Frames acumulats durant la finestra actual
|
||||||
|
Uint32 fps_window_start_ticks_{0}; // Inici de la finestra d'1s actual (SDL_GetTicks)
|
||||||
|
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
// GPU / shaders
|
||||||
|
std::unique_ptr<Rendering::ShaderBackend> shader_backend_; // Backend GPU (nullptr si inactivo)
|
||||||
|
std::vector<Uint32> pixel_buffer_; // Buffer de readback del gameCanvas (ARGB8888)
|
||||||
|
#endif
|
||||||
|
};
|
||||||
@@ -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__
|
||||||
@@ -0,0 +1,651 @@
|
|||||||
|
#include "core/rendering/sdl3gpu/sdl3gpu_shader.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL_log.h>
|
||||||
|
|
||||||
|
#include <algorithm> // std::min, std::max, std::floor
|
||||||
|
#include <cmath> // std::floor
|
||||||
|
#include <cstring> // memcpy, strlen
|
||||||
|
#include <iostream> // std::cout
|
||||||
|
|
||||||
|
#ifndef __APPLE__
|
||||||
|
#include "core/rendering/sdl3gpu/spv/crtpi_frag_spv.h"
|
||||||
|
#include "core/rendering/sdl3gpu/spv/postfx_frag_spv.h"
|
||||||
|
#include "core/rendering/sdl3gpu/spv/postfx_vert_spv.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
#include "core/rendering/sdl3gpu/msl/crtpi_frag.msl.h"
|
||||||
|
#include "core/rendering/sdl3gpu/msl/postfx_frag.msl.h"
|
||||||
|
#include "core/rendering/sdl3gpu/msl/postfx_vert.msl.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Rendering {
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Destructor
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
SDL3GPUShader::~SDL3GPUShader() {
|
||||||
|
destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// init
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
auto SDL3GPUShader::init(SDL_Window* window,
|
||||||
|
SDL_Texture* texture,
|
||||||
|
const std::string& /*vertex_source*/,
|
||||||
|
const std::string& /*fragment_source*/) -> bool {
|
||||||
|
// Si ya estaba inicializado (p.ej. al cambiar borde), liberar recursos
|
||||||
|
// de textura/pipeline pero mantener el device vivo para evitar conflictos
|
||||||
|
// con SDL_Renderer en Windows/Vulkan.
|
||||||
|
if (is_initialized_) {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
window_ = window;
|
||||||
|
|
||||||
|
// Dimensions from the SDL_Texture placeholder
|
||||||
|
float fw = 0.0F;
|
||||||
|
float fh = 0.0F;
|
||||||
|
SDL_GetTextureSize(texture, &fw, &fh);
|
||||||
|
game_width_ = static_cast<int>(fw);
|
||||||
|
game_height_ = static_cast<int>(fh);
|
||||||
|
uniforms_.screen_height = static_cast<float>(game_height_);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 1. Create GPU device (solo si no existe ya)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
if (preferred_driver_ == "none") {
|
||||||
|
SDL_Log("SDL3GPUShader: GPU disabled by config, using SDL_Renderer fallback");
|
||||||
|
driver_name_ = ""; // vacío → RenderInfo mostrará "sdl"
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (device_ == nullptr) {
|
||||||
|
#ifdef __APPLE__
|
||||||
|
const SDL_GPUShaderFormat PREFERRED = SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_METALLIB;
|
||||||
|
#else
|
||||||
|
const SDL_GPUShaderFormat PREFERRED = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||||
|
#endif
|
||||||
|
const char* preferred = preferred_driver_.empty() ? nullptr : preferred_driver_.c_str();
|
||||||
|
device_ = SDL_CreateGPUDevice(PREFERRED, false, preferred);
|
||||||
|
if (device_ == nullptr && preferred != nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: driver '%s' not available, falling back to auto", preferred);
|
||||||
|
device_ = SDL_CreateGPUDevice(PREFERRED, false, nullptr);
|
||||||
|
}
|
||||||
|
if (device_ == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: SDL_CreateGPUDevice failed: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
driver_name_ = SDL_GetGPUDeviceDriver(device_);
|
||||||
|
std::cout << "GPU Driver : " << driver_name_ << '\n';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 2. Claim window (una sola vez — no liberar hasta destroy())
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
|
||||||
|
SDL_Log("SDL3GPUShader: SDL_ClaimWindowForGPUDevice failed: %s", SDL_GetError());
|
||||||
|
SDL_DestroyGPUDevice(device_);
|
||||||
|
device_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 3. Create scene texture (upload target, always game resolution)
|
||||||
|
// Format: B8G8R8A8_UNORM matches SDL ARGB8888 byte layout on LE
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
SDL_GPUTextureCreateInfo tex_info = {};
|
||||||
|
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||||
|
tex_info.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
|
||||||
|
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||||
|
tex_info.width = static_cast<Uint32>(game_width_);
|
||||||
|
tex_info.height = static_cast<Uint32>(game_height_);
|
||||||
|
tex_info.layer_count_or_depth = 1;
|
||||||
|
tex_info.num_levels = 1;
|
||||||
|
scene_texture_ = SDL_CreateGPUTexture(device_, &tex_info);
|
||||||
|
if (scene_texture_ == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: failed to create scene texture: %s", SDL_GetError());
|
||||||
|
cleanup();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 4. Create upload transfer buffer (CPU → GPU, always game resolution)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
SDL_GPUTransferBufferCreateInfo tb_info = {};
|
||||||
|
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||||
|
tb_info.size = static_cast<Uint32>(game_width_ * game_height_ * 4);
|
||||||
|
upload_buffer_ = SDL_CreateGPUTransferBuffer(device_, &tb_info);
|
||||||
|
if (upload_buffer_ == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: failed to create upload buffer: %s", SDL_GetError());
|
||||||
|
cleanup();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 5. Create sampler: NEAREST (pixel art)
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
SDL_GPUSamplerCreateInfo samp_info = {};
|
||||||
|
samp_info.min_filter = SDL_GPU_FILTER_NEAREST;
|
||||||
|
samp_info.mag_filter = SDL_GPU_FILTER_NEAREST;
|
||||||
|
samp_info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
|
||||||
|
samp_info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||||
|
samp_info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||||
|
samp_info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
|
||||||
|
sampler_ = SDL_CreateGPUSampler(device_, &samp_info);
|
||||||
|
if (sampler_ == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: failed to create sampler: %s", SDL_GetError());
|
||||||
|
cleanup();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 6. Create PostFX graphics pipeline
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
if (!createPipeline()) {
|
||||||
|
cleanup();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
// 7. Create CrtPi graphics pipeline
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
if (!createCrtPiPipeline()) {
|
||||||
|
cleanup();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
is_initialized_ = true;
|
||||||
|
std::cout << "GPU Shader : initialized OK — game " << game_width_ << 'x' << game_height_ << '\n';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// createPostfxVertexShader — fullscreen-triangle vertex compartit per tots els pipelines
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
auto SDL3GPUShader::createPostfxVertexShader() -> SDL_GPUShader* {
|
||||||
|
#ifdef __APPLE__
|
||||||
|
return createShaderMSL(device_, Msl::kPostfxVert, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||||
|
#else
|
||||||
|
return createShaderSPIRV(device_, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// createPostfxLikePipeline — empaqueta vert(postfx) + frag dado + target en un pipeline.
|
||||||
|
// Pren ownership de `frag` (el libera abans de retornar).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
auto SDL3GPUShader::createPostfxLikePipeline(SDL_GPUShader* frag, SDL_GPUTextureFormat format, const char* debug_name) -> SDL_GPUGraphicsPipeline* {
|
||||||
|
if (frag == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: %s frag shader is null", debug_name);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
SDL_GPUShader* vert = createPostfxVertexShader();
|
||||||
|
if (vert == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: %s vert shader creation failed", debug_name);
|
||||||
|
SDL_ReleaseGPUShader(device_, frag);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_GPUColorTargetBlendState no_blend = {};
|
||||||
|
no_blend.enable_blend = false;
|
||||||
|
no_blend.enable_color_write_mask = false;
|
||||||
|
|
||||||
|
SDL_GPUColorTargetDescription color_target = {};
|
||||||
|
color_target.format = format;
|
||||||
|
color_target.blend_state = no_blend;
|
||||||
|
|
||||||
|
SDL_GPUVertexInputState no_input = {};
|
||||||
|
|
||||||
|
SDL_GPUGraphicsPipelineCreateInfo info = {};
|
||||||
|
info.vertex_shader = vert;
|
||||||
|
info.fragment_shader = frag;
|
||||||
|
info.vertex_input_state = no_input;
|
||||||
|
info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
|
||||||
|
info.target_info.num_color_targets = 1;
|
||||||
|
info.target_info.color_target_descriptions = &color_target;
|
||||||
|
|
||||||
|
SDL_GPUGraphicsPipeline* pipeline = SDL_CreateGPUGraphicsPipeline(device_, &info);
|
||||||
|
|
||||||
|
SDL_ReleaseGPUShader(device_, vert);
|
||||||
|
SDL_ReleaseGPUShader(device_, frag);
|
||||||
|
|
||||||
|
if (pipeline == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: %s pipeline creation failed: %s", debug_name, SDL_GetError());
|
||||||
|
}
|
||||||
|
return pipeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// createPipeline — crea el pipeline PostFX que va directament al swapchain
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
auto SDL3GPUShader::createPipeline() -> bool {
|
||||||
|
const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
SDL_GPUShader* postfx_frag = createShaderMSL(device_, Msl::kPostfxFrag, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||||
|
#else
|
||||||
|
SDL_GPUShader* postfx_frag = createShaderSPIRV(device_, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
pipeline_ = createPostfxLikePipeline(postfx_frag, SWAPCHAIN_FMT, "PostFX");
|
||||||
|
return pipeline_ != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// createCrtPiPipeline — pipeline dedicado para el shader CRT-Pi.
|
||||||
|
// Usa el mismo vertex shader que postfx (fullscreen-triangle genérico).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
auto SDL3GPUShader::createCrtPiPipeline() -> bool {
|
||||||
|
const SDL_GPUTextureFormat SWAPCHAIN_FMT = SDL_GetGPUSwapchainTextureFormat(device_, window_);
|
||||||
|
#ifdef __APPLE__
|
||||||
|
SDL_GPUShader* frag = createShaderMSL(device_, Msl::kCrtpiFrag, "crtpi_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||||
|
#else
|
||||||
|
SDL_GPUShader* frag = createShaderSPIRV(device_, kcrtpi_frag_spv, kcrtpi_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
|
||||||
|
#endif
|
||||||
|
crtpi_pipeline_ = createPostfxLikePipeline(frag, SWAPCHAIN_FMT, "CrtPi");
|
||||||
|
return crtpi_pipeline_ != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// uploadPixels — copies ARGB8888 CPU pixels into the GPU transfer buffer.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void SDL3GPUShader::uploadPixels(const Uint32* pixels, int width, int height) {
|
||||||
|
if (!is_initialized_ || (upload_buffer_ == nullptr)) { return; }
|
||||||
|
|
||||||
|
void* mapped = SDL_MapGPUTransferBuffer(device_, upload_buffer_, false);
|
||||||
|
if (mapped == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: SDL_MapGPUTransferBuffer failed: %s", SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(mapped, pixels, static_cast<size_t>(width) * height * 4);
|
||||||
|
|
||||||
|
SDL_UnmapGPUTransferBuffer(device_, upload_buffer_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// uploadSceneTexture — copy pass: transfer buffer → scene texture
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void SDL3GPUShader::uploadSceneTexture(SDL_GPUCommandBuffer* cmd) {
|
||||||
|
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd);
|
||||||
|
if (copy == nullptr) { return; }
|
||||||
|
|
||||||
|
SDL_GPUTextureTransferInfo src = {};
|
||||||
|
src.transfer_buffer = upload_buffer_;
|
||||||
|
src.offset = 0;
|
||||||
|
src.pixels_per_row = static_cast<Uint32>(game_width_);
|
||||||
|
src.rows_per_layer = static_cast<Uint32>(game_height_);
|
||||||
|
|
||||||
|
SDL_GPUTextureRegion dst = {};
|
||||||
|
dst.texture = scene_texture_;
|
||||||
|
dst.w = static_cast<Uint32>(game_width_);
|
||||||
|
dst.h = static_cast<Uint32>(game_height_);
|
||||||
|
dst.d = 1;
|
||||||
|
|
||||||
|
SDL_UploadToGPUTexture(copy, &src, &dst, false);
|
||||||
|
SDL_EndGPUCopyPass(copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// computeViewport — dimensions lògiques del canvas dins del swapchain (letterbox)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
auto SDL3GPUShader::computeViewport(Uint32 sw, Uint32 sh) const -> Viewport {
|
||||||
|
const auto SWF = static_cast<float>(sw);
|
||||||
|
const auto SHF = static_cast<float>(sh);
|
||||||
|
const float CANVAS_RATIO = static_cast<float>(game_width_) / static_cast<float>(game_height_);
|
||||||
|
const float WINDOW_RATIO = SWF / SHF;
|
||||||
|
|
||||||
|
float vw = 0.0F;
|
||||||
|
float vh = 0.0F;
|
||||||
|
switch (presentation_mode_) {
|
||||||
|
case PresentationMode::INTEGER_SCALE: {
|
||||||
|
const int SCALE = std::max(1, std::min(static_cast<int>(sw) / game_width_, static_cast<int>(sh) / game_height_));
|
||||||
|
vw = static_cast<float>(game_width_ * SCALE);
|
||||||
|
vh = static_cast<float>(game_height_ * SCALE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PresentationMode::LETTERBOX: {
|
||||||
|
if (WINDOW_RATIO >= CANVAS_RATIO) {
|
||||||
|
vh = SHF;
|
||||||
|
vw = SHF * CANVAS_RATIO;
|
||||||
|
} else {
|
||||||
|
vw = SWF;
|
||||||
|
vh = SWF / CANVAS_RATIO;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PresentationMode::STRETCHED: {
|
||||||
|
vw = SWF;
|
||||||
|
vh = SHF;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PresentationMode::OVERSCAN: {
|
||||||
|
if (WINDOW_RATIO >= CANVAS_RATIO) {
|
||||||
|
vw = SWF;
|
||||||
|
vh = SWF / CANVAS_RATIO;
|
||||||
|
} else {
|
||||||
|
vh = SHF;
|
||||||
|
vw = SHF * CANVAS_RATIO;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const float VX = std::floor((SWF - vw) * 0.5F);
|
||||||
|
const float VY = std::floor((SHF - vh) * 0.5F);
|
||||||
|
return {.x = VX, .y = VY, .w = vw, .h = vh};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// updateDynamicUniforms — actualitza pixel_scale i time per a aquest frame
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void SDL3GPUShader::updateDynamicUniforms(float viewport_h) {
|
||||||
|
uniforms_.pixel_scale = (game_height_ > 0) ? (viewport_h / static_cast<float>(game_height_)) : 1.0F;
|
||||||
|
uniforms_.time = static_cast<float>(SDL_GetTicks()) / 1000.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// runCrtPiPass — scene_texture_ → swapchain via pipeline CrtPi (sense SS ni Lanczos)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void SDL3GPUShader::runCrtPiPass(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchain, const Viewport& vp) {
|
||||||
|
SDL_GPUColorTargetInfo color_target = {};
|
||||||
|
color_target.texture = swapchain;
|
||||||
|
color_target.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||||
|
color_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||||
|
color_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
||||||
|
|
||||||
|
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr);
|
||||||
|
if (pass == nullptr) { return; }
|
||||||
|
SDL_BindGPUGraphicsPipeline(pass, crtpi_pipeline_);
|
||||||
|
SDL_GPUViewport sdlvp = {.x = vp.x, .y = vp.y, .w = vp.w, .h = vp.h, .min_depth = 0.0F, .max_depth = 1.0F};
|
||||||
|
SDL_SetGPUViewport(pass, &sdlvp);
|
||||||
|
|
||||||
|
SDL_GPUTextureSamplerBinding binding = {};
|
||||||
|
binding.texture = scene_texture_;
|
||||||
|
binding.sampler = sampler_; // NEAREST: el shader CrtPi fa el seu filtrat analític
|
||||||
|
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
|
||||||
|
|
||||||
|
crtpi_uniforms_.texture_width = static_cast<float>(game_width_);
|
||||||
|
crtpi_uniforms_.texture_height = static_cast<float>(game_height_);
|
||||||
|
SDL_PushGPUFragmentUniformData(cmd, 0, &crtpi_uniforms_, sizeof(CrtPiUniforms));
|
||||||
|
|
||||||
|
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
|
||||||
|
SDL_EndGPURenderPass(pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// runDirectPostfxPass — PostFX → swapchain directament
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void SDL3GPUShader::runDirectPostfxPass(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchain, const Viewport& vp) {
|
||||||
|
SDL_GPUColorTargetInfo color_target = {};
|
||||||
|
color_target.texture = swapchain;
|
||||||
|
color_target.load_op = SDL_GPU_LOADOP_CLEAR;
|
||||||
|
color_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||||
|
color_target.clear_color = {.r = 0.0F, .g = 0.0F, .b = 0.0F, .a = 1.0F};
|
||||||
|
|
||||||
|
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &color_target, 1, nullptr);
|
||||||
|
if (pass == nullptr) { return; }
|
||||||
|
SDL_BindGPUGraphicsPipeline(pass, pipeline_);
|
||||||
|
SDL_GPUViewport sdlvp = {.x = vp.x, .y = vp.y, .w = vp.w, .h = vp.h, .min_depth = 0.0F, .max_depth = 1.0F};
|
||||||
|
SDL_SetGPUViewport(pass, &sdlvp);
|
||||||
|
|
||||||
|
SDL_GPUTextureSamplerBinding binding = {};
|
||||||
|
binding.texture = scene_texture_;
|
||||||
|
binding.sampler = sampler_;
|
||||||
|
SDL_BindGPUFragmentSamplers(pass, 0, &binding, 1);
|
||||||
|
|
||||||
|
SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms_, sizeof(PostFXUniforms));
|
||||||
|
SDL_DrawGPUPrimitives(pass, 3, 1, 0, 0);
|
||||||
|
SDL_EndGPURenderPass(pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// render — orquestra upload + path PostFX (CrtPi / direct)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void SDL3GPUShader::render() {
|
||||||
|
if (!is_initialized_) { return; }
|
||||||
|
|
||||||
|
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
|
||||||
|
if (cmd == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadSceneTexture(cmd);
|
||||||
|
|
||||||
|
SDL_GPUTexture* swapchain = nullptr;
|
||||||
|
Uint32 sw = 0;
|
||||||
|
Uint32 sh = 0;
|
||||||
|
if (!SDL_AcquireGPUSwapchainTexture(cmd, window_, &swapchain, &sw, &sh)) {
|
||||||
|
SDL_Log("SDL3GPUShader: SDL_AcquireGPUSwapchainTexture failed: %s", SDL_GetError());
|
||||||
|
SDL_SubmitGPUCommandBuffer(cmd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (swapchain == nullptr) {
|
||||||
|
// Finestra minimitzada — saltem el frame
|
||||||
|
SDL_SubmitGPUCommandBuffer(cmd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Viewport VP = computeViewport(sw, sh);
|
||||||
|
updateDynamicUniforms(VP.h);
|
||||||
|
|
||||||
|
if (active_shader_ == ShaderType::CRTPI && crtpi_pipeline_ != nullptr) {
|
||||||
|
runCrtPiPass(cmd, swapchain, VP);
|
||||||
|
} else {
|
||||||
|
runDirectPostfxPass(cmd, swapchain, VP);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SubmitGPUCommandBuffer(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// cleanup — libera pipeline/texturas/buffer pero mantiene device + swapchain
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void SDL3GPUShader::cleanup() {
|
||||||
|
is_initialized_ = false;
|
||||||
|
|
||||||
|
if (device_ != nullptr) {
|
||||||
|
SDL_WaitForGPUIdle(device_);
|
||||||
|
|
||||||
|
if (pipeline_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_);
|
||||||
|
pipeline_ = nullptr;
|
||||||
|
}
|
||||||
|
if (crtpi_pipeline_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUGraphicsPipeline(device_, crtpi_pipeline_);
|
||||||
|
crtpi_pipeline_ = nullptr;
|
||||||
|
}
|
||||||
|
if (scene_texture_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
||||||
|
scene_texture_ = nullptr;
|
||||||
|
}
|
||||||
|
if (upload_buffer_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_);
|
||||||
|
upload_buffer_ = nullptr;
|
||||||
|
}
|
||||||
|
if (sampler_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUSampler(device_, sampler_);
|
||||||
|
sampler_ = nullptr;
|
||||||
|
}
|
||||||
|
// device_ y el claim de la ventana se mantienen vivos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// destroy — limpieza completa incluyendo device y swapchain (solo al cerrar)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void SDL3GPUShader::destroy() {
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
if (device_ != nullptr) {
|
||||||
|
if (window_ != nullptr) {
|
||||||
|
SDL_ReleaseWindowFromGPUDevice(device_, window_);
|
||||||
|
}
|
||||||
|
SDL_DestroyGPUDevice(device_);
|
||||||
|
device_ = nullptr;
|
||||||
|
}
|
||||||
|
window_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Shader creation helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
auto SDL3GPUShader::createShaderMSL(SDL_GPUDevice* device,
|
||||||
|
const char* msl_source,
|
||||||
|
const char* entrypoint,
|
||||||
|
SDL_GPUShaderStage stage,
|
||||||
|
Uint32 num_samplers,
|
||||||
|
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
|
||||||
|
SDL_GPUShaderCreateInfo info = {};
|
||||||
|
info.code = reinterpret_cast<const Uint8*>(msl_source);
|
||||||
|
info.code_size = std::strlen(msl_source) + 1;
|
||||||
|
info.entrypoint = entrypoint;
|
||||||
|
info.format = SDL_GPU_SHADERFORMAT_MSL;
|
||||||
|
info.stage = stage;
|
||||||
|
info.num_samplers = num_samplers;
|
||||||
|
info.num_uniform_buffers = num_uniform_buffers;
|
||||||
|
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
|
||||||
|
if (shader == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: MSL shader '%s' failed: %s", entrypoint, SDL_GetError());
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SDL3GPUShader::createShaderSPIRV(SDL_GPUDevice* device,
|
||||||
|
const uint8_t* spv_code,
|
||||||
|
size_t spv_size,
|
||||||
|
const char* entrypoint,
|
||||||
|
SDL_GPUShaderStage stage,
|
||||||
|
Uint32 num_samplers,
|
||||||
|
Uint32 num_uniform_buffers) -> SDL_GPUShader* {
|
||||||
|
SDL_GPUShaderCreateInfo info = {};
|
||||||
|
info.code = spv_code;
|
||||||
|
info.code_size = spv_size;
|
||||||
|
info.entrypoint = entrypoint;
|
||||||
|
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
|
||||||
|
info.stage = stage;
|
||||||
|
info.num_samplers = num_samplers;
|
||||||
|
info.num_uniform_buffers = num_uniform_buffers;
|
||||||
|
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
|
||||||
|
if (shader == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: SPIRV shader '%s' failed: %s", entrypoint, SDL_GetError());
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDL3GPUShader::setPostFXParams(const PostFXParams& p) {
|
||||||
|
uniforms_.vignette_strength = p.vignette;
|
||||||
|
uniforms_.chroma_min = p.chroma_min;
|
||||||
|
uniforms_.chroma_max = p.chroma_max;
|
||||||
|
uniforms_.mask_strength = p.mask;
|
||||||
|
uniforms_.gamma_strength = p.gamma;
|
||||||
|
uniforms_.curvature = p.curvature;
|
||||||
|
uniforms_.bleeding = p.bleeding;
|
||||||
|
uniforms_.flicker = p.flicker;
|
||||||
|
uniforms_.scanline_strength = p.scanlines;
|
||||||
|
uniforms_.scan_dark_ratio = p.scan_dark_ratio;
|
||||||
|
uniforms_.scan_dark_floor = p.scan_dark_floor;
|
||||||
|
uniforms_.scan_edge_soft = p.scan_edge_soft;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDL3GPUShader::setCrtPiParams(const CrtPiParams& p) {
|
||||||
|
crtpi_uniforms_.scanline_weight = p.scanline_weight;
|
||||||
|
crtpi_uniforms_.scanline_gap_brightness = p.scanline_gap_brightness;
|
||||||
|
crtpi_uniforms_.bloom_factor = p.bloom_factor;
|
||||||
|
crtpi_uniforms_.input_gamma = p.input_gamma;
|
||||||
|
crtpi_uniforms_.output_gamma = p.output_gamma;
|
||||||
|
crtpi_uniforms_.mask_brightness = p.mask_brightness;
|
||||||
|
crtpi_uniforms_.curvature_x = p.curvature_x;
|
||||||
|
crtpi_uniforms_.curvature_y = p.curvature_y;
|
||||||
|
crtpi_uniforms_.mask_type = p.mask_type;
|
||||||
|
crtpi_uniforms_.enable_scanlines = p.enable_scanlines ? 1 : 0;
|
||||||
|
crtpi_uniforms_.enable_multisample = p.enable_multisample ? 1 : 0;
|
||||||
|
crtpi_uniforms_.enable_gamma = p.enable_gamma ? 1 : 0;
|
||||||
|
crtpi_uniforms_.enable_curvature = p.enable_curvature ? 1 : 0;
|
||||||
|
crtpi_uniforms_.enable_sharper = p.enable_sharper ? 1 : 0;
|
||||||
|
// texture_width/height se inyectan en render() cada frame
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDL3GPUShader::setActiveShader(ShaderType type) {
|
||||||
|
active_shader_ = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SDL3GPUShader::bestPresentMode(bool vsync) const -> SDL_GPUPresentMode {
|
||||||
|
if (vsync) {
|
||||||
|
return SDL_GPU_PRESENTMODE_VSYNC;
|
||||||
|
}
|
||||||
|
// IMMEDIATE: sin sincronización — el driver puede no soportarlo en Wayland/compositing
|
||||||
|
if (SDL_WindowSupportsGPUPresentMode(device_, window_, SDL_GPU_PRESENTMODE_IMMEDIATE)) {
|
||||||
|
return SDL_GPU_PRESENTMODE_IMMEDIATE;
|
||||||
|
}
|
||||||
|
// MAILBOX: presenta en el siguiente VBlank pero sin bloquear el hilo (triple buffer)
|
||||||
|
if (SDL_WindowSupportsGPUPresentMode(device_, window_, SDL_GPU_PRESENTMODE_MAILBOX)) {
|
||||||
|
SDL_Log("SDL3GPUShader: IMMEDIATE no soportado, usando MAILBOX para VSync desactivado");
|
||||||
|
return SDL_GPU_PRESENTMODE_MAILBOX;
|
||||||
|
}
|
||||||
|
SDL_Log("SDL3GPUShader: IMMEDIATE y MAILBOX no soportados, forzando VSYNC");
|
||||||
|
return SDL_GPU_PRESENTMODE_VSYNC;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDL3GPUShader::setVSync(bool vsync) {
|
||||||
|
vsync_ = vsync;
|
||||||
|
if (device_ != nullptr && window_ != nullptr) {
|
||||||
|
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, bestPresentMode(vsync_));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SDL3GPUShader::setPresentationMode(PresentationMode mode) {
|
||||||
|
presentation_mode_ = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// reinitTexturesAndBuffer — recrea scene_texture_ i upload_buffer_.
|
||||||
|
// No toca pipelines ni samplers.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
auto SDL3GPUShader::reinitTexturesAndBuffer() -> bool {
|
||||||
|
if (device_ == nullptr) { return false; }
|
||||||
|
SDL_WaitForGPUIdle(device_);
|
||||||
|
|
||||||
|
if (scene_texture_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
||||||
|
scene_texture_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upload_buffer_ != nullptr) {
|
||||||
|
SDL_ReleaseGPUTransferBuffer(device_, upload_buffer_);
|
||||||
|
upload_buffer_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
uniforms_.screen_height = static_cast<float>(game_height_);
|
||||||
|
|
||||||
|
SDL_GPUTextureCreateInfo tex_info = {};
|
||||||
|
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
|
||||||
|
tex_info.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
|
||||||
|
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
|
||||||
|
tex_info.width = static_cast<Uint32>(game_width_);
|
||||||
|
tex_info.height = static_cast<Uint32>(game_height_);
|
||||||
|
tex_info.layer_count_or_depth = 1;
|
||||||
|
tex_info.num_levels = 1;
|
||||||
|
scene_texture_ = SDL_CreateGPUTexture(device_, &tex_info);
|
||||||
|
if (scene_texture_ == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: reinit — failed to create scene texture: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_GPUTransferBufferCreateInfo tb_info = {};
|
||||||
|
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
|
||||||
|
tb_info.size = static_cast<Uint32>(game_width_ * game_height_ * 4);
|
||||||
|
upload_buffer_ = SDL_CreateGPUTransferBuffer(device_, &tb_info);
|
||||||
|
if (upload_buffer_ == nullptr) {
|
||||||
|
SDL_Log("SDL3GPUShader: reinit — failed to create upload buffer: %s", SDL_GetError());
|
||||||
|
SDL_ReleaseGPUTexture(device_, scene_texture_);
|
||||||
|
scene_texture_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Rendering
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
|
||||||
|
#include "core/rendering/shader_backend.hpp"
|
||||||
|
|
||||||
|
// PostFX uniforms pushed to fragment stage each frame.
|
||||||
|
// Must match the MSL struct and GLSL uniform block layout.
|
||||||
|
// 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_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)
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// (Downscale removed — el shader PostFX nou filtra scanlines analíticament i no necessita Lanczos.)
|
||||||
|
|
||||||
|
// CrtPi uniforms pushed to fragment stage each frame.
|
||||||
|
// Must match the MSL struct and GLSL uniform block layout.
|
||||||
|
// 14 fields (8 floats + 6 ints) + 2 floats (texture size) = 16 fields = 64 bytes — 4 × 16-byte alignment.
|
||||||
|
struct CrtPiUniforms {
|
||||||
|
// vec4 #0
|
||||||
|
float scanline_weight; // Ajuste gaussiano (default 6.0)
|
||||||
|
float scanline_gap_brightness; // Brillo mínimo entre scanlines (default 0.12)
|
||||||
|
float bloom_factor; // Factor brillo zonas iluminadas (default 3.5)
|
||||||
|
float input_gamma; // Gamma de entrada (default 2.4)
|
||||||
|
// vec4 #1
|
||||||
|
float output_gamma; // Gamma de salida (default 2.2)
|
||||||
|
float mask_brightness; // Brillo sub-píxeles máscara (default 0.80)
|
||||||
|
float curvature_x; // Distorsión barrel X (default 0.05)
|
||||||
|
float curvature_y; // Distorsión barrel Y (default 0.10)
|
||||||
|
// vec4 #2
|
||||||
|
int mask_type; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
|
||||||
|
int enable_scanlines; // 0 = off, 1 = on
|
||||||
|
int enable_multisample; // 0 = off, 1 = on (antialiasing analítico)
|
||||||
|
int enable_gamma; // 0 = off, 1 = on
|
||||||
|
// vec4 #3
|
||||||
|
int enable_curvature; // 0 = off, 1 = on
|
||||||
|
int enable_sharper; // 0 = off, 1 = on
|
||||||
|
float texture_width; // Ancho del canvas en píxeles (inyectado en render)
|
||||||
|
float texture_height; // Alto del canvas en píxeles (inyectado en render)
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace Rendering {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Backend de shaders usando SDL3 GPU API (Metal en macOS, Vulkan/SPIR-V en Win/Linux)
|
||||||
|
*
|
||||||
|
* Reemplaza el backend OpenGL para que los shaders PostFX funcionen en macOS.
|
||||||
|
* Pipeline: Surface pixels (CPU) → SDL_GPUTransferBuffer → SDL_GPUTexture (scene)
|
||||||
|
* → PostFX render pass → swapchain → present
|
||||||
|
*/
|
||||||
|
class SDL3GPUShader : public ShaderBackend {
|
||||||
|
public:
|
||||||
|
SDL3GPUShader() = default;
|
||||||
|
~SDL3GPUShader() override;
|
||||||
|
|
||||||
|
auto init(SDL_Window* window,
|
||||||
|
SDL_Texture* texture,
|
||||||
|
const std::string& vertex_source,
|
||||||
|
const std::string& fragment_source) -> bool override;
|
||||||
|
|
||||||
|
void render() 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_; }
|
||||||
|
[[nodiscard]] auto getDriverName() const -> std::string override { return driver_name_; }
|
||||||
|
|
||||||
|
// Establece el driver GPU preferido (vacío = auto). Debe llamarse antes de init().
|
||||||
|
void setPreferredDriver(const std::string& driver) override { preferred_driver_ = driver; }
|
||||||
|
|
||||||
|
// Sube píxeles ARGB8888 desde CPU; llamado antes de render()
|
||||||
|
void uploadPixels(const Uint32* pixels, int width, int height) override;
|
||||||
|
|
||||||
|
// Actualiza los parámetros de intensidad de los efectos PostFX
|
||||||
|
void setPostFXParams(const PostFXParams& p) override;
|
||||||
|
|
||||||
|
// Activa/desactiva VSync en el swapchain
|
||||||
|
void setVSync(bool vsync) override;
|
||||||
|
|
||||||
|
// Estableix el mode de presentacio del canvas
|
||||||
|
void setPresentationMode(PresentationMode mode) override;
|
||||||
|
|
||||||
|
// Selecciona el shader de post-procesado activo (POSTFX o CRTPI)
|
||||||
|
void setActiveShader(ShaderType type) override;
|
||||||
|
|
||||||
|
// Actualiza los parámetros del shader CRT-Pi
|
||||||
|
void setCrtPiParams(const CrtPiParams& p) override;
|
||||||
|
|
||||||
|
// Devuelve el shader activo
|
||||||
|
[[nodiscard]] auto getActiveShader() const -> ShaderType override { return active_shader_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static auto createShaderMSL(SDL_GPUDevice* device,
|
||||||
|
const char* msl_source,
|
||||||
|
const char* entrypoint,
|
||||||
|
SDL_GPUShaderStage stage,
|
||||||
|
Uint32 num_samplers,
|
||||||
|
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
|
||||||
|
|
||||||
|
static auto createShaderSPIRV(SDL_GPUDevice* device,
|
||||||
|
const uint8_t* spv_code,
|
||||||
|
size_t spv_size,
|
||||||
|
const char* entrypoint,
|
||||||
|
SDL_GPUShaderStage stage,
|
||||||
|
Uint32 num_samplers,
|
||||||
|
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
|
||||||
|
|
||||||
|
auto createPipeline() -> bool;
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
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;
|
||||||
|
std::string driver_name_;
|
||||||
|
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
|
||||||
|
bool is_initialized_ = false;
|
||||||
|
bool vsync_ = true;
|
||||||
|
PresentationMode presentation_mode_ = PresentationMode::INTEGER_SCALE;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Rendering
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
DisableFormat: true
|
||||||
|
SortIncludes: Never
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# source/core/rendering/sdl3gpu/spv/.clang-tidy
|
||||||
|
Checks: '-*'
|
||||||
|
WarningsAsErrors: ''
|
||||||
|
HeaderFilterRegex: ''
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,161 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Rendering {
|
||||||
|
|
||||||
|
/** @brief Identificador del shader de post-procesado activo */
|
||||||
|
enum class ShaderType : std::uint8_t { POSTFX,
|
||||||
|
CRTPI };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parámetros de intensidad de los efectos PostFX
|
||||||
|
* Definido a nivel de namespace para facilitar el uso desde subclases y screen.cpp
|
||||||
|
*/
|
||||||
|
struct PostFXParams {
|
||||||
|
float vignette = 0.0F; // Intensidad de la viñeta
|
||||||
|
float scanlines = 0.0F; // Intensidad de las scanlines
|
||||||
|
// 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
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Parámetros del shader CRT-Pi (algoritmo de scanlines continuas)
|
||||||
|
* Diferente al PostFX: usa pesos gaussianos por distancia subpixel y bloom.
|
||||||
|
*/
|
||||||
|
struct CrtPiParams {
|
||||||
|
float scanline_weight{6.0F}; // Ajuste gaussiano (mayor = scanlines más estrechas)
|
||||||
|
float scanline_gap_brightness{0.12F}; // Brillo mínimo en las ranuras entre scanlines
|
||||||
|
float bloom_factor{3.5F}; // Factor de brillo para zonas iluminadas
|
||||||
|
float input_gamma{2.4F}; // Gamma de entrada (linealización)
|
||||||
|
float output_gamma{2.2F}; // Gamma de salida (codificación)
|
||||||
|
float mask_brightness{0.80F}; // Sub-píxeles tenues en la máscara de fósforo
|
||||||
|
float curvature_x{0.05F}; // Distorsión barrel eje X
|
||||||
|
float curvature_y{0.10F}; // Distorsión barrel eje Y
|
||||||
|
int mask_type{2}; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
|
||||||
|
bool enable_scanlines{true}; // Activar efecto de scanlines
|
||||||
|
bool enable_multisample{true}; // Antialiasing analítico de scanlines
|
||||||
|
bool enable_gamma{true}; // Corrección gamma
|
||||||
|
bool enable_curvature{false}; // Distorsión barrel CRT
|
||||||
|
bool enable_sharper{false}; // Submuestreo más nítido (modo SHARPER)
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Interfaz abstracta para backends de renderizado con shaders
|
||||||
|
*
|
||||||
|
* Esta interfaz define el contrato que todos los backends de shaders
|
||||||
|
* deben cumplir (OpenGL, Metal, Vulkan, etc.)
|
||||||
|
*/
|
||||||
|
class ShaderBackend {
|
||||||
|
public:
|
||||||
|
virtual ~ShaderBackend() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Inicializa el backend de shaders
|
||||||
|
* @param window Ventana SDL
|
||||||
|
* @param texture Textura de backbuffer a la que aplicar shaders
|
||||||
|
* @param vertex_source Código fuente del vertex shader
|
||||||
|
* @param fragment_source Código fuente del fragment shader
|
||||||
|
* @return true si la inicialización fue exitosa
|
||||||
|
*/
|
||||||
|
virtual auto init(SDL_Window* window,
|
||||||
|
SDL_Texture* texture,
|
||||||
|
const std::string& vertex_source,
|
||||||
|
const std::string& fragment_source) -> bool = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Renderiza la textura con los shaders aplicados
|
||||||
|
*/
|
||||||
|
virtual void render() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Establece el tamaño de la textura como parámetro del shader
|
||||||
|
* @param width Ancho de la textura
|
||||||
|
* @param height Alto de la textura
|
||||||
|
*/
|
||||||
|
virtual void setTextureSize(float width, float height) = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Limpia y libera recursos del backend
|
||||||
|
*/
|
||||||
|
virtual void cleanup() = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Sube píxeles ARGB8888 desde la CPU al backend de shaders
|
||||||
|
* Usado por SDL3GPUShader para evitar pasar por SDL_Texture
|
||||||
|
*/
|
||||||
|
virtual void uploadPixels(const Uint32* /*pixels*/, int /*width*/, int /*height*/) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Establece los parámetros de intensidad de los efectos PostFX
|
||||||
|
* @param p Struct con todos los parámetros PostFX
|
||||||
|
*/
|
||||||
|
virtual void setPostFXParams(const PostFXParams& /*p*/) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Activa o desactiva VSync en el swapchain del GPU device
|
||||||
|
*/
|
||||||
|
virtual void setVSync(bool /*vsync*/) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Estableix el mode de presentacio del canvas dins del swapchain.
|
||||||
|
* El backend calcula el viewport en consequencia.
|
||||||
|
*/
|
||||||
|
enum class PresentationMode : std::uint8_t {
|
||||||
|
INTEGER_SCALE,
|
||||||
|
LETTERBOX,
|
||||||
|
STRETCHED,
|
||||||
|
OVERSCAN
|
||||||
|
};
|
||||||
|
virtual void setPresentationMode(PresentationMode /*mode*/) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Verifica si el backend está usando aceleración por hardware
|
||||||
|
* @return true si usa aceleración (OpenGL/Metal/Vulkan)
|
||||||
|
*/
|
||||||
|
[[nodiscard]] virtual auto isHardwareAccelerated() const -> bool = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Nombre del driver GPU activo (p.ej. "vulkan", "metal", "direct3d12")
|
||||||
|
* @return Cadena vacía si no disponible
|
||||||
|
*/
|
||||||
|
[[nodiscard]] virtual auto getDriverName() const -> std::string { return {}; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Establece el driver GPU preferido antes de init().
|
||||||
|
* Vacío = selección automática de SDL. Implementado en SDL3GPUShader.
|
||||||
|
*/
|
||||||
|
virtual void setPreferredDriver(const std::string& /*driver*/) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Selecciona el shader de post-procesado activo (POSTFX o CRTPI).
|
||||||
|
* Debe llamarse antes de render(). No recrea pipelines.
|
||||||
|
*/
|
||||||
|
virtual void setActiveShader(ShaderType /*type*/) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Establece los parámetros del shader CRT-Pi.
|
||||||
|
*/
|
||||||
|
virtual void setCrtPiParams(const CrtPiParams& /*p*/) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Devuelve el shader de post-procesado activo.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] virtual auto getActiveShader() const -> ShaderType { return ShaderType::POSTFX; }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Rendering
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
#include "core/rendering/smartsprite.h"
|
||||||
|
|
||||||
|
#include "core/rendering/movingsprite.h" // for MovingSprite
|
||||||
|
class Texture;
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
SmartSprite::SmartSprite(Texture *texture, SDL_Renderer *renderer) {
|
||||||
|
// Copia punteros
|
||||||
|
setTexture(texture);
|
||||||
|
setRenderer(renderer);
|
||||||
|
|
||||||
|
// Inicializa el objeto
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializa el objeto
|
||||||
|
void SmartSprite::init() {
|
||||||
|
enabled_ = false;
|
||||||
|
enabled_counter_ = 0;
|
||||||
|
on_destination_ = false;
|
||||||
|
dest_x_ = 0;
|
||||||
|
dest_y_ = 0;
|
||||||
|
finished_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// La velocitat i acceleració són en px/s i px/s²; el temps de permanència
|
||||||
|
// després d'arribar al destí ve donat per setRemainingTime().
|
||||||
|
void SmartSprite::update(float dt_s) {
|
||||||
|
if (enabled_) {
|
||||||
|
// NOLINTNEXTLINE(bugprone-parent-virtual-call): salt deliberat a l'avi — SmartSprite hereta d'AnimatedSprite només per reutilitzar API, però no usa animació de frames, així que es salta AnimatedSprite::update() (que cridaria animate())
|
||||||
|
MovingSprite::update(dt_s);
|
||||||
|
|
||||||
|
checkMove();
|
||||||
|
checkFinished(dt_s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pinta el objeto en pantalla
|
||||||
|
void SmartSprite::render() {
|
||||||
|
if (enabled_) {
|
||||||
|
// Muestra el sprite por pantalla
|
||||||
|
MovingSprite::render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto SmartSprite::getEnabledCounter() const -> int {
|
||||||
|
return enabled_counter_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void SmartSprite::setEnabledCounter(int value) {
|
||||||
|
enabled_counter_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time-based: temps de visibilitat post-arribada
|
||||||
|
void SmartSprite::setRemainingTime(float seconds) {
|
||||||
|
remaining_time_s_ = seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto SmartSprite::getRemainingTime() const -> float {
|
||||||
|
return remaining_time_s_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void SmartSprite::setDestX(int x) {
|
||||||
|
dest_x_ = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void SmartSprite::setDestY(int y) {
|
||||||
|
dest_y_ = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto SmartSprite::getDestX() const -> int {
|
||||||
|
return dest_x_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto SmartSprite::getDestY() const -> int {
|
||||||
|
return dest_y_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba el movimiento
|
||||||
|
void SmartSprite::checkMove() {
|
||||||
|
// Comprueba si se desplaza en el eje X hacia la derecha
|
||||||
|
if (getAccelX() > 0 || getVelX() > 0) {
|
||||||
|
// Comprueba si ha llegado al destino
|
||||||
|
if (getPosX() > dest_x_) {
|
||||||
|
// Lo coloca en posición
|
||||||
|
setPosX(dest_x_);
|
||||||
|
|
||||||
|
// Lo detiene
|
||||||
|
setVelX(0.0F);
|
||||||
|
setAccelX(0.0F);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Comprueba si se desplaza en el eje X hacia la izquierda
|
||||||
|
else if (getAccelX() < 0 || getVelX() < 0) {
|
||||||
|
// Comprueba si ha llegado al destino
|
||||||
|
if (getPosX() < dest_x_) {
|
||||||
|
// Lo coloca en posición
|
||||||
|
setPosX(dest_x_);
|
||||||
|
|
||||||
|
// Lo detiene
|
||||||
|
setVelX(0.0F);
|
||||||
|
setAccelX(0.0F);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si se desplaza en el eje Y hacia abajo
|
||||||
|
if (getAccelY() > 0 || getVelY() > 0) {
|
||||||
|
// Comprueba si ha llegado al destino
|
||||||
|
if (getPosY() > dest_y_) {
|
||||||
|
// Lo coloca en posición
|
||||||
|
setPosY(dest_y_);
|
||||||
|
|
||||||
|
// Lo detiene
|
||||||
|
setVelY(0.0F);
|
||||||
|
setAccelY(0.0F);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Comprueba si se desplaza en el eje Y hacia arriba
|
||||||
|
else if (getAccelY() < 0 || getVelY() < 0) {
|
||||||
|
// Comprueba si ha llegado al destino
|
||||||
|
if (getPosY() < dest_y_) {
|
||||||
|
// Lo coloca en posición
|
||||||
|
setPosY(dest_y_);
|
||||||
|
|
||||||
|
// Lo detiene
|
||||||
|
setVelY(0.0F);
|
||||||
|
setAccelY(0.0F);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrementa el temps restant cada crida si està al destí
|
||||||
|
void SmartSprite::checkFinished(float dt_s) {
|
||||||
|
on_destination_ = getPosX() == dest_x_ && getPosY() == dest_y_;
|
||||||
|
|
||||||
|
if (on_destination_) {
|
||||||
|
if (remaining_time_s_ <= 0.0F) {
|
||||||
|
finished_ = true;
|
||||||
|
} else {
|
||||||
|
remaining_time_s_ -= dt_s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto SmartSprite::isOnDestination() const -> bool {
|
||||||
|
return on_destination_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto SmartSprite::hasFinished() const -> bool {
|
||||||
|
return finished_;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include "core/rendering/animatedsprite.h" // for AnimatedSprite
|
||||||
|
class Texture;
|
||||||
|
|
||||||
|
// Clase SmartSprite
|
||||||
|
class SmartSprite : public AnimatedSprite {
|
||||||
|
public:
|
||||||
|
SmartSprite(Texture *texture, SDL_Renderer *renderer); // Constructor
|
||||||
|
|
||||||
|
void init(); // Inicializa el objeto
|
||||||
|
void update(float dt_s) override; // Actualiza la posicion
|
||||||
|
void render() override; // Pinta el objeto en pantalla
|
||||||
|
|
||||||
|
[[nodiscard]] auto getEnabledCounter() const -> int; // Obtiene el valor de la variable
|
||||||
|
void setEnabledCounter(int value); // Establece el valor de la variable
|
||||||
|
void setRemainingTime(float seconds); // Time-based: temps que es queda visible despres d'arribar al desti
|
||||||
|
[[nodiscard]] auto getRemainingTime() const -> float; // Time-based: temps restant
|
||||||
|
void setDestX(int x); // Establece el valor de la variable
|
||||||
|
void setDestY(int y); // Establece el valor de la variable
|
||||||
|
[[nodiscard]] auto getDestX() const -> int; // Obtiene el valor de la variable
|
||||||
|
[[nodiscard]] auto getDestY() const -> int; // Obtiene el valor de la variable
|
||||||
|
[[nodiscard]] auto isOnDestination() const -> bool; // Obtiene el valor de la variable
|
||||||
|
[[nodiscard]] auto hasFinished() const -> bool; // Obtiene el valor de la variable
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Variables
|
||||||
|
bool on_destination_; // Indica si está en el destino
|
||||||
|
int dest_x_; // Posicion de destino en el eje X
|
||||||
|
int dest_y_; // Posicion de destino en el eje Y
|
||||||
|
int enabled_counter_; // Contador (frames, derivat de remaining_time_s_ * 60)
|
||||||
|
float remaining_time_s_{0.0F}; // Temps restant per a deshabilitar-lo
|
||||||
|
bool finished_; // Indica si ya ha terminado
|
||||||
|
|
||||||
|
void checkMove(); // Comprueba el movimiento
|
||||||
|
void checkFinished(float dt_s); // Comprueba si ha terminado
|
||||||
|
};
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
#include "core/rendering/sprite.h"
|
||||||
|
|
||||||
|
#include "core/rendering/texture.h" // for Texture
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Sprite::Sprite(int x, int y, int w, int h, Texture *texture, SDL_Renderer *renderer)
|
||||||
|
: x_(x),
|
||||||
|
y_(y),
|
||||||
|
w_(w),
|
||||||
|
h_(h),
|
||||||
|
renderer_(renderer),
|
||||||
|
texture_(texture),
|
||||||
|
sprite_clip_{0, 0, w, h},
|
||||||
|
enabled_(true) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Sprite::Sprite(SDL_Rect rect, Texture *texture, SDL_Renderer *renderer)
|
||||||
|
: x_(rect.x),
|
||||||
|
y_(rect.y),
|
||||||
|
w_(rect.w),
|
||||||
|
h_(rect.h),
|
||||||
|
renderer_(renderer),
|
||||||
|
texture_(texture),
|
||||||
|
sprite_clip_{0, 0, rect.w, rect.h},
|
||||||
|
enabled_(true) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
Sprite::~Sprite() {
|
||||||
|
texture_ = nullptr;
|
||||||
|
renderer_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Muestra el sprite por pantalla
|
||||||
|
void Sprite::render() {
|
||||||
|
if (enabled_) {
|
||||||
|
texture_->render(renderer_, x_, y_, &sprite_clip_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obten el valor de la variable
|
||||||
|
auto Sprite::getPosX() const -> int {
|
||||||
|
return x_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obten el valor de la variable
|
||||||
|
auto Sprite::getPosY() const -> int {
|
||||||
|
return y_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obten el valor de la variable
|
||||||
|
auto Sprite::getWidth() const -> int {
|
||||||
|
return w_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obten el valor de la variable
|
||||||
|
auto Sprite::getHeight() const -> int {
|
||||||
|
return h_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la posición del objeto
|
||||||
|
void Sprite::setPos(SDL_Rect rect) {
|
||||||
|
this->x_ = rect.x;
|
||||||
|
this->y_ = rect.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Sprite::setPosX(int x) {
|
||||||
|
this->x_ = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Sprite::setPosY(int y) {
|
||||||
|
this->y_ = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Sprite::setWidth(int w) {
|
||||||
|
this->w_ = w;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Sprite::setHeight(int h) {
|
||||||
|
this->h_ = h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obten el valor de la variable
|
||||||
|
auto Sprite::getSpriteClip() -> SDL_Rect {
|
||||||
|
return sprite_clip_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Sprite::setSpriteClip(SDL_Rect rect) {
|
||||||
|
sprite_clip_ = rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Sprite::setSpriteClip(int x, int y, int w, int h) {
|
||||||
|
sprite_clip_ = {.x = x, .y = y, .w = w, .h = h};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obten el valor de la variable
|
||||||
|
auto Sprite::getTexture() -> Texture * {
|
||||||
|
return texture_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Sprite::setTexture(Texture *texture) {
|
||||||
|
this->texture_ = texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obten el valor de la variable
|
||||||
|
auto Sprite::getRenderer() -> SDL_Renderer * {
|
||||||
|
return renderer_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Sprite::setRenderer(SDL_Renderer *renderer) {
|
||||||
|
this->renderer_ = renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Sprite::setEnabled(bool value) {
|
||||||
|
enabled_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si el objeto está habilitado
|
||||||
|
auto Sprite::isEnabled() -> bool {
|
||||||
|
return enabled_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devuelve el rectangulo donde está el sprite
|
||||||
|
auto Sprite::getRect() -> SDL_Rect {
|
||||||
|
SDL_Rect rect = {x_, y_, w_, h_};
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece los valores de posición y tamaño del sprite
|
||||||
|
void Sprite::setRect(SDL_Rect rect) {
|
||||||
|
x_ = rect.x;
|
||||||
|
y_ = rect.y;
|
||||||
|
w_ = rect.w;
|
||||||
|
h_ = rect.h;
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
class Texture;
|
||||||
|
|
||||||
|
// Clase sprite
|
||||||
|
class Sprite {
|
||||||
|
public:
|
||||||
|
explicit Sprite(int x = 0, int y = 0, int w = 0, int h = 0, Texture *texture = nullptr, SDL_Renderer *renderer = nullptr); // Constructor
|
||||||
|
Sprite(SDL_Rect rect, Texture *texture, SDL_Renderer *renderer);
|
||||||
|
|
||||||
|
virtual ~Sprite(); // Destructor
|
||||||
|
|
||||||
|
virtual void render(); // Muestra el sprite por pantalla
|
||||||
|
|
||||||
|
[[nodiscard]] auto getPosX() const -> int; // Obten el valor de la variable
|
||||||
|
[[nodiscard]] auto getPosY() const -> int; // Obten el valor de la variable
|
||||||
|
[[nodiscard]] auto getWidth() const -> int; // Obten el valor de la variable
|
||||||
|
[[nodiscard]] auto getHeight() const -> int; // Obten el valor de la variable
|
||||||
|
|
||||||
|
void setPos(SDL_Rect rect); // Establece la posición del objeto
|
||||||
|
void setPosX(int x); // Establece el valor de la variable
|
||||||
|
void setPosY(int y); // Establece el valor de la variable
|
||||||
|
void setWidth(int w); // Establece el valor de la variable
|
||||||
|
void setHeight(int h); // Establece el valor de la variable
|
||||||
|
|
||||||
|
auto getSpriteClip() -> SDL_Rect; // Obten el valor de la variable
|
||||||
|
void setSpriteClip(SDL_Rect rect); // Establece el valor de la variable
|
||||||
|
void setSpriteClip(int x, int y, int w, int h); // Establece el valor de la variable
|
||||||
|
|
||||||
|
auto getTexture() -> Texture *; // Obten el valor de la variable
|
||||||
|
void setTexture(Texture *texture); // Establece el valor de la variable
|
||||||
|
|
||||||
|
auto getRenderer() -> SDL_Renderer *; // Obten el valor de la variable
|
||||||
|
void setRenderer(SDL_Renderer *renderer); // Establece el valor de la variable
|
||||||
|
|
||||||
|
virtual void setEnabled(bool value); // Establece el valor de la variable
|
||||||
|
virtual auto isEnabled() -> bool; // Comprueba si el objeto está habilitado
|
||||||
|
|
||||||
|
virtual auto getRect() -> SDL_Rect; // Devuelve el rectangulo donde está el sprite
|
||||||
|
virtual void setRect(SDL_Rect rect); // Establece los valores de posición y tamaño del sprite
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int x_; // Posición en el eje X donde dibujar el sprite
|
||||||
|
int y_; // Posición en el eje Y donde dibujar el sprite
|
||||||
|
int w_; // Ancho del sprite
|
||||||
|
int h_; // Alto del sprite
|
||||||
|
|
||||||
|
SDL_Renderer *renderer_; // Puntero al renderizador de la ventana
|
||||||
|
Texture *texture_; // Textura donde estan todos los dibujos del sprite
|
||||||
|
SDL_Rect sprite_clip_; // Rectangulo de origen de la textura que se dibujará en pantalla
|
||||||
|
|
||||||
|
bool enabled_; // Indica si el sprite esta habilitado
|
||||||
|
};
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
|
||||||
|
#include "core/rendering/text.h"
|
||||||
|
|
||||||
|
#include <iostream> // for cout
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include "core/rendering/sprite.h" // for Sprite
|
||||||
|
#include "core/rendering/texture.h" // for Texture
|
||||||
|
#include "core/resources/resource_helper.h" // for loadFile (pack + filesystem fallback)
|
||||||
|
#include "utils/utils.h" // for Color
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Estructura intermedia para serializar/parsear el bitmap font.
|
||||||
|
// No se expone fuera del TU: solo la usan los constructores de Text.
|
||||||
|
struct TextFile {
|
||||||
|
int box_width; // Anchura de la caja de cada caracter en el png
|
||||||
|
int box_height; // Altura de la caja de cada caracter en el png
|
||||||
|
Text::Offset offset[128]; // Vector con las posiciones y ancho de cada letra
|
||||||
|
};
|
||||||
|
|
||||||
|
void parseTextFileStream(std::istream &rfile, TextFile &tf) {
|
||||||
|
std::string buffer;
|
||||||
|
std::getline(rfile, buffer);
|
||||||
|
std::getline(rfile, buffer);
|
||||||
|
tf.box_width = std::stoi(buffer);
|
||||||
|
|
||||||
|
std::getline(rfile, buffer);
|
||||||
|
std::getline(rfile, buffer);
|
||||||
|
tf.box_height = std::stoi(buffer);
|
||||||
|
|
||||||
|
int index = 32;
|
||||||
|
int line_read = 0;
|
||||||
|
while (std::getline(rfile, buffer)) {
|
||||||
|
if (line_read % 2 == 1) {
|
||||||
|
tf.offset[index++].w = std::stoi(buffer);
|
||||||
|
}
|
||||||
|
buffer.clear();
|
||||||
|
line_read++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void computeTextFileOffsets(TextFile &tf) {
|
||||||
|
for (int i = 32; i < 128; ++i) {
|
||||||
|
tf.offset[i].x = ((i - 32) % 15) * tf.box_width;
|
||||||
|
tf.offset[i].y = ((i - 32) / 15) * tf.box_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llena un TextFile desde bytes en memoria
|
||||||
|
auto loadTextFileFromMemory(const std::vector<uint8_t> &bytes, bool verbose) -> TextFile {
|
||||||
|
TextFile tf;
|
||||||
|
tf.box_width = 0;
|
||||||
|
tf.box_height = 0;
|
||||||
|
for (auto &i : tf.offset) {
|
||||||
|
i.x = 0;
|
||||||
|
i.y = 0;
|
||||||
|
i.w = 0;
|
||||||
|
}
|
||||||
|
if (!bytes.empty()) {
|
||||||
|
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
|
||||||
|
std::stringstream ss(content);
|
||||||
|
parseTextFileStream(ss, tf);
|
||||||
|
if (verbose) {
|
||||||
|
std::cout << "Text loaded from memory" << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
computeTextFileOffsets(tf);
|
||||||
|
return tf;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llena un TextFile desde un fichero (vía ResourceHelper: pack o filesystem)
|
||||||
|
auto loadTextFile(const std::string &file, bool verbose = false) -> TextFile {
|
||||||
|
const std::string FILE_NAME = file.substr(file.find_last_of("\\/") + 1);
|
||||||
|
auto bytes = ResourceHelper::loadFile(file);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
if (verbose) {
|
||||||
|
std::cout << "Warning: Unable to open " << FILE_NAME.c_str() << " file" << '\n';
|
||||||
|
}
|
||||||
|
TextFile tf;
|
||||||
|
tf.box_width = 0;
|
||||||
|
tf.box_height = 0;
|
||||||
|
for (auto &i : tf.offset) {
|
||||||
|
i.x = 0;
|
||||||
|
i.y = 0;
|
||||||
|
i.w = 0;
|
||||||
|
}
|
||||||
|
computeTextFileOffsets(tf);
|
||||||
|
return tf;
|
||||||
|
}
|
||||||
|
if (verbose) {
|
||||||
|
std::cout << "Text loaded: " << FILE_NAME.c_str() << '\n';
|
||||||
|
}
|
||||||
|
return loadTextFileFromMemory(bytes, verbose);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Text::Text(const std::string &bitmap_file, const std::string &text_file, SDL_Renderer *renderer) {
|
||||||
|
// Carga los offsets desde el fichero
|
||||||
|
TextFile tf = loadTextFile(text_file);
|
||||||
|
|
||||||
|
// Inicializa variables desde la estructura
|
||||||
|
box_height_ = tf.box_height;
|
||||||
|
box_width_ = tf.box_width;
|
||||||
|
for (int i = 0; i < 128; ++i) {
|
||||||
|
offset_[i].x = tf.offset[i].x;
|
||||||
|
offset_[i].y = tf.offset[i].y;
|
||||||
|
offset_[i].w = tf.offset[i].w;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea los objetos
|
||||||
|
texture_ = new Texture(renderer, bitmap_file);
|
||||||
|
sprite_ = new Sprite({0, 0, box_width_, box_height_}, texture_, renderer);
|
||||||
|
|
||||||
|
// Inicializa variables
|
||||||
|
fixed_width_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor desde bytes
|
||||||
|
Text::Text(const std::vector<uint8_t> &png_bytes, const std::vector<uint8_t> &txt_bytes, SDL_Renderer *renderer) {
|
||||||
|
TextFile tf = loadTextFileFromMemory(txt_bytes, false);
|
||||||
|
box_height_ = tf.box_height;
|
||||||
|
box_width_ = tf.box_width;
|
||||||
|
for (int i = 0; i < 128; ++i) {
|
||||||
|
offset_[i].x = tf.offset[i].x;
|
||||||
|
offset_[i].y = tf.offset[i].y;
|
||||||
|
offset_[i].w = tf.offset[i].w;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea la textura desde bytes (Text es dueño en este overload)
|
||||||
|
texture_ = new Texture(renderer, png_bytes);
|
||||||
|
sprite_ = new Sprite({0, 0, box_width_, box_height_}, texture_, renderer);
|
||||||
|
|
||||||
|
fixed_width_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
Text::~Text() {
|
||||||
|
delete sprite_;
|
||||||
|
|
||||||
|
delete texture_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escribe texto en pantalla
|
||||||
|
void Text::write(int x, int y, const std::string &text, int kerning, int lenght) {
|
||||||
|
int shift = 0;
|
||||||
|
|
||||||
|
if (lenght == -1) {
|
||||||
|
lenght = text.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
sprite_->setPosY(y);
|
||||||
|
const int WIDTH = sprite_->getWidth();
|
||||||
|
const int HEIGHT = sprite_->getHeight();
|
||||||
|
for (int i = 0; i < lenght; ++i) {
|
||||||
|
const int INDEX = static_cast<unsigned char>(text[i]);
|
||||||
|
sprite_->setSpriteClip(offset_[INDEX].x, offset_[INDEX].y, WIDTH, HEIGHT);
|
||||||
|
sprite_->setPosX(x + shift);
|
||||||
|
sprite_->render();
|
||||||
|
shift += fixed_width_ ? box_width_ : (offset_[INDEX].w + kerning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escribe el texto con colores
|
||||||
|
void Text::writeColored(int x, int y, const std::string &text, Color color, int kerning, int lenght) {
|
||||||
|
sprite_->getTexture()->setColor(color.r, color.g, color.b);
|
||||||
|
write(x, y, text, kerning, lenght);
|
||||||
|
sprite_->getTexture()->setColor(255, 255, 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escribe el texto con sombra
|
||||||
|
void Text::writeShadowed(int x, int y, const std::string &text, Color color, Uint8 shadow_distance, int kerning, int lenght) {
|
||||||
|
sprite_->getTexture()->setColor(color.r, color.g, color.b);
|
||||||
|
write(x + shadow_distance, y + shadow_distance, text, kerning, lenght);
|
||||||
|
sprite_->getTexture()->setColor(255, 255, 255);
|
||||||
|
write(x, y, text, kerning, lenght);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escribe el texto centrado en un punto x
|
||||||
|
void Text::writeCentered(int x, int y, const std::string &text, int kerning, int lenght) {
|
||||||
|
x -= (Text::lenght(text, kerning) / 2);
|
||||||
|
write(x, y, text, kerning, lenght);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escribe texto con extras
|
||||||
|
void Text::writeDX(Uint8 flags, int x, int y, const std::string &text, int kerning, Color text_color, Uint8 shadow_distance, Color shadow_color, int lenght) {
|
||||||
|
const bool CENTERED = ((flags & Text::FLAG_CENTER) == Text::FLAG_CENTER);
|
||||||
|
const bool SHADOWED = ((flags & Text::FLAG_SHADOW) == Text::FLAG_SHADOW);
|
||||||
|
const bool COLORED = ((flags & Text::FLAG_COLOR) == Text::FLAG_COLOR);
|
||||||
|
const bool STROKED = ((flags & Text::FLAG_STROKE) == Text::FLAG_STROKE);
|
||||||
|
|
||||||
|
if (CENTERED) {
|
||||||
|
x -= (Text::lenght(text, kerning) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SHADOWED) {
|
||||||
|
writeColored(x + shadow_distance, y + shadow_distance, text, shadow_color, kerning, lenght);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (STROKED) {
|
||||||
|
for (int dist = 1; dist <= shadow_distance; ++dist) {
|
||||||
|
for (int dy = -dist; dy <= dist; ++dy) {
|
||||||
|
for (int dx = -dist; dx <= dist; ++dx) {
|
||||||
|
writeColored(x + dx, y + dy, text, shadow_color, kerning, lenght);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (COLORED) {
|
||||||
|
writeColored(x, y, text, text_color, kerning, lenght);
|
||||||
|
} else {
|
||||||
|
write(x, y, text, kerning, lenght);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene la longitud en pixels de una cadena
|
||||||
|
auto Text::lenght(const std::string &text, int kerning) -> int {
|
||||||
|
int shift = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < (int)text.length(); ++i) {
|
||||||
|
shift += (offset_[static_cast<unsigned char>(text[i])].w + kerning);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descuenta el kerning del último caracter
|
||||||
|
return shift - kerning;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devuelve el valor de la variable
|
||||||
|
auto Text::getCharacterSize() const -> int {
|
||||||
|
return box_width_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recarga la textura
|
||||||
|
void Text::reLoadTexture() {
|
||||||
|
sprite_->getTexture()->reLoad();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece si se usa un tamaño fijo de letra
|
||||||
|
void Text::setFixedWidth(bool value) {
|
||||||
|
fixed_width_ = value;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string> // for string
|
||||||
|
#include <vector>
|
||||||
|
class Sprite;
|
||||||
|
class Texture;
|
||||||
|
#include "utils/utils.h"
|
||||||
|
|
||||||
|
// Clase texto. Pinta texto en pantalla a partir de un bitmap
|
||||||
|
class Text {
|
||||||
|
public:
|
||||||
|
// Flags bitmask para writeDX
|
||||||
|
static constexpr int FLAG_COLOR = 1;
|
||||||
|
static constexpr int FLAG_SHADOW = 2;
|
||||||
|
static constexpr int FLAG_CENTER = 4;
|
||||||
|
static constexpr int FLAG_STROKE = 8;
|
||||||
|
|
||||||
|
struct Offset {
|
||||||
|
int x; // Posición X dentro del bitmap
|
||||||
|
int y; // Posición Y dentro del bitmap
|
||||||
|
int w; // Anchura del glifo
|
||||||
|
};
|
||||||
|
|
||||||
|
Text(const std::string &bitmap_file, const std::string &text_file, SDL_Renderer *renderer); // Constructor desde paths
|
||||||
|
Text(const std::vector<uint8_t> &png_bytes, const std::vector<uint8_t> &txt_bytes, SDL_Renderer *renderer); // Constructor desde bytes en memoria
|
||||||
|
|
||||||
|
~Text(); // Destructor
|
||||||
|
|
||||||
|
// No copiable (gestiona memoria dinámica)
|
||||||
|
Text(const Text &) = delete;
|
||||||
|
auto operator=(const Text &) -> Text & = delete;
|
||||||
|
|
||||||
|
void write(int x, int y, const std::string &text, int kerning = 1, int lenght = -1); // Escribe el texto en pantalla
|
||||||
|
void writeColored(int x, int y, const std::string &text, Color color, int kerning = 1, int lenght = -1); // Escribe el texto con colores
|
||||||
|
void writeShadowed(int x, int y, const std::string &text, Color color, Uint8 shadow_distance = 1, int kerning = 1, int lenght = -1); // Escribe el texto con sombra
|
||||||
|
void writeCentered(int x, int y, const std::string &text, int kerning = 1, int lenght = -1); // Escribe el texto centrado en un punto x
|
||||||
|
void writeDX(Uint8 flags, int x, int y, const std::string &text, int kerning = 1, Color text_color = Color(255, 255, 255), Uint8 shadow_distance = 1, Color shadow_color = Color(0, 0, 0), int lenght = -1); // Escribe texto con extras
|
||||||
|
|
||||||
|
auto lenght(const std::string &text, int kerning = 1) -> int; // Obtiene la longitud en pixels de una cadena
|
||||||
|
|
||||||
|
[[nodiscard]] auto getCharacterSize() const -> int; // Devuelve el valor de la variable
|
||||||
|
|
||||||
|
void reLoadTexture(); // Recarga la textura
|
||||||
|
void setFixedWidth(bool value); // Establece si se usa un tamaño fijo de letra
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Objetos y punteros
|
||||||
|
Sprite *sprite_; // Objeto con los graficos para el texto
|
||||||
|
Texture *texture_; // Textura con los bitmaps del texto
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
int box_width_; // Anchura de la caja de cada caracter en el png
|
||||||
|
int box_height_; // Altura de la caja de cada caracter en el png
|
||||||
|
bool fixed_width_; // Indica si el texto se ha de escribir con longitud fija en todas las letras
|
||||||
|
Offset offset_[128]; // Vector con las posiciones y ancho de cada letra
|
||||||
|
};
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
|
||||||
|
#include "core/rendering/texture.h"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdlib> // for exit
|
||||||
|
#include <iostream> // for basic_ostream, operator<<, cout, endl
|
||||||
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
|
#include "core/resources/resource_helper.h" // for loadFile (pack + filesystem fallback)
|
||||||
|
#include "external/stb_image.h" // for stbi_failure_reason, stbi_image_free
|
||||||
|
|
||||||
|
SDL_ScaleMode Texture::current_scale_mode = SDL_SCALEMODE_NEAREST;
|
||||||
|
|
||||||
|
void Texture::setGlobalScaleMode(SDL_ScaleMode mode) {
|
||||||
|
current_scale_mode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Texture::Texture(SDL_Renderer *renderer, const std::string &path, bool verbose)
|
||||||
|
: texture_(nullptr),
|
||||||
|
renderer_(renderer),
|
||||||
|
width_(0),
|
||||||
|
height_(0),
|
||||||
|
path_(path) {
|
||||||
|
// Carga el fichero en la textura
|
||||||
|
if (!path.empty()) {
|
||||||
|
loadFromFile(path, renderer, verbose);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor desde bytes
|
||||||
|
Texture::Texture(SDL_Renderer *renderer, const std::vector<uint8_t> &bytes, bool verbose)
|
||||||
|
: texture_(nullptr),
|
||||||
|
renderer_(renderer),
|
||||||
|
width_(0),
|
||||||
|
height_(0) {
|
||||||
|
if (!bytes.empty()) {
|
||||||
|
loadFromMemory(bytes.data(), bytes.size(), renderer, verbose);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
Texture::~Texture() {
|
||||||
|
// Libera memoria
|
||||||
|
unload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: convierte píxeles RGBA decodificados por stbi en SDL_Texture
|
||||||
|
static auto createTextureFromPixels(SDL_Renderer *renderer, unsigned char *data, int w, int h, int *out_w, int *out_h) -> SDL_Texture * {
|
||||||
|
const int PITCH = 4 * w;
|
||||||
|
SDL_Surface *loaded_surface = SDL_CreateSurfaceFrom(w, h, SDL_PIXELFORMAT_RGBA32, static_cast<void *>(data), PITCH);
|
||||||
|
if (loaded_surface == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
SDL_Texture *new_texture = SDL_CreateTextureFromSurface(renderer, loaded_surface);
|
||||||
|
if (new_texture != nullptr) {
|
||||||
|
*out_w = loaded_surface->w;
|
||||||
|
*out_h = loaded_surface->h;
|
||||||
|
SDL_SetTextureScaleMode(new_texture, Texture::current_scale_mode);
|
||||||
|
}
|
||||||
|
SDL_DestroySurface(loaded_surface);
|
||||||
|
return new_texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carga una imagen desde un fichero (vía ResourceHelper: pack si està inicialitzat, filesystem si no)
|
||||||
|
auto Texture::loadFromFile(const std::string &path, SDL_Renderer *renderer, bool verbose) -> bool {
|
||||||
|
const std::string FILE_NAME = path.substr(path.find_last_of("\\/") + 1);
|
||||||
|
|
||||||
|
auto bytes = ResourceHelper::loadFile(path);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
SDL_Log("Loading image failed: can't open %s", path.c_str());
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int req_format = STBI_rgb_alpha;
|
||||||
|
int w;
|
||||||
|
int h;
|
||||||
|
int orig_format;
|
||||||
|
unsigned char *data = stbi_load_from_memory(bytes.data(), static_cast<int>(bytes.size()), &w, &h, &orig_format, req_format);
|
||||||
|
if (data == nullptr) {
|
||||||
|
SDL_Log("Loading image failed: %s", stbi_failure_reason());
|
||||||
|
exit(1);
|
||||||
|
} else if (verbose) {
|
||||||
|
std::cout << "Image loaded: " << FILE_NAME.c_str() << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
unload();
|
||||||
|
SDL_Texture *new_texture = createTextureFromPixels(renderer, data, w, h, &this->width_, &this->height_);
|
||||||
|
if (new_texture == nullptr && verbose) {
|
||||||
|
std::cout << "Unable to load image " << path.c_str() << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
stbi_image_free(data);
|
||||||
|
texture_ = new_texture;
|
||||||
|
return texture_ != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carga una imagen desde bytes en memoria
|
||||||
|
auto Texture::loadFromMemory(const uint8_t *data, size_t size, SDL_Renderer *renderer, bool verbose) -> bool {
|
||||||
|
int w;
|
||||||
|
int h;
|
||||||
|
int orig_format;
|
||||||
|
unsigned char *pixels = stbi_load_from_memory(data, (int)size, &w, &h, &orig_format, STBI_rgb_alpha);
|
||||||
|
if (pixels == nullptr) {
|
||||||
|
SDL_Log("Loading image from memory failed: %s", stbi_failure_reason());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unload();
|
||||||
|
SDL_Texture *new_texture = createTextureFromPixels(renderer, pixels, w, h, &this->width_, &this->height_);
|
||||||
|
if (new_texture == nullptr && verbose) {
|
||||||
|
std::cout << "Unable to create texture from memory" << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
stbi_image_free(pixels);
|
||||||
|
texture_ = new_texture;
|
||||||
|
return texture_ != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea una textura en blanco
|
||||||
|
auto Texture::createBlank(SDL_Renderer *renderer, int width, int height, SDL_TextureAccess access) -> bool {
|
||||||
|
// Crea una textura sin inicializar
|
||||||
|
texture_ = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, access, width, height);
|
||||||
|
if (texture_ == nullptr) {
|
||||||
|
std::cout << "Unable to create blank texture! SDL Error: " << SDL_GetError() << '\n';
|
||||||
|
} else {
|
||||||
|
this->width_ = width;
|
||||||
|
this->height_ = height;
|
||||||
|
SDL_SetTextureScaleMode(texture_, current_scale_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return texture_ != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Libera la memoria de la textura
|
||||||
|
void Texture::unload() {
|
||||||
|
// Libera la textura si existe
|
||||||
|
if (texture_ != nullptr) {
|
||||||
|
SDL_DestroyTexture(texture_);
|
||||||
|
texture_ = nullptr;
|
||||||
|
width_ = 0;
|
||||||
|
height_ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el color para la modulacion
|
||||||
|
void Texture::setColor(Uint8 red, Uint8 green, Uint8 blue) {
|
||||||
|
SDL_SetTextureColorMod(texture_, red, green, blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el blending
|
||||||
|
void Texture::setBlendMode(SDL_BlendMode blending) {
|
||||||
|
SDL_SetTextureBlendMode(texture_, blending);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el alpha para la modulación
|
||||||
|
void Texture::setAlpha(Uint8 alpha) {
|
||||||
|
SDL_SetTextureAlphaMod(texture_, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderiza la textura en un punto específico
|
||||||
|
void Texture::render(SDL_Renderer *renderer, int x, int y, const SDL_Rect *clip, float zoom_w, float zoom_h, double angle, const SDL_Point *center, SDL_FlipMode flip) {
|
||||||
|
// Establece el destino de renderizado en la pantalla
|
||||||
|
SDL_FRect render_quad = {(float)x, (float)y, (float)width_, (float)height_};
|
||||||
|
|
||||||
|
// Obtiene las dimesiones del clip de renderizado
|
||||||
|
if (clip != nullptr) {
|
||||||
|
render_quad.w = (float)clip->w;
|
||||||
|
render_quad.h = (float)clip->h;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_quad.w = render_quad.w * zoom_w;
|
||||||
|
render_quad.h = render_quad.h * zoom_h;
|
||||||
|
|
||||||
|
// Convierte el clip a SDL_FRect
|
||||||
|
SDL_FRect src_rect;
|
||||||
|
SDL_FRect *src_rect_ptr = nullptr;
|
||||||
|
if (clip != nullptr) {
|
||||||
|
src_rect = {.x = (float)clip->x, .y = (float)clip->y, .w = (float)clip->w, .h = (float)clip->h};
|
||||||
|
src_rect_ptr = &src_rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convierte el centro a SDL_FPoint
|
||||||
|
SDL_FPoint f_center;
|
||||||
|
SDL_FPoint *f_center_ptr = nullptr;
|
||||||
|
if (center != nullptr) {
|
||||||
|
f_center = {.x = (float)center->x, .y = (float)center->y};
|
||||||
|
f_center_ptr = &f_center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renderiza a pantalla
|
||||||
|
SDL_RenderTextureRotated(renderer, texture_, src_rect_ptr, &render_quad, angle, f_center_ptr, flip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la textura como objetivo de renderizado
|
||||||
|
void Texture::setAsRenderTarget(SDL_Renderer *renderer) {
|
||||||
|
SDL_SetRenderTarget(renderer, texture_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el ancho de la imagen
|
||||||
|
auto Texture::getWidth() const -> int {
|
||||||
|
return width_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el alto de la imagen
|
||||||
|
auto Texture::getHeight() const -> int {
|
||||||
|
return height_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recarga la textura
|
||||||
|
auto Texture::reLoad() -> bool {
|
||||||
|
return loadFromFile(path_, renderer_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene la textura
|
||||||
|
auto Texture::getSDLTexture() -> SDL_Texture * {
|
||||||
|
return texture_;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string> // for basic_string, string
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class Texture {
|
||||||
|
public:
|
||||||
|
static SDL_ScaleMode current_scale_mode; // Modo de escalado global para nuevas texturas
|
||||||
|
|
||||||
|
static void setGlobalScaleMode(SDL_ScaleMode mode); // Establece el modo de escalado global para nuevas texturas
|
||||||
|
|
||||||
|
explicit Texture(SDL_Renderer *renderer, const std::string &path = "", bool verbose = false); // Constructor
|
||||||
|
Texture(SDL_Renderer *renderer, const std::vector<uint8_t> &bytes, bool verbose = false); // Constructor desde bytes (PNG en memoria)
|
||||||
|
~Texture(); // Destructor
|
||||||
|
|
||||||
|
auto loadFromFile(const std::string &path, SDL_Renderer *renderer, bool verbose = false) -> bool; // Carga una imagen desde un fichero
|
||||||
|
auto loadFromMemory(const uint8_t *data, size_t size, SDL_Renderer *renderer, bool verbose = false) -> bool; // Carga una imagen desde bytes en memoria
|
||||||
|
auto createBlank(SDL_Renderer *renderer, int width, int height, SDL_TextureAccess /*access*/ = SDL_TEXTUREACCESS_STREAMING) -> bool; // Crea una textura en blanco
|
||||||
|
void unload(); // Libera la memoria de la textura
|
||||||
|
|
||||||
|
void setColor(Uint8 red, Uint8 green, Uint8 blue); // Establece el color para la modulacion
|
||||||
|
void setBlendMode(SDL_BlendMode blending); // Establece el blending
|
||||||
|
void setAlpha(Uint8 alpha); // Establece el alpha para la modulación
|
||||||
|
|
||||||
|
void render(SDL_Renderer *renderer, int x, int y, const SDL_Rect *clip = nullptr, float zoom_w = 1, float zoom_h = 1, double angle = 0.0, const SDL_Point *center = nullptr, SDL_FlipMode flip = SDL_FLIP_NONE); // Renderiza la textura en un punto específico
|
||||||
|
void setAsRenderTarget(SDL_Renderer *renderer); // Establece la textura como objetivo de renderizado
|
||||||
|
|
||||||
|
[[nodiscard]] auto getWidth() const -> int; // Obtiene el ancho de la imagen
|
||||||
|
[[nodiscard]] auto getHeight() const -> int; // Obtiene el alto de la imagen
|
||||||
|
|
||||||
|
auto reLoad() -> bool; // Recarga la textura
|
||||||
|
auto getSDLTexture() -> SDL_Texture *; // Obtiene la textura
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Objetos y punteros
|
||||||
|
SDL_Texture *texture_; // La textura
|
||||||
|
SDL_Renderer *renderer_; // Renderizador donde dibujar la textura
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
int width_; // Ancho de la imagen
|
||||||
|
int height_; // Alto de la imagen
|
||||||
|
std::string path_; // Ruta de la imagen de la textura
|
||||||
|
};
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#include "core/rendering/writer.h"
|
||||||
|
|
||||||
|
#include "core/rendering/text.h" // for Text
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Writer::Writer(Text *text)
|
||||||
|
: text_(text) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avança un caracter cada `seconds_per_char_` i un cop completat es queda
|
||||||
|
// visible `remaining_time_s_` segons abans de finalitzar.
|
||||||
|
void Writer::update(float dt_s) {
|
||||||
|
if (!enabled_) { return; }
|
||||||
|
|
||||||
|
if (!completed_) {
|
||||||
|
char_timer_s_ += dt_s;
|
||||||
|
while (char_timer_s_ >= seconds_per_char_ && index_ < length_) {
|
||||||
|
char_timer_s_ -= seconds_per_char_;
|
||||||
|
++index_;
|
||||||
|
}
|
||||||
|
if (index_ >= length_) {
|
||||||
|
completed_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (completed_) {
|
||||||
|
if (remaining_time_s_ <= 0.0F) {
|
||||||
|
finished_ = true;
|
||||||
|
} else {
|
||||||
|
remaining_time_s_ -= dt_s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dibuja el objeto en pantalla
|
||||||
|
void Writer::render() {
|
||||||
|
if (enabled_) {
|
||||||
|
text_->write(pos_x_, pos_y_, caption_, kerning_, index_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Writer::setPosX(int value) {
|
||||||
|
pos_x_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Writer::setPosY(int value) {
|
||||||
|
pos_y_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Writer::setKerning(int value) {
|
||||||
|
kerning_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Writer::setCaption(const std::string &text) {
|
||||||
|
caption_ = text;
|
||||||
|
length_ = text.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Segons per caracter. Quan s'usa, l'update(dt) avança index.
|
||||||
|
void Writer::setSecondsPerChar(float seconds) {
|
||||||
|
seconds_per_char_ = seconds;
|
||||||
|
char_timer_s_ = 0.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Writer::setEnabled(bool value) {
|
||||||
|
enabled_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto Writer::isEnabled() const -> bool {
|
||||||
|
return enabled_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temps que es mante visible despres de completar el text.
|
||||||
|
void Writer::setRemainingTime(float seconds) {
|
||||||
|
remaining_time_s_ = seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Writer::getRemainingTime() const -> float {
|
||||||
|
return remaining_time_s_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Centra la cadena de texto a un punto X
|
||||||
|
void Writer::center(int x) {
|
||||||
|
setPosX(x - (text_->lenght(caption_, kerning_) / 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto Writer::hasFinished() const -> bool {
|
||||||
|
return finished_;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string> // for string, basic_string
|
||||||
|
class Text;
|
||||||
|
|
||||||
|
// Clase Writer. Pinta texto en pantalla letra a letra a partir de una cadena y un bitmap
|
||||||
|
class Writer {
|
||||||
|
public:
|
||||||
|
explicit Writer(Text *text); // Constructor
|
||||||
|
|
||||||
|
void update(float dt_s); // Actualiza el objeto
|
||||||
|
void render(); // Dibuja el objeto en pantalla
|
||||||
|
|
||||||
|
void setPosX(int value); // Establece el valor de la variable
|
||||||
|
void setPosY(int value); // Establece el valor de la variable
|
||||||
|
void setKerning(int value); // Establece el valor de la variable
|
||||||
|
void setCaption(const std::string &text); // Establece el valor de la variable
|
||||||
|
void setSecondsPerChar(float seconds); // Segons per caracter
|
||||||
|
void setEnabled(bool value); // Establece el valor de la variable
|
||||||
|
[[nodiscard]] auto isEnabled() const -> bool; // Obtiene el valor de la variable
|
||||||
|
|
||||||
|
void setRemainingTime(float seconds); // Temps despres de completar
|
||||||
|
[[nodiscard]] auto getRemainingTime() const -> float; // Temps restant
|
||||||
|
|
||||||
|
void center(int x); // Centra la cadena de texto a un punto X
|
||||||
|
[[nodiscard]] auto hasFinished() const -> bool; // Obtiene el valor de la variable
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Objetos y punteros
|
||||||
|
Text *text_; // Objeto encargado de escribir el texto
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
int pos_x_{0}; // Posicion en el eje X donde empezar a escribir el texto
|
||||||
|
int pos_y_{0}; // Posicion en el eje Y donde empezar a escribir el texto
|
||||||
|
int kerning_{0}; // Kerning del texto, es decir, espaciado entre caracteres
|
||||||
|
std::string caption_; // El texto para escribir
|
||||||
|
float seconds_per_char_{0.0F}; // Segons per caracter
|
||||||
|
float char_timer_s_{0.0F}; // Acumulador d'avanç de caracter
|
||||||
|
int index_{0}; // Posición del texto que se está escribiendo
|
||||||
|
int length_{0}; // Longitud de la cadena a escribir
|
||||||
|
bool completed_{false}; // Indica si se ha escrito todo el texto
|
||||||
|
bool enabled_{false}; // Indica si el objeto está habilitado
|
||||||
|
float remaining_time_s_{0.0F}; // Temps restant per a deshabilitar
|
||||||
|
bool finished_{false}; // Indica si ya ha terminado
|
||||||
|
};
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
#include "core/resources/asset.h"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstddef> // for size_t
|
||||||
|
#include <iostream> // for basic_ostream, operator<<, cout, endl
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.h"
|
||||||
|
|
||||||
|
// Instancia única
|
||||||
|
Asset *Asset::instance = nullptr;
|
||||||
|
|
||||||
|
// Singleton API
|
||||||
|
void Asset::init(const std::string &executable_path) {
|
||||||
|
Asset::instance = new Asset(executable_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Asset::destroy() {
|
||||||
|
delete Asset::instance;
|
||||||
|
Asset::instance = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Asset::get() -> Asset * {
|
||||||
|
return Asset::instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Asset::Asset(const std::string &executable_path)
|
||||||
|
: executable_path_(executable_path.substr(0, executable_path.find_last_of("\\/"))) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Añade un elemento a la lista
|
||||||
|
void Asset::add(const std::string &file, Type type, bool required, bool absolute) {
|
||||||
|
Item temp;
|
||||||
|
temp.file = absolute ? file : executable_path_ + file;
|
||||||
|
temp.type = type;
|
||||||
|
temp.required = required;
|
||||||
|
file_list_.push_back(temp);
|
||||||
|
|
||||||
|
const std::string FILE_NAME = file.substr(file.find_last_of("\\/") + 1);
|
||||||
|
longest_name_ = SDL_max(longest_name_, FILE_NAME.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devuelve el fichero de un elemento de la lista a partir de una cadena
|
||||||
|
auto Asset::get(const std::string &text) -> std::string {
|
||||||
|
for (const auto &f : file_list_) {
|
||||||
|
const size_t LAST_INDEX = f.file.find_last_of('/') + 1;
|
||||||
|
const std::string FILE_NAME = f.file.substr(LAST_INDEX);
|
||||||
|
|
||||||
|
if (FILE_NAME == text) {
|
||||||
|
return f.file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verbose_) {
|
||||||
|
std::cout << "Warning: file " << text.c_str() << " not found" << '\n';
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba que existen todos los elementos
|
||||||
|
auto Asset::check() -> bool {
|
||||||
|
bool success = true;
|
||||||
|
|
||||||
|
if (verbose_) {
|
||||||
|
std::cout << "\n** Checking files" << '\n';
|
||||||
|
|
||||||
|
std::cout << "Executable path is: " << executable_path_ << '\n';
|
||||||
|
std::cout << "Sample filepath: " << file_list_.back().file << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba la lista de ficheros clasificandolos por tipo
|
||||||
|
for (int i = 0; i < static_cast<int>(Type::COUNT); ++i) {
|
||||||
|
const Type TYPE = static_cast<Type>(i);
|
||||||
|
|
||||||
|
// Comprueba si hay ficheros de ese tipo
|
||||||
|
bool any = false;
|
||||||
|
|
||||||
|
for (const auto &f : file_list_) {
|
||||||
|
if (f.required && f.type == TYPE) {
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si hay ficheros de ese tipo, comprueba si existen
|
||||||
|
if (any) {
|
||||||
|
if (verbose_) {
|
||||||
|
std::cout << "\n>> " << getTypeName(TYPE).c_str() << " FILES" << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &f : file_list_) {
|
||||||
|
if (f.required && f.type == TYPE) {
|
||||||
|
success &= checkFile(f.file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resultado
|
||||||
|
if (verbose_) {
|
||||||
|
if (success) {
|
||||||
|
std::cout << "\n** All files OK.\n"
|
||||||
|
<< '\n';
|
||||||
|
} else {
|
||||||
|
std::cout << "\n** A file is missing. Exiting.\n"
|
||||||
|
<< '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba que existe un fichero
|
||||||
|
auto Asset::checkFile(const std::string &path) const -> bool {
|
||||||
|
bool success = false;
|
||||||
|
std::string result = "ERROR";
|
||||||
|
|
||||||
|
// Comprueba si existe el fichero (pack o filesystem)
|
||||||
|
const std::string FILE_NAME = path.substr(path.find_last_of("\\/") + 1);
|
||||||
|
if (ResourceHelper::shouldUseResourcePack(path)) {
|
||||||
|
auto bytes = ResourceHelper::loadFile(path);
|
||||||
|
if (!bytes.empty()) {
|
||||||
|
result = "OK";
|
||||||
|
success = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SDL_IOStream *file = SDL_IOFromFile(path.c_str(), "rb");
|
||||||
|
if (file != nullptr) {
|
||||||
|
result = "OK";
|
||||||
|
success = true;
|
||||||
|
SDL_CloseIO(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verbose_) {
|
||||||
|
std::cout.setf(std::ios::left, std::ios::adjustfield);
|
||||||
|
std::cout << "Checking file: ";
|
||||||
|
std::cout.width(longest_name_ + 2);
|
||||||
|
std::cout.fill('.');
|
||||||
|
std::cout << FILE_NAME + " ";
|
||||||
|
std::cout << " [" + result + "]" << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devuelve el nombre del tipo de recurso
|
||||||
|
auto Asset::getTypeName(Type type) -> std::string {
|
||||||
|
switch (type) {
|
||||||
|
case Type::BITMAP:
|
||||||
|
return "BITMAP";
|
||||||
|
case Type::MUSIC:
|
||||||
|
return "MUSIC";
|
||||||
|
case Type::SOUND:
|
||||||
|
return "SOUND";
|
||||||
|
case Type::FONT:
|
||||||
|
return "FONT";
|
||||||
|
case Type::LANG:
|
||||||
|
return "LANG";
|
||||||
|
case Type::DATA:
|
||||||
|
return "DATA";
|
||||||
|
case Type::ROOM:
|
||||||
|
return "ROOM";
|
||||||
|
case Type::ENEMY:
|
||||||
|
return "ENEMY";
|
||||||
|
case Type::ITEM:
|
||||||
|
return "ITEM";
|
||||||
|
case Type::COUNT:
|
||||||
|
default:
|
||||||
|
return "ERROR";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece si ha de mostrar texto por pantalla
|
||||||
|
void Asset::setVerbose(bool value) {
|
||||||
|
verbose_ = value;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef> // for size_t
|
||||||
|
#include <cstdint> // for uint8_t
|
||||||
|
#include <string> // for string, basic_string
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
// Clase Asset
|
||||||
|
class Asset {
|
||||||
|
public:
|
||||||
|
// Tipos de recurso
|
||||||
|
enum class Type : std::uint8_t {
|
||||||
|
BITMAP,
|
||||||
|
MUSIC,
|
||||||
|
SOUND,
|
||||||
|
FONT,
|
||||||
|
LANG,
|
||||||
|
DATA,
|
||||||
|
ROOM,
|
||||||
|
ENEMY,
|
||||||
|
ITEM,
|
||||||
|
COUNT // Centinela: número total de tipos
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estructura para definir un item
|
||||||
|
struct Item {
|
||||||
|
std::string file; // Ruta del fichero desde la raiz del directorio
|
||||||
|
Type type; // Indica el tipo de recurso
|
||||||
|
bool required; // Indica si es un fichero que debe de existir
|
||||||
|
};
|
||||||
|
|
||||||
|
// Singleton API
|
||||||
|
static void init(const std::string &executable_path); // Crea la instancia
|
||||||
|
static void destroy(); // Libera la instancia
|
||||||
|
static auto get() -> Asset *; // Obtiene el puntero a la instancia
|
||||||
|
|
||||||
|
void add(const std::string &file, Type type, bool required = true, bool absolute = false); // Añade un elemento a la lista
|
||||||
|
auto get(const std::string &text) -> std::string; // Devuelve un elemento de la lista a partir de una cadena
|
||||||
|
[[nodiscard]] auto getAll() const -> const std::vector<Item> & { return file_list_; } // Devuelve toda la lista de items registrados
|
||||||
|
auto check() -> bool; // Comprueba que existen todos los elementos
|
||||||
|
void setVerbose(bool value); // Establece si ha de mostrar texto por pantalla
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Variables
|
||||||
|
std::size_t longest_name_{0}; // Contiene la longitud del nombre de fichero mas largo
|
||||||
|
std::vector<Item> file_list_; // Listado con todas las rutas a los ficheros
|
||||||
|
std::string executable_path_; // Ruta al ejecutable
|
||||||
|
bool verbose_{true}; // Indica si ha de mostrar información por pantalla
|
||||||
|
|
||||||
|
static Asset *instance; // Instancia única
|
||||||
|
|
||||||
|
explicit Asset(const std::string &path); // Constructor privado (usar Asset::init)
|
||||||
|
|
||||||
|
[[nodiscard]] auto checkFile(const std::string &executable_path) const -> bool; // Comprueba que existe un fichero
|
||||||
|
static auto getTypeName(Type type) -> std::string; // Devuelve el nombre del tipo de recurso
|
||||||
|
};
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
#include "core/resources/resource.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include "core/audio/jail_audio.hpp"
|
||||||
|
#include "core/rendering/text.h"
|
||||||
|
#include "core/rendering/texture.h"
|
||||||
|
#include "core/resources/asset.h"
|
||||||
|
#include "core/resources/resource_helper.h"
|
||||||
|
#include "game/ui/menu.h"
|
||||||
|
|
||||||
|
// Nota: Asset::get() e Input::get() se consultan en preloadAll y al construir
|
||||||
|
// los menús; no se guardan punteros en el objeto Resource.
|
||||||
|
|
||||||
|
Resource *Resource::instance = nullptr;
|
||||||
|
|
||||||
|
static auto basename(const std::string &path) -> std::string {
|
||||||
|
return path.substr(path.find_last_of("\\/") + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static auto stem(const std::string &path) -> std::string {
|
||||||
|
std::string b = basename(path);
|
||||||
|
size_t dot = b.find_last_of('.');
|
||||||
|
if (dot == std::string::npos) {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
return b.substr(0, dot);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Resource::init(SDL_Renderer *renderer) {
|
||||||
|
if (instance == nullptr) {
|
||||||
|
instance = new Resource(renderer);
|
||||||
|
instance->preloadAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Resource::destroy() {
|
||||||
|
delete instance;
|
||||||
|
instance = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Resource::get() -> Resource * {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource::Resource(SDL_Renderer *renderer)
|
||||||
|
: renderer_(renderer) {}
|
||||||
|
|
||||||
|
Resource::~Resource() {
|
||||||
|
for (auto &[name, m] : menus_) {
|
||||||
|
delete m;
|
||||||
|
}
|
||||||
|
menus_.clear();
|
||||||
|
|
||||||
|
for (auto &[name, t] : texts_) {
|
||||||
|
delete t;
|
||||||
|
}
|
||||||
|
texts_.clear();
|
||||||
|
|
||||||
|
for (auto &[name, t] : textures_) {
|
||||||
|
delete t;
|
||||||
|
}
|
||||||
|
textures_.clear();
|
||||||
|
|
||||||
|
for (auto &[name, s] : sounds_) {
|
||||||
|
Ja::deleteSound(s);
|
||||||
|
}
|
||||||
|
sounds_.clear();
|
||||||
|
|
||||||
|
for (auto &[name, m] : musics_) {
|
||||||
|
Ja::deleteMusic(m);
|
||||||
|
}
|
||||||
|
musics_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Resource::preloadAll() {
|
||||||
|
preloadResources();
|
||||||
|
preloadFonts();
|
||||||
|
preloadMenus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 1: texturas, sonidos, músicas y datos (animaciones / demo / menús)
|
||||||
|
void Resource::preloadResources() {
|
||||||
|
const auto &items = Asset::get()->getAll();
|
||||||
|
|
||||||
|
for (const auto &it : items) {
|
||||||
|
if (!ResourceHelper::shouldUseResourcePack(it.file) && it.type != Asset::Type::LANG) {
|
||||||
|
// Ficheros absolutos (config.txt, score.bin, systemFolder) — no se precargan
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto bytes = ResourceHelper::loadFile(it.file);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string BASE_NAME = basename(it.file);
|
||||||
|
|
||||||
|
switch (it.type) {
|
||||||
|
case Asset::Type::BITMAP: {
|
||||||
|
auto *tex = new Texture(renderer_, bytes);
|
||||||
|
textures_[BASE_NAME] = tex;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Asset::Type::SOUND: {
|
||||||
|
Ja::Sound *s = Ja::loadSound(bytes.data(), (uint32_t)bytes.size());
|
||||||
|
if (s != nullptr) {
|
||||||
|
sounds_[BASE_NAME] = s;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Asset::Type::MUSIC: {
|
||||||
|
Ja::Music *m = Ja::loadMusic(bytes.data(), (Uint32)bytes.size());
|
||||||
|
if (m != nullptr) {
|
||||||
|
musics_[BASE_NAME] = m;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case Asset::Type::DATA:
|
||||||
|
loadDataAsset(BASE_NAME, bytes);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Asset::Type::FONT: // Fonts: se emparejan en pass 2
|
||||||
|
case Asset::Type::LANG: // Lenguaje: lo sigue leyendo la clase Lang via ResourceHelper
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Despacha un asset Asset::Type::DATA en función de la extensión / nombre
|
||||||
|
void Resource::loadDataAsset(const std::string &bname, const std::vector<uint8_t> &bytes) {
|
||||||
|
if (bname.size() >= 4 && bname.substr(bname.size() - 4) == ".ani") {
|
||||||
|
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
|
||||||
|
std::stringstream ss(content);
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
std::string line;
|
||||||
|
while (std::getline(ss, line)) {
|
||||||
|
// Normalitza CRLF perquè loadFromVector compari línies amb literals
|
||||||
|
// ("[animation]", "[/animation]") sense \r residual.
|
||||||
|
if (!line.empty() && line.back() == '\r') {
|
||||||
|
line.pop_back();
|
||||||
|
}
|
||||||
|
lines.push_back(line);
|
||||||
|
}
|
||||||
|
animation_lines_[bname] = std::move(lines);
|
||||||
|
} else if (bname.size() > 5 && bname.substr(0, 4) == "demo" && bname.substr(bname.size() - 4) == ".bin") {
|
||||||
|
// Acumula tots els demo*.bin (demo1.bin, demo2.bin, ...) en ordre d'aparicio
|
||||||
|
demo_bytes_.push_back(bytes);
|
||||||
|
}
|
||||||
|
// Menús (.men): se construyen en pass 2 porque dependen de textos y sonidos
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2a: construye Text por cada par basename.png + basename.txt
|
||||||
|
void Resource::preloadFonts() {
|
||||||
|
const auto &items = Asset::get()->getAll();
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::vector<uint8_t>> font_pngs;
|
||||||
|
std::unordered_map<std::string, std::vector<uint8_t>> font_txts;
|
||||||
|
for (const auto &it : items) {
|
||||||
|
if (it.type != Asset::Type::FONT) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto bytes = ResourceHelper::loadFile(it.file);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::string S = stem(it.file);
|
||||||
|
const std::string BASE_NAME = basename(it.file);
|
||||||
|
if (BASE_NAME.size() >= 4 && BASE_NAME.substr(BASE_NAME.size() - 4) == ".png") {
|
||||||
|
font_pngs[S] = std::move(bytes);
|
||||||
|
} else if (BASE_NAME.size() >= 4 && BASE_NAME.substr(BASE_NAME.size() - 4) == ".txt") {
|
||||||
|
font_txts[S] = std::move(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto &[s, png] : font_pngs) {
|
||||||
|
auto it_txt = font_txts.find(s);
|
||||||
|
if (it_txt == font_txts.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Text *t = new Text(png, it_txt->second, renderer_);
|
||||||
|
texts_[s] = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2b: construye los Menu (dependen de Text+sonidos cargados antes)
|
||||||
|
//
|
||||||
|
// NOTA: Menu::loadFromBytes aún llama internamente a asset->get() y Text/
|
||||||
|
// Ja::loadSound por path. Funciona en modo fallback; en pack estricto requiere
|
||||||
|
// que Menu se adapte a cargar desde ResourceHelper. Migración pendiente.
|
||||||
|
void Resource::preloadMenus() {
|
||||||
|
const auto &items = Asset::get()->getAll();
|
||||||
|
|
||||||
|
for (const auto &it : items) {
|
||||||
|
if (it.type != Asset::Type::DATA) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::string BASE_NAME = basename(it.file);
|
||||||
|
if (BASE_NAME.size() < 4 || BASE_NAME.substr(BASE_NAME.size() - 4) != ".men") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
auto bytes = ResourceHelper::loadFile(it.file);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Menu *m = new Menu(renderer_, "");
|
||||||
|
m->loadFromBytes(bytes, BASE_NAME);
|
||||||
|
const std::string S = stem(it.file);
|
||||||
|
menus_[S] = m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Resource::getTexture(const std::string &name) -> Texture * {
|
||||||
|
auto it = textures_.find(name);
|
||||||
|
if (it == textures_.end()) {
|
||||||
|
std::cerr << "Resource::getTexture: missing " << name << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Resource::getSound(const std::string &name) -> Ja::Sound * {
|
||||||
|
auto it = sounds_.find(name);
|
||||||
|
if (it == sounds_.end()) {
|
||||||
|
std::cerr << "Resource::getSound: missing " << name << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Resource::getMusic(const std::string &name) -> Ja::Music * {
|
||||||
|
auto it = musics_.find(name);
|
||||||
|
if (it == musics_.end()) {
|
||||||
|
std::cerr << "Resource::getMusic: missing " << name << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Resource::getAnimationLines(const std::string &name) -> std::vector<std::string> & {
|
||||||
|
auto it = animation_lines_.find(name);
|
||||||
|
if (it == animation_lines_.end()) {
|
||||||
|
static std::vector<std::string> empty_;
|
||||||
|
std::cerr << "Resource::getAnimationLines: missing " << name << '\n';
|
||||||
|
return empty_;
|
||||||
|
}
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Resource::getText(const std::string &name) -> Text * {
|
||||||
|
auto it = texts_.find(name);
|
||||||
|
if (it == texts_.end()) {
|
||||||
|
std::cerr << "Resource::getText: missing " << name << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Resource::getMenu(const std::string &name) -> Menu * {
|
||||||
|
auto it = menus_.find(name);
|
||||||
|
if (it == menus_.end()) {
|
||||||
|
std::cerr << "Resource::getMenu: missing " << name << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class Menu;
|
||||||
|
class Text;
|
||||||
|
class Texture;
|
||||||
|
namespace Ja {
|
||||||
|
struct Music;
|
||||||
|
struct Sound;
|
||||||
|
} // namespace Ja
|
||||||
|
|
||||||
|
// Precarga y posee todos los recursos del juego durante toda la vida de la app.
|
||||||
|
// Singleton inicializado desde Director; las escenas consultan handles via get*().
|
||||||
|
class Resource {
|
||||||
|
public:
|
||||||
|
static void init(SDL_Renderer *renderer);
|
||||||
|
static void destroy();
|
||||||
|
static auto get() -> Resource *;
|
||||||
|
|
||||||
|
auto getTexture(const std::string &name) -> Texture *;
|
||||||
|
auto getSound(const std::string &name) -> Ja::Sound *;
|
||||||
|
auto getMusic(const std::string &name) -> Ja::Music *;
|
||||||
|
auto getAnimationLines(const std::string &name) -> std::vector<std::string> &;
|
||||||
|
auto getText(const std::string &name) -> Text *; // name sin extensión: "smb2", "nokia2", ...
|
||||||
|
auto getMenu(const std::string &name) -> Menu *; // name sin extensión: "title", "options", ...
|
||||||
|
[[nodiscard]] auto getDemoCount() const -> size_t { return demo_bytes_.size(); }
|
||||||
|
[[nodiscard]] auto getDemoBytes(size_t index) const -> const std::vector<uint8_t> & { return demo_bytes_.at(index); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit Resource(SDL_Renderer *renderer);
|
||||||
|
~Resource();
|
||||||
|
|
||||||
|
void preloadAll();
|
||||||
|
|
||||||
|
// Helpers de preloadAll
|
||||||
|
void preloadResources();
|
||||||
|
void loadDataAsset(const std::string &bname, const std::vector<uint8_t> &bytes);
|
||||||
|
void preloadFonts();
|
||||||
|
void preloadMenus();
|
||||||
|
|
||||||
|
SDL_Renderer *renderer_;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, Texture *> textures_;
|
||||||
|
std::unordered_map<std::string, Ja::Sound *> sounds_;
|
||||||
|
std::unordered_map<std::string, Ja::Music *> musics_;
|
||||||
|
std::unordered_map<std::string, std::vector<std::string>> animation_lines_;
|
||||||
|
std::unordered_map<std::string, Text *> texts_;
|
||||||
|
std::unordered_map<std::string, Menu *> menus_;
|
||||||
|
std::vector<std::vector<uint8_t>> demo_bytes_;
|
||||||
|
|
||||||
|
static Resource *instance;
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#include "core/resources/resource_helper.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "core/resources/resource_loader.h"
|
||||||
|
|
||||||
|
namespace ResourceHelper {
|
||||||
|
static bool resource_system_initialized = false;
|
||||||
|
|
||||||
|
auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback) -> bool {
|
||||||
|
auto& loader = ResourceLoader::getInstance();
|
||||||
|
bool ok = loader.initialize(pack_file, enable_fallback);
|
||||||
|
resource_system_initialized = ok;
|
||||||
|
|
||||||
|
if (ok && loader.getLoadedResourceCount() > 0) {
|
||||||
|
std::cout << "Resource system initialized with pack: " << pack_file << '\n';
|
||||||
|
} else if (ok) {
|
||||||
|
std::cout << "Resource system using fallback mode (filesystem only)" << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
void shutdownResourceSystem() {
|
||||||
|
if (resource_system_initialized) {
|
||||||
|
ResourceLoader::getInstance().shutdown();
|
||||||
|
resource_system_initialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto loadFile(const std::string& filepath) -> std::vector<uint8_t> {
|
||||||
|
if (resource_system_initialized && shouldUseResourcePack(filepath)) {
|
||||||
|
auto& loader = ResourceLoader::getInstance();
|
||||||
|
std::string pack_path = getPackPath(filepath);
|
||||||
|
|
||||||
|
auto data = loader.loadResource(pack_path);
|
||||||
|
if (!data.empty()) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
|
||||||
|
if (!file) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::streamsize file_size = file.tellg();
|
||||||
|
file.seekg(0, std::ios::beg);
|
||||||
|
|
||||||
|
std::vector<uint8_t> data(file_size);
|
||||||
|
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto shouldUseResourcePack(const std::string& filepath) -> bool {
|
||||||
|
// Solo entran al pack los ficheros dentro de data/
|
||||||
|
return filepath.find("data/") != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto getPackPath(const std::string& asset_path) -> std::string {
|
||||||
|
std::string pack_path = asset_path;
|
||||||
|
std::ranges::replace(pack_path, '\\', '/');
|
||||||
|
|
||||||
|
// Toma la última aparición de "data/" como prefijo a quitar
|
||||||
|
size_t last_data = pack_path.rfind("data/");
|
||||||
|
if (last_data != std::string::npos) {
|
||||||
|
pack_path = pack_path.substr(last_data + 5);
|
||||||
|
}
|
||||||
|
return pack_path;
|
||||||
|
}
|
||||||
|
} // namespace ResourceHelper
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace ResourceHelper {
|
||||||
|
auto initializeResourceSystem(const std::string& pack_file = "resources.pack", bool enable_fallback = true) -> bool;
|
||||||
|
void shutdownResourceSystem();
|
||||||
|
|
||||||
|
auto loadFile(const std::string& filepath) -> std::vector<uint8_t>;
|
||||||
|
|
||||||
|
auto shouldUseResourcePack(const std::string& filepath) -> bool;
|
||||||
|
auto getPackPath(const std::string& asset_path) -> std::string;
|
||||||
|
} // namespace ResourceHelper
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#include "core/resources/resource_loader.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "core/resources/resource_pack.h"
|
||||||
|
|
||||||
|
std::unique_ptr<ResourceLoader> ResourceLoader::instance = nullptr;
|
||||||
|
|
||||||
|
ResourceLoader::ResourceLoader() = default;
|
||||||
|
|
||||||
|
auto ResourceLoader::getInstance() -> ResourceLoader& {
|
||||||
|
if (!instance) {
|
||||||
|
instance = std::unique_ptr<ResourceLoader>(new ResourceLoader());
|
||||||
|
}
|
||||||
|
return *instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceLoader::~ResourceLoader() {
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourceLoader::initialize(const std::string& pack_file, bool enable_fallback) -> bool {
|
||||||
|
shutdown();
|
||||||
|
|
||||||
|
fallback_to_files_ = enable_fallback;
|
||||||
|
pack_path_ = pack_file;
|
||||||
|
|
||||||
|
if (std::filesystem::exists(pack_file)) {
|
||||||
|
resource_pack_ = new ResourcePack();
|
||||||
|
if (resource_pack_->loadPack(pack_file)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
delete resource_pack_;
|
||||||
|
resource_pack_ = nullptr;
|
||||||
|
std::cerr << "Failed to load resource pack: " << pack_file << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallback_to_files_) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cerr << "Resource pack not found and fallback disabled: " << pack_file << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResourceLoader::shutdown() {
|
||||||
|
if (resource_pack_ != nullptr) {
|
||||||
|
delete resource_pack_;
|
||||||
|
resource_pack_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourceLoader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
|
||||||
|
if ((resource_pack_ != nullptr) && resource_pack_->hasResource(filename)) {
|
||||||
|
return resource_pack_->getResource(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallback_to_files_) {
|
||||||
|
return loadFromFile(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cerr << "Resource not found: " << filename << '\n';
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourceLoader::resourceExists(const std::string& filename) -> bool {
|
||||||
|
if ((resource_pack_ != nullptr) && resource_pack_->hasResource(filename)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallback_to_files_) {
|
||||||
|
std::string full_path = getDataPath(filename);
|
||||||
|
return std::filesystem::exists(full_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourceLoader::loadFromFile(const std::string& filename) -> std::vector<uint8_t> {
|
||||||
|
std::string full_path = getDataPath(filename);
|
||||||
|
|
||||||
|
std::ifstream file(full_path, std::ios::binary | std::ios::ate);
|
||||||
|
if (!file) {
|
||||||
|
std::cerr << "Error: Could not open file: " << full_path << '\n';
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::streamsize file_size = file.tellg();
|
||||||
|
file.seekg(0, std::ios::beg);
|
||||||
|
|
||||||
|
std::vector<uint8_t> data(file_size);
|
||||||
|
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
|
||||||
|
std::cerr << "Error: Could not read file: " << full_path << '\n';
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourceLoader::getDataPath(const std::string& filename) -> std::string {
|
||||||
|
return "data/" + filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourceLoader::getLoadedResourceCount() const -> size_t {
|
||||||
|
if (resource_pack_ != nullptr) {
|
||||||
|
return resource_pack_->getResourceCount();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourceLoader::getAvailableResources() const -> std::vector<std::string> {
|
||||||
|
if (resource_pack_ != nullptr) {
|
||||||
|
return resource_pack_->getResourceList();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> result;
|
||||||
|
if (fallback_to_files_ && std::filesystem::exists("data")) {
|
||||||
|
for (const auto& entry : std::filesystem::recursive_directory_iterator("data")) {
|
||||||
|
if (entry.is_regular_file()) {
|
||||||
|
std::string filename = std::filesystem::relative(entry.path(), "data").string();
|
||||||
|
std::ranges::replace(filename, '\\', '/');
|
||||||
|
result.push_back(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class ResourcePack;
|
||||||
|
|
||||||
|
class ResourceLoader {
|
||||||
|
public:
|
||||||
|
static auto getInstance() -> ResourceLoader&;
|
||||||
|
~ResourceLoader();
|
||||||
|
|
||||||
|
auto initialize(const std::string& pack_file, bool enable_fallback = true) -> bool;
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
|
auto loadResource(const std::string& filename) -> std::vector<uint8_t>;
|
||||||
|
auto resourceExists(const std::string& filename) -> bool;
|
||||||
|
|
||||||
|
void setFallbackToFiles(bool enable) { fallback_to_files_ = enable; }
|
||||||
|
[[nodiscard]] auto getFallbackToFiles() const -> bool { return fallback_to_files_; }
|
||||||
|
|
||||||
|
[[nodiscard]] auto getLoadedResourceCount() const -> size_t;
|
||||||
|
[[nodiscard]] auto getAvailableResources() const -> std::vector<std::string>;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ResourceLoader(); // Constructor privado (singleton)
|
||||||
|
|
||||||
|
static auto loadFromFile(const std::string& filename) -> std::vector<uint8_t>;
|
||||||
|
static auto getDataPath(const std::string& filename) -> std::string;
|
||||||
|
|
||||||
|
static std::unique_ptr<ResourceLoader> instance;
|
||||||
|
ResourcePack* resource_pack_{nullptr};
|
||||||
|
std::string pack_path_;
|
||||||
|
bool fallback_to_files_{true};
|
||||||
|
};
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
#include "core/resources/resource_pack.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <numeric>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
const std::string ResourcePack::DEFAULT_ENCRYPT_KEY = "CCRS_RESOURCES__2026";
|
||||||
|
|
||||||
|
ResourcePack::ResourcePack() = default;
|
||||||
|
|
||||||
|
ResourcePack::~ResourcePack() {
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourcePack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
|
||||||
|
return std::accumulate(data.begin(), data.end(), uint32_t(0x12345678), [](uint32_t acc, uint8_t b) { return ((acc << 5) + acc) + b; });
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResourcePack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
|
||||||
|
if (key.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < data.size(); ++i) {
|
||||||
|
data[i] ^= key[i % key.length()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResourcePack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
|
||||||
|
encryptData(data, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourcePack::loadPack(const std::string& pack_file) -> bool {
|
||||||
|
std::ifstream file(pack_file, std::ios::binary);
|
||||||
|
if (!file) {
|
||||||
|
std::cerr << "Error: Could not open pack file: " << pack_file << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<char, 4> header;
|
||||||
|
file.read(header.data(), 4);
|
||||||
|
if (std::string(header.data(), 4) != "CCRS") {
|
||||||
|
std::cerr << "Error: Invalid pack file format" << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t version;
|
||||||
|
file.read(reinterpret_cast<char*>(&version), sizeof(version));
|
||||||
|
if (version != 1) {
|
||||||
|
std::cerr << "Error: Unsupported pack version: " << version << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t resource_count;
|
||||||
|
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
|
||||||
|
|
||||||
|
resources_.clear();
|
||||||
|
resources_.reserve(resource_count);
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < resource_count; ++i) {
|
||||||
|
uint32_t filename_length;
|
||||||
|
file.read(reinterpret_cast<char*>(&filename_length), sizeof(filename_length));
|
||||||
|
|
||||||
|
std::string filename(filename_length, '\0');
|
||||||
|
file.read(filename.data(), filename_length);
|
||||||
|
|
||||||
|
ResourceEntry entry;
|
||||||
|
entry.filename = filename;
|
||||||
|
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
|
||||||
|
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
|
||||||
|
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
|
||||||
|
|
||||||
|
resources_[filename] = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t data_size;
|
||||||
|
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
|
||||||
|
|
||||||
|
data_.resize(data_size);
|
||||||
|
file.read(reinterpret_cast<char*>(data_.data()), data_size);
|
||||||
|
|
||||||
|
decryptData(data_, DEFAULT_ENCRYPT_KEY);
|
||||||
|
|
||||||
|
loaded_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourcePack::savePack(const std::string& pack_file) -> bool {
|
||||||
|
std::ofstream file(pack_file, std::ios::binary);
|
||||||
|
if (!file) {
|
||||||
|
std::cerr << "Error: Could not create pack file: " << pack_file << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.write("CCRS", 4);
|
||||||
|
|
||||||
|
uint32_t version = 1;
|
||||||
|
file.write(reinterpret_cast<const char*>(&version), sizeof(version));
|
||||||
|
|
||||||
|
auto resource_count = static_cast<uint32_t>(resources_.size());
|
||||||
|
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
|
||||||
|
|
||||||
|
for (const auto& [filename, entry] : resources_) {
|
||||||
|
auto filename_length = static_cast<uint32_t>(filename.length());
|
||||||
|
file.write(reinterpret_cast<const char*>(&filename_length), sizeof(filename_length));
|
||||||
|
file.write(filename.c_str(), filename_length);
|
||||||
|
|
||||||
|
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
|
||||||
|
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
|
||||||
|
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> encrypted_data = data_;
|
||||||
|
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
|
||||||
|
|
||||||
|
uint64_t data_size = encrypted_data.size();
|
||||||
|
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
|
||||||
|
file.write(reinterpret_cast<const char*>(encrypted_data.data()), data_size);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourcePack::addFile(const std::string& filename, const std::string& filepath) -> bool {
|
||||||
|
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
|
||||||
|
if (!file) {
|
||||||
|
std::cerr << "Error: Could not open file: " << filepath << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::streamsize file_size = file.tellg();
|
||||||
|
file.seekg(0, std::ios::beg);
|
||||||
|
|
||||||
|
std::vector<uint8_t> file_data(file_size);
|
||||||
|
if (!file.read(reinterpret_cast<char*>(file_data.data()), file_size)) {
|
||||||
|
std::cerr << "Error: Could not read file: " << filepath << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceEntry entry;
|
||||||
|
entry.filename = filename;
|
||||||
|
entry.offset = data_.size();
|
||||||
|
entry.size = file_data.size();
|
||||||
|
entry.checksum = calculateChecksum(file_data);
|
||||||
|
|
||||||
|
data_.insert(data_.end(), file_data.begin(), file_data.end());
|
||||||
|
resources_[filename] = entry;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourcePack::addDirectory(const std::string& directory) -> bool {
|
||||||
|
if (!std::filesystem::exists(directory)) {
|
||||||
|
std::cerr << "Error: Directory does not exist: " << directory << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto iter = std::filesystem::recursive_directory_iterator(directory);
|
||||||
|
return std::all_of(begin(iter), end(iter), [&](const std::filesystem::directory_entry& entry) {
|
||||||
|
if (!entry.is_regular_file()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::string filepath = entry.path().string();
|
||||||
|
std::string filename = std::filesystem::relative(entry.path(), directory).string();
|
||||||
|
std::ranges::replace(filename, '\\', '/');
|
||||||
|
return addFile(filename, filepath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourcePack::getResource(const std::string& filename) -> std::vector<uint8_t> {
|
||||||
|
auto it = resources_.find(filename);
|
||||||
|
if (it == resources_.end()) {
|
||||||
|
std::cerr << "Error: Resource not found: " << filename << '\n';
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResourceEntry& entry = it->second;
|
||||||
|
if (entry.offset + entry.size > data_.size()) {
|
||||||
|
std::cerr << "Error: Invalid resource data: " << filename << '\n';
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> result(data_.begin() + entry.offset,
|
||||||
|
data_.begin() + entry.offset + entry.size);
|
||||||
|
|
||||||
|
uint32_t checksum = calculateChecksum(result);
|
||||||
|
if (checksum != entry.checksum) {
|
||||||
|
std::cerr << "Warning: Checksum mismatch for resource: " << filename << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourcePack::hasResource(const std::string& filename) const -> bool {
|
||||||
|
return resources_.find(filename) != resources_.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResourcePack::clear() {
|
||||||
|
resources_.clear();
|
||||||
|
data_.clear();
|
||||||
|
loaded_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourcePack::getResourceCount() const -> size_t {
|
||||||
|
return resources_.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ResourcePack::getResourceList() const -> std::vector<std::string> {
|
||||||
|
std::vector<std::string> result;
|
||||||
|
result.reserve(resources_.size());
|
||||||
|
for (const auto& [filename, entry] : resources_) {
|
||||||
|
result.push_back(filename);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct ResourceEntry {
|
||||||
|
std::string filename;
|
||||||
|
uint64_t offset;
|
||||||
|
uint64_t size;
|
||||||
|
uint32_t checksum;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ResourcePack {
|
||||||
|
public:
|
||||||
|
ResourcePack();
|
||||||
|
~ResourcePack();
|
||||||
|
|
||||||
|
auto loadPack(const std::string& pack_file) -> bool;
|
||||||
|
auto savePack(const std::string& pack_file) -> bool;
|
||||||
|
|
||||||
|
auto addFile(const std::string& filename, const std::string& filepath) -> bool;
|
||||||
|
auto addDirectory(const std::string& directory) -> bool;
|
||||||
|
|
||||||
|
auto getResource(const std::string& filename) -> std::vector<uint8_t>;
|
||||||
|
auto hasResource(const std::string& filename) const -> bool;
|
||||||
|
|
||||||
|
void clear();
|
||||||
|
auto getResourceCount() const -> size_t;
|
||||||
|
auto getResourceList() const -> std::vector<std::string>;
|
||||||
|
|
||||||
|
static const std::string DEFAULT_ENCRYPT_KEY;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static auto calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t;
|
||||||
|
static void encryptData(std::vector<uint8_t>& data, const std::string& key);
|
||||||
|
static void decryptData(std::vector<uint8_t>& data, const std::string& key);
|
||||||
|
|
||||||
|
std::unordered_map<std::string, ResourceEntry> resources_;
|
||||||
|
std::vector<uint8_t> data_;
|
||||||
|
bool loaded_{false};
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#include "core/system/delta_time.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
namespace DeltaTime {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
Uint64 last_time_ms = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
last_time_ms = SDL_GetTicks();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto tick() -> float {
|
||||||
|
const Uint64 NOW_MS = SDL_GetTicks();
|
||||||
|
const float DELTA_S = static_cast<float>(NOW_MS - last_time_ms) / 1000.0F;
|
||||||
|
last_time_ms = NOW_MS;
|
||||||
|
return DELTA_S;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace DeltaTime
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Font única de delta_time per al joc. El loop principal NO té vsync ni
|
||||||
|
// gates: cada escena crida `tick()` al començament del seu iterate() i rep
|
||||||
|
// els segons reals transcorreguts des de l'última crida. Així el moviment és
|
||||||
|
// independent del framerate (visualment suau a 2000 FPS o a 60 FPS).
|
||||||
|
//
|
||||||
|
// `reset()` reinicia el rellotge intern: cal cridar-lo en cada canvi
|
||||||
|
// d'escena (després de càrregues llargues que podrien generar un primer
|
||||||
|
// delta enorme) i quan es reprèn d'una pausa.
|
||||||
|
|
||||||
|
namespace DeltaTime {
|
||||||
|
|
||||||
|
// Reinicia el rellotge a "ara". Cap delta acumulat del passat.
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// Retorna els segons des de l'última crida a `tick()` o `reset()`.
|
||||||
|
auto tick() -> float;
|
||||||
|
|
||||||
|
} // namespace DeltaTime
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#include "core/system/demo.hpp"
|
||||||
|
|
||||||
|
#include <cstring> // for memcpy
|
||||||
|
|
||||||
|
// Desempaqueta un blob binari amb TOTAL_DEMO_DATA registres consecutius
|
||||||
|
// de DemoKeys (struct POD de 6 bytes). Si el blob no te la mida esperada,
|
||||||
|
// torna un vector buit perque el playback el detecti i no peti.
|
||||||
|
auto loadDemoDataFromBytes(const std::vector<uint8_t> &bytes) -> DemoData {
|
||||||
|
DemoData dd;
|
||||||
|
const size_t EXPECTED = sizeof(DemoKeys) * TOTAL_DEMO_DATA;
|
||||||
|
if (bytes.size() < EXPECTED) {
|
||||||
|
return dd;
|
||||||
|
}
|
||||||
|
dd.resize(TOTAL_DEMO_DATA);
|
||||||
|
std::memcpy(dd.data(), bytes.data(), EXPECTED);
|
||||||
|
return dd;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Total de "frames" gravats a 60Hz de referencia. Equival a 2000/60 ~ 33.3s
|
||||||
|
// reals, independentment del refresc real perque el playback es time-based
|
||||||
|
// (index = elapsed_s * 60).
|
||||||
|
constexpr int TOTAL_DEMO_DATA = 2000;
|
||||||
|
|
||||||
|
// Pulsacions per frame de referencia gravades a disc / reproduides al playback.
|
||||||
|
struct DemoKeys {
|
||||||
|
Uint8 left;
|
||||||
|
Uint8 right;
|
||||||
|
Uint8 no_input;
|
||||||
|
Uint8 fire;
|
||||||
|
Uint8 fire_left;
|
||||||
|
Uint8 fire_right;
|
||||||
|
|
||||||
|
explicit DemoKeys(Uint8 l = 0, Uint8 r = 0, Uint8 ni = 0, Uint8 f = 0, Uint8 fl = 0, Uint8 fr = 0)
|
||||||
|
: left(l),
|
||||||
|
right(r),
|
||||||
|
no_input(ni),
|
||||||
|
fire(f),
|
||||||
|
fire_left(fl),
|
||||||
|
fire_right(fr) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Una demo completa: vector de frames.
|
||||||
|
using DemoData = std::vector<DemoKeys>;
|
||||||
|
|
||||||
|
// Estat del subsistema de demo dins de Game.
|
||||||
|
struct Demo {
|
||||||
|
bool enabled{false}; // Mode demo actiu (reproduccio)
|
||||||
|
bool recording{false}; // Mode gravacio actiu
|
||||||
|
float elapsed_s{0.0F}; // Temps acumulat des de l'inici de la demo
|
||||||
|
int index{0}; // index = elapsed_s * 60 (derivat)
|
||||||
|
DemoKeys keys; // Buffer de tecles del frame actual (gravacio)
|
||||||
|
std::vector<DemoData> data; // Vector de sets de demo carregats (multi-fitxer)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Carrega un fitxer .bin (TOTAL_DEMO_DATA * sizeof(DemoKeys) bytes) i
|
||||||
|
// retorna el DemoData. Si el fitxer no es troba, retorna un DemoData buit.
|
||||||
|
auto loadDemoDataFromBytes(const std::vector<uint8_t> &bytes) -> DemoData;
|
||||||
@@ -0,0 +1,687 @@
|
|||||||
|
#include "core/system/director.h"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cerrno> // for errno, EEXIST, EACCES, ENAMETOO...
|
||||||
|
#include <cstdio> // for printf, perror
|
||||||
|
#include <cstring> // for strcmp
|
||||||
|
#ifndef __EMSCRIPTEN__
|
||||||
|
#include <sys/stat.h> // for mkdir, stat, S_IRWXU
|
||||||
|
#include <unistd.h> // for getuid
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <cstdlib> // for exit, EXIT_FAILURE, srand
|
||||||
|
#include <filesystem>
|
||||||
|
#include <iostream> // for cout
|
||||||
|
#include <memory>
|
||||||
|
#include <string> // for basic_string, operator+, char_t...
|
||||||
|
|
||||||
|
#include "core/audio/audio.hpp" // for Audio::init, Audio::destroy
|
||||||
|
#include "core/input/global_inputs.hpp" // for GlobalInputs::wantsQuit
|
||||||
|
#include "core/input/input.h" // for Input, InputAction
|
||||||
|
#include "core/input/mouse.hpp" // for Mouse::handleEvent, Mouse::upda...
|
||||||
|
#include "core/locale/lang.h" // for Lang, Lang::Code
|
||||||
|
#include "core/rendering/notifications.hpp" // for Notifications::show
|
||||||
|
#include "core/rendering/screen.h" // for Screen
|
||||||
|
#include "core/rendering/texture.h" // for Texture
|
||||||
|
#include "core/resources/asset.h" // for Asset, Asset::Type
|
||||||
|
#include "core/resources/resource.h"
|
||||||
|
#include "core/resources/resource_helper.h"
|
||||||
|
#include "game/defaults.hpp" // for SECTION_PROG_LOGO, GAMECANVAS_H...
|
||||||
|
#include "game/game.h" // for Game
|
||||||
|
#include "game/options.hpp" // for Options::init, loadFromFile...
|
||||||
|
#include "game/scenes/intro.h" // for Intro
|
||||||
|
#include "game/scenes/logo.h" // for Logo
|
||||||
|
#include "game/scenes/title.h" // for Title
|
||||||
|
#include "utils/utils.h" // for InputDevice, boolToString
|
||||||
|
|
||||||
|
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
|
||||||
|
#include <pwd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Director::Director(int argc, const char *argv[]) {
|
||||||
|
std::cout << "Game start" << '\n';
|
||||||
|
// Inicializa variables
|
||||||
|
section_ = new Section();
|
||||||
|
section_->name = SECTION_PROG_LOGO;
|
||||||
|
|
||||||
|
// Inicializa las opciones del programa (defaults + dispositivos d'entrada)
|
||||||
|
Options::init();
|
||||||
|
|
||||||
|
// Obtén la ruta del directori on viu l'executable (acabada amb '/').
|
||||||
|
// SDL_GetBasePath és independent del CWD i evita el `argv[0]` poc fiable.
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
// En Emscripten els assets viuen a l'arrel del MEMFS — no hi ha ruta real.
|
||||||
|
executablePath = "";
|
||||||
|
#else
|
||||||
|
const char *base_path = SDL_GetBasePath();
|
||||||
|
executable_path_ = (base_path != nullptr) ? base_path : "";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Comprueba los parametros del programa (pot activar console)
|
||||||
|
checkProgramArguments(argc, argv);
|
||||||
|
|
||||||
|
// Crea la carpeta del sistema donde guardar datos
|
||||||
|
createSystemFolder("jailgames");
|
||||||
|
#ifndef DEBUG
|
||||||
|
createSystemFolder("jailgames/coffee_crisis");
|
||||||
|
#else
|
||||||
|
createSystemFolder("jailgames/coffee_crisis_debug");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Estableix el fitxer de configuració i carrega les opcions (o crea el
|
||||||
|
// YAML amb defaults si no existeix).
|
||||||
|
Options::setConfigFile(system_folder_ + "/config.yaml");
|
||||||
|
Options::loadFromFile();
|
||||||
|
|
||||||
|
// Presets de shaders (creats amb defaults si no existeixen).
|
||||||
|
Options::setPostFXFile(system_folder_ + "/postfx.yaml");
|
||||||
|
Options::loadPostFXFromFile();
|
||||||
|
Options::setCrtPiFile(system_folder_ + "/crtpi.yaml");
|
||||||
|
Options::loadCrtPiFromFile();
|
||||||
|
|
||||||
|
// Inicializa el sistema de recursos (pack + fallback).
|
||||||
|
// En wasm siempre se usa filesystem (MEMFS) porque el propio --preload-file
|
||||||
|
// de emscripten ya empaqueta data/ — no hay resources.pack.
|
||||||
|
{
|
||||||
|
#if defined(__EMSCRIPTEN__)
|
||||||
|
const bool ENABLE_FALLBACK = true;
|
||||||
|
#elif defined(RELEASE_BUILD)
|
||||||
|
const bool ENABLE_FALLBACK = false;
|
||||||
|
#else
|
||||||
|
const bool ENABLE_FALLBACK = true;
|
||||||
|
#endif
|
||||||
|
#ifdef MACOS_BUNDLE
|
||||||
|
const std::string PACK_PATH = executablePath + "../Resources/resources.pack";
|
||||||
|
#else
|
||||||
|
const std::string PACK_PATH = executable_path_ + "resources.pack";
|
||||||
|
#endif
|
||||||
|
if (!ResourceHelper::initializeResourceSystem(PACK_PATH, ENABLE_FALLBACK)) {
|
||||||
|
std::cerr << "Fatal: resource system init failed (missing resources.pack?)" << '\n';
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea el objeto que controla los ficheros de recursos
|
||||||
|
Asset::init(executable_path_);
|
||||||
|
Asset::get()->setVerbose(Options::settings.console);
|
||||||
|
|
||||||
|
// Si falta algún fichero no inicia el programa
|
||||||
|
if (!setFileList()) {
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializa SDL
|
||||||
|
initSDL();
|
||||||
|
|
||||||
|
// Inicializa JailAudio
|
||||||
|
initJailAudio();
|
||||||
|
|
||||||
|
// Establece el modo de escalado de texturas
|
||||||
|
Texture::setGlobalScaleMode(Options::video.scale_mode);
|
||||||
|
|
||||||
|
// Crea los objetos
|
||||||
|
Lang::init();
|
||||||
|
Lang::get()->setLang(Options::settings.language);
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
Input::init("/gamecontrollerdb.txt");
|
||||||
|
#else
|
||||||
|
{
|
||||||
|
const std::string BIN_DIR = std::filesystem::path(executable_path_).parent_path().string();
|
||||||
|
#ifdef MACOS_BUNDLE
|
||||||
|
Input::init(BIN_DIR + "/../Resources/gamecontrollerdb.txt");
|
||||||
|
#else
|
||||||
|
Input::init(BIN_DIR + "/gamecontrollerdb.txt");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
initInput();
|
||||||
|
|
||||||
|
// Orden importante: Screen + initShaders ANTES de Resource::init.
|
||||||
|
// Si `Resource::init` se ejecuta primero, carga ~100 texturas vía
|
||||||
|
// `SDL_CreateTexture` que dejan el SDL_Renderer con el swapchain en un
|
||||||
|
// estado que hace crashear al driver Vulkan cuando después `initShaders`
|
||||||
|
// intenta reclamar la ventana para el dispositivo SDL3 GPU.
|
||||||
|
//
|
||||||
|
// Por eso el constructor de Screen NO carga notificationText desde
|
||||||
|
// Resource; se enlaza después vía `Screen::get()->initNotifications()`.
|
||||||
|
Screen::init(window_, renderer_);
|
||||||
|
|
||||||
|
#ifndef NO_SHADERS
|
||||||
|
if (Options::video.gpu.acceleration) {
|
||||||
|
Screen::get()->initShaders();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Ahora sí, precarga todos los recursos en memoria (texturas, sonidos,
|
||||||
|
// música, ...). Vivirán durante toda la vida de la app.
|
||||||
|
Resource::init(renderer_);
|
||||||
|
|
||||||
|
// Completa el enlazado de Screen con recursos que necesitan Resource
|
||||||
|
// inicializado (actualmente sólo el Text de las notificaciones).
|
||||||
|
Screen::get()->initNotifications();
|
||||||
|
|
||||||
|
active_section_ = ActiveSection::NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Director::~Director() {
|
||||||
|
Options::saveToFile();
|
||||||
|
|
||||||
|
// Libera las secciones primero: sus destructores tocan audio/render SDL
|
||||||
|
// (p.ej. Intro::~Intro llama a Ja::deleteMusic) y deben ejecutarse antes
|
||||||
|
// de SDL_Quit().
|
||||||
|
logo_.reset();
|
||||||
|
intro_.reset();
|
||||||
|
title_.reset();
|
||||||
|
game_.reset();
|
||||||
|
|
||||||
|
// Screen puede tener referencias a Text propiedad de Resource: destruir
|
||||||
|
// Screen antes que Resource.
|
||||||
|
Screen::destroy();
|
||||||
|
|
||||||
|
// Libera todos los recursos precargados antes de cerrar SDL.
|
||||||
|
Resource::destroy();
|
||||||
|
|
||||||
|
Asset::destroy();
|
||||||
|
Input::destroy();
|
||||||
|
Lang::destroy();
|
||||||
|
delete section_;
|
||||||
|
|
||||||
|
Audio::destroy();
|
||||||
|
|
||||||
|
SDL_DestroyRenderer(renderer_);
|
||||||
|
SDL_DestroyWindow(window_);
|
||||||
|
|
||||||
|
SDL_Quit();
|
||||||
|
|
||||||
|
ResourceHelper::shutdownResourceSystem();
|
||||||
|
|
||||||
|
std::cout << "\nBye!" << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializa el objeto input
|
||||||
|
void Director::initInput() {
|
||||||
|
// Establece si ha de mostrar mensajes
|
||||||
|
Input::get()->setVerbose(Options::settings.console);
|
||||||
|
|
||||||
|
// Busca si hay un mando conectado
|
||||||
|
Input::get()->discoverGameController();
|
||||||
|
|
||||||
|
// Teclado - Movimiento del jugador
|
||||||
|
Input::get()->bindKey(Input::Action::UP, SDL_SCANCODE_UP);
|
||||||
|
Input::get()->bindKey(Input::Action::DOWN, SDL_SCANCODE_DOWN);
|
||||||
|
Input::get()->bindKey(Input::Action::LEFT, SDL_SCANCODE_LEFT);
|
||||||
|
Input::get()->bindKey(Input::Action::RIGHT, SDL_SCANCODE_RIGHT);
|
||||||
|
Input::get()->bindKey(Input::Action::FIRE_LEFT, SDL_SCANCODE_Q);
|
||||||
|
Input::get()->bindKey(Input::Action::FIRE_CENTER, SDL_SCANCODE_W);
|
||||||
|
Input::get()->bindKey(Input::Action::FIRE_RIGHT, SDL_SCANCODE_E);
|
||||||
|
|
||||||
|
// Teclado - Otros
|
||||||
|
Input::get()->bindKey(Input::Action::ACCEPT, SDL_SCANCODE_RETURN);
|
||||||
|
// ESC només dispara EXIT (gestionat globalment per GlobalInputs com a
|
||||||
|
// confirmació de doble pulsació). PAUSE i CANCEL tenen tecles dedicades
|
||||||
|
// perquè cap escena ha de tractar ESC localment.
|
||||||
|
Input::get()->bindKey(Input::Action::EXIT, SDL_SCANCODE_ESCAPE);
|
||||||
|
Input::get()->bindKey(Input::Action::CANCEL, SDL_SCANCODE_BACKSPACE);
|
||||||
|
Input::get()->bindKey(Input::Action::PAUSE, SDL_SCANCODE_F12);
|
||||||
|
Input::get()->bindKey(Input::Action::WINDOW_DEC_ZOOM, SDL_SCANCODE_F1);
|
||||||
|
Input::get()->bindKey(Input::Action::WINDOW_INC_ZOOM, SDL_SCANCODE_F2);
|
||||||
|
Input::get()->bindKey(Input::Action::WINDOW_FULLSCREEN, SDL_SCANCODE_F3);
|
||||||
|
Input::get()->bindKey(Input::Action::TOGGLE_SHADER, SDL_SCANCODE_F4);
|
||||||
|
Input::get()->bindKey(Input::Action::TOGGLE_SHADER_TYPE, SDL_SCANCODE_F5);
|
||||||
|
Input::get()->bindKey(Input::Action::NEXT_SHADER_PRESET, SDL_SCANCODE_F6);
|
||||||
|
Input::get()->bindKey(Input::Action::TOGGLE_VSYNC, SDL_SCANCODE_F7);
|
||||||
|
Input::get()->bindKey(Input::Action::NEXT_PRESENTATION_MODE, SDL_SCANCODE_F8);
|
||||||
|
Input::get()->bindKey(Input::Action::TOGGLE_FPS, SDL_SCANCODE_F10);
|
||||||
|
Input::get()->bindKey(Input::Action::SHOW_VERSION, SDL_SCANCODE_F11);
|
||||||
|
|
||||||
|
// Mando - Movimiento del jugador
|
||||||
|
Input::get()->bindGameControllerButton(Input::Action::UP, SDL_GAMEPAD_BUTTON_DPAD_UP);
|
||||||
|
Input::get()->bindGameControllerButton(Input::Action::DOWN, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
|
||||||
|
Input::get()->bindGameControllerButton(Input::Action::LEFT, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
||||||
|
Input::get()->bindGameControllerButton(Input::Action::RIGHT, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
|
||||||
|
Input::get()->bindGameControllerButton(Input::Action::FIRE_LEFT, SDL_GAMEPAD_BUTTON_WEST);
|
||||||
|
Input::get()->bindGameControllerButton(Input::Action::FIRE_CENTER, SDL_GAMEPAD_BUTTON_NORTH);
|
||||||
|
Input::get()->bindGameControllerButton(Input::Action::FIRE_RIGHT, SDL_GAMEPAD_BUTTON_EAST);
|
||||||
|
|
||||||
|
// Mando - Otros
|
||||||
|
// SOUTH queda sin asignar para evitar salidas accidentales: pausa/cancel se hace con START/BACK.
|
||||||
|
Input::get()->bindGameControllerButton(Input::Action::ACCEPT, SDL_GAMEPAD_BUTTON_EAST);
|
||||||
|
#ifdef GAME_CONSOLE
|
||||||
|
Input::get()->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_BACK);
|
||||||
|
Input::get()->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_START);
|
||||||
|
#else
|
||||||
|
Input::get()->bindGameControllerButton(Input::Action::PAUSE, SDL_GAMEPAD_BUTTON_START);
|
||||||
|
Input::get()->bindGameControllerButton(Input::Action::EXIT, SDL_GAMEPAD_BUTTON_BACK);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializa JailAudio
|
||||||
|
void Director::initJailAudio() {
|
||||||
|
Audio::init();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arranca SDL y crea la ventana
|
||||||
|
auto Director::initSDL() -> bool {
|
||||||
|
// Indicador de éxito
|
||||||
|
bool success = true;
|
||||||
|
|
||||||
|
// Inicializa SDL
|
||||||
|
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD)) {
|
||||||
|
if (Options::settings.console) {
|
||||||
|
std::cout << "SDL could not initialize!\nSDL Error: " << SDL_GetError() << '\n';
|
||||||
|
}
|
||||||
|
success = false;
|
||||||
|
} else {
|
||||||
|
// Inicia el generador de numeros aleatorios
|
||||||
|
std::srand(static_cast<unsigned int>(SDL_GetTicks()));
|
||||||
|
|
||||||
|
// Calcula el zoom màxim windowed segons el display actual i clampa
|
||||||
|
// `Options::window.zoom` abans de crear la finestra.
|
||||||
|
Screen::detectMaxZoom();
|
||||||
|
|
||||||
|
// Crea la ventana
|
||||||
|
window_ = SDL_CreateWindow(
|
||||||
|
Options::window.caption.c_str(),
|
||||||
|
GAMECANVAS_WIDTH * Options::window.zoom,
|
||||||
|
GAMECANVAS_HEIGHT * Options::window.zoom,
|
||||||
|
0);
|
||||||
|
if (window_ == nullptr) {
|
||||||
|
if (Options::settings.console) {
|
||||||
|
std::cout << "Window could not be created!\nSDL Error: " << SDL_GetError() << '\n';
|
||||||
|
}
|
||||||
|
success = false;
|
||||||
|
} else {
|
||||||
|
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||||
|
|
||||||
|
// Crea un renderizador para la ventana
|
||||||
|
renderer_ = SDL_CreateRenderer(window_, nullptr);
|
||||||
|
|
||||||
|
if (renderer_ == nullptr) {
|
||||||
|
if (Options::settings.console) {
|
||||||
|
std::cout << "Renderer could not be created!\nSDL Error: " << SDL_GetError() << '\n';
|
||||||
|
}
|
||||||
|
success = false;
|
||||||
|
} else {
|
||||||
|
// Modo de blending por defecto (consistente con CCAE):
|
||||||
|
// permite alpha blending para fades y notificaciones.
|
||||||
|
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
|
||||||
|
|
||||||
|
// Activa vsync si es necesario
|
||||||
|
if (Options::video.vsync) {
|
||||||
|
SDL_SetRenderVSync(renderer_, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializa el color de renderizado
|
||||||
|
SDL_SetRenderDrawColor(renderer_, 0x00, 0x00, 0x00, 0xFF);
|
||||||
|
|
||||||
|
// Establece el tamaño del buffer de renderizado
|
||||||
|
SDL_SetRenderLogicalPresentation(renderer_, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT, SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||||
|
|
||||||
|
// Establece el modo de mezcla
|
||||||
|
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Options::settings.console) {
|
||||||
|
std::cout << '\n';
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea el indice de ficheros
|
||||||
|
auto Director::setFileList() -> bool {
|
||||||
|
#ifdef MACOS_BUNDLE
|
||||||
|
const std::string PREFIX = "/../Resources";
|
||||||
|
#else
|
||||||
|
const std::string PREFIX;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Ficheros de configuración
|
||||||
|
Asset::get()->add(system_folder_ + "/score.bin", Asset::Type::DATA, false, true);
|
||||||
|
Asset::get()->add(PREFIX + "/data/demo/demo1.bin", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/demo/demo2.bin", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/demo/demo3.bin", Asset::Type::DATA);
|
||||||
|
|
||||||
|
// Musicas
|
||||||
|
Asset::get()->add(PREFIX + "/data/music/intro.ogg", Asset::Type::MUSIC);
|
||||||
|
Asset::get()->add(PREFIX + "/data/music/playing.ogg", Asset::Type::MUSIC);
|
||||||
|
Asset::get()->add(PREFIX + "/data/music/title.ogg", Asset::Type::MUSIC);
|
||||||
|
|
||||||
|
// Sonidos
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/balloon.wav", Asset::Type::SOUND);
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/bubble1.wav", Asset::Type::SOUND);
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/bubble2.wav", Asset::Type::SOUND);
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/bubble3.wav", Asset::Type::SOUND);
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/bubble4.wav", Asset::Type::SOUND);
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/bullet.wav", Asset::Type::SOUND);
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/coffeeout.wav", Asset::Type::SOUND);
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/hiscore.wav", Asset::Type::SOUND);
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/itemdrop.wav", Asset::Type::SOUND);
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/itempickup.wav", Asset::Type::SOUND);
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/menu_move.wav", Asset::Type::SOUND);
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/menu_select.wav", Asset::Type::SOUND);
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/player_collision.wav", Asset::Type::SOUND);
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/stage_change.wav", Asset::Type::SOUND);
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/title.wav", Asset::Type::SOUND);
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/clock.wav", Asset::Type::SOUND);
|
||||||
|
Asset::get()->add(PREFIX + "/data/sound/powerball.wav", Asset::Type::SOUND);
|
||||||
|
|
||||||
|
// Texturas
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/balloon1.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/balloon1.ani", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/balloon2.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/balloon2.ani", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/balloon3.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/balloon3.ani", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/balloon4.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/balloon4.ani", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/bullet.png", Asset::Type::BITMAP);
|
||||||
|
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/game_buildings.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/game_clouds.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/game_grass.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/game_power_meter.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/game_sky_colors.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/game_text.png", Asset::Type::BITMAP);
|
||||||
|
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/intro.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/logo.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/menu_game_over.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/menu_game_over_end.png", Asset::Type::BITMAP);
|
||||||
|
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/item_points1_disk.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/item_points1_disk.ani", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/item_points2_gavina.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/item_points2_gavina.ani", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/item_points3_pacmar.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/item_points3_pacmar.ani", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/item_clock.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/item_clock.ani", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/item_coffee.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/item_coffee.ani", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/item_coffee_machine.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/item_coffee_machine.ani", Asset::Type::DATA);
|
||||||
|
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/title_bg_tile.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/title_coffee.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/title_crisis.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/title_dust.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/title_dust.ani", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/title_gradient.png", Asset::Type::BITMAP);
|
||||||
|
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/player_head.ani", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/player_body.ani", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/player_legs.ani", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/player_death.ani", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/player_fire.ani", Asset::Type::DATA);
|
||||||
|
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/player_bal1_head.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/player_bal1_body.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/player_bal1_legs.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/player_bal1_death.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/player_bal1_fire.png", Asset::Type::BITMAP);
|
||||||
|
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/player_arounder_head.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/player_arounder_body.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/player_arounder_legs.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/player_arounder_death.png", Asset::Type::BITMAP);
|
||||||
|
Asset::get()->add(PREFIX + "/data/gfx/player_arounder_fire.png", Asset::Type::BITMAP);
|
||||||
|
|
||||||
|
// Fuentes
|
||||||
|
Asset::get()->add(PREFIX + "/data/font/8bithud.png", Asset::Type::FONT);
|
||||||
|
Asset::get()->add(PREFIX + "/data/font/8bithud.txt", Asset::Type::FONT);
|
||||||
|
Asset::get()->add(PREFIX + "/data/font/nokia.png", Asset::Type::FONT);
|
||||||
|
Asset::get()->add(PREFIX + "/data/font/nokia_big2.png", Asset::Type::FONT);
|
||||||
|
Asset::get()->add(PREFIX + "/data/font/nokia.txt", Asset::Type::FONT);
|
||||||
|
Asset::get()->add(PREFIX + "/data/font/nokia2.png", Asset::Type::FONT);
|
||||||
|
Asset::get()->add(PREFIX + "/data/font/nokia2.txt", Asset::Type::FONT);
|
||||||
|
Asset::get()->add(PREFIX + "/data/font/nokia_big2.txt", Asset::Type::FONT);
|
||||||
|
Asset::get()->add(PREFIX + "/data/font/smb2_big.png", Asset::Type::FONT);
|
||||||
|
Asset::get()->add(PREFIX + "/data/font/smb2_big.txt", Asset::Type::FONT);
|
||||||
|
Asset::get()->add(PREFIX + "/data/font/smb2.png", Asset::Type::FONT);
|
||||||
|
Asset::get()->add(PREFIX + "/data/font/smb2.txt", Asset::Type::FONT);
|
||||||
|
|
||||||
|
// Textos
|
||||||
|
Asset::get()->add(PREFIX + "/data/lang/es_ES.txt", Asset::Type::LANG);
|
||||||
|
Asset::get()->add(PREFIX + "/data/lang/en_UK.txt", Asset::Type::LANG);
|
||||||
|
Asset::get()->add(PREFIX + "/data/lang/ba_BA.txt", Asset::Type::LANG);
|
||||||
|
|
||||||
|
// Menus
|
||||||
|
Asset::get()->add(PREFIX + "/data/menu/title.men", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/menu/title_gc.men", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/menu/options.men", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/menu/options_gc.men", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/menu/pause.men", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/menu/gameover.men", Asset::Type::DATA);
|
||||||
|
Asset::get()->add(PREFIX + "/data/menu/player_select.men", Asset::Type::DATA);
|
||||||
|
|
||||||
|
return Asset::get()->check();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba los parametros del programa
|
||||||
|
void Director::checkProgramArguments(int argc, const char *argv[]) {
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
if (strcmp(argv[i], "--console") == 0) {
|
||||||
|
Options::settings.console = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea la carpeta del sistema donde guardar datos
|
||||||
|
void Director::createSystemFolder(const std::string &folder) {
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
// En Emscripten usamos una carpeta en MEMFS (no persistente)
|
||||||
|
system_folder_ = "/config/" + folder;
|
||||||
|
#elif _WIN32
|
||||||
|
system_folder_ = std::string(getenv("APPDATA")) + "/" + folder;
|
||||||
|
#elif __APPLE__
|
||||||
|
struct passwd *pw = getpwuid(getuid());
|
||||||
|
const char *homedir = pw->pw_dir;
|
||||||
|
system_folder_ = std::string(homedir) + "/Library/Application Support" + "/" + folder;
|
||||||
|
#elif __linux__
|
||||||
|
struct passwd *pw = getpwuid(getuid());
|
||||||
|
const char *homedir = pw->pw_dir;
|
||||||
|
system_folder_ = std::string(homedir) + "/.config/" + folder;
|
||||||
|
|
||||||
|
{
|
||||||
|
// Intenta crear ".config", per si no existeix
|
||||||
|
std::string config_base_folder = std::string(homedir) + "/.config";
|
||||||
|
int ret = mkdir(config_base_folder.c_str(), S_IRWXU);
|
||||||
|
if (ret == -1 && errno != EEXIST) {
|
||||||
|
printf("ERROR CREATING CONFIG BASE FOLDER.");
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
// En Emscripten no necesitamos crear carpetas (MEMFS las crea automáticamente)
|
||||||
|
(void)folder;
|
||||||
|
#else
|
||||||
|
struct stat st{};
|
||||||
|
if (stat(system_folder_.c_str(), &st) == -1) {
|
||||||
|
errno = 0;
|
||||||
|
#ifdef _WIN32
|
||||||
|
int ret = mkdir(system_folder_.c_str());
|
||||||
|
#else
|
||||||
|
int ret = mkdir(system_folder_.c_str(), S_IRWXU);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (ret == -1) {
|
||||||
|
switch (errno) {
|
||||||
|
case EACCES:
|
||||||
|
printf("the parent directory does not allow write");
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
|
||||||
|
case EEXIST:
|
||||||
|
printf("pathname already exists");
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
|
||||||
|
case ENAMETOOLONG:
|
||||||
|
printf("pathname is too long");
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
|
||||||
|
default:
|
||||||
|
perror("mkdir");
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestiona las transiciones entre secciones
|
||||||
|
void Director::handleSectionTransition() {
|
||||||
|
// Determina qué sección debería estar activa
|
||||||
|
ActiveSection target_section = ActiveSection::NONE;
|
||||||
|
switch (section_->name) {
|
||||||
|
case SECTION_PROG_LOGO:
|
||||||
|
target_section = ActiveSection::LOGO;
|
||||||
|
break;
|
||||||
|
case SECTION_PROG_INTRO:
|
||||||
|
target_section = ActiveSection::INTRO;
|
||||||
|
break;
|
||||||
|
case SECTION_PROG_TITLE:
|
||||||
|
target_section = ActiveSection::TITLE;
|
||||||
|
break;
|
||||||
|
case SECTION_PROG_GAME:
|
||||||
|
target_section = ActiveSection::GAME;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si no ha cambiado, no hay nada que hacer
|
||||||
|
if (target_section == active_section_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destruye la sección anterior
|
||||||
|
logo_.reset();
|
||||||
|
intro_.reset();
|
||||||
|
title_.reset();
|
||||||
|
game_.reset();
|
||||||
|
|
||||||
|
// Crea la nueva sección
|
||||||
|
active_section_ = target_section;
|
||||||
|
switch (active_section_) {
|
||||||
|
case ActiveSection::LOGO:
|
||||||
|
logo_ = std::make_unique<Logo>(renderer_, section_);
|
||||||
|
break;
|
||||||
|
case ActiveSection::INTRO:
|
||||||
|
intro_ = std::make_unique<Intro>(renderer_, section_);
|
||||||
|
break;
|
||||||
|
case ActiveSection::TITLE:
|
||||||
|
title_ = std::make_unique<Title>(renderer_, section_);
|
||||||
|
break;
|
||||||
|
case ActiveSection::GAME: {
|
||||||
|
const int NUM_PLAYERS = section_->subsection == SUBSECTION_GAME_PLAY_1P ? 1 : 2;
|
||||||
|
game_ = std::make_unique<Game>(NUM_PLAYERS, 0, renderer_, false, section_);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ActiveSection::NONE:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ejecuta un frame del juego
|
||||||
|
auto Director::iterate() -> SDL_AppResult {
|
||||||
|
#ifndef __EMSCRIPTEN__
|
||||||
|
// Doble pulsació d'ESC confirmada des de qualsevol escena.
|
||||||
|
if (GlobalInputs::wantsQuit()) {
|
||||||
|
section_->name = SECTION_PROG_QUIT;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
// En WASM no se puede salir: reinicia al logo
|
||||||
|
if (section->name == SECTION_PROG_QUIT) {
|
||||||
|
section->name = SECTION_PROG_LOGO;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (section_->name == SECTION_PROG_QUIT) {
|
||||||
|
return SDL_APP_SUCCESS;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Actualiza la visibilidad del cursor del ratón
|
||||||
|
Mouse::updateCursorVisibility(Options::video.fullscreen);
|
||||||
|
|
||||||
|
// Gestiona las transiciones entre secciones
|
||||||
|
handleSectionTransition();
|
||||||
|
|
||||||
|
// Ejecuta un frame de la sección activa
|
||||||
|
switch (active_section_) {
|
||||||
|
case ActiveSection::LOGO:
|
||||||
|
logo_->iterate();
|
||||||
|
break;
|
||||||
|
case ActiveSection::INTRO:
|
||||||
|
intro_->iterate();
|
||||||
|
break;
|
||||||
|
case ActiveSection::TITLE:
|
||||||
|
title_->iterate();
|
||||||
|
break;
|
||||||
|
case ActiveSection::GAME:
|
||||||
|
game_->iterate();
|
||||||
|
break;
|
||||||
|
case ActiveSection::NONE:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Procesa un evento
|
||||||
|
auto Director::handleEvent(SDL_Event *event) -> SDL_AppResult {
|
||||||
|
#ifndef __EMSCRIPTEN__
|
||||||
|
// Evento de salida de la aplicación
|
||||||
|
if (event->type == SDL_EVENT_QUIT) {
|
||||||
|
section_->name = SECTION_PROG_QUIT;
|
||||||
|
return SDL_APP_SUCCESS;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Hot-plug de mandos
|
||||||
|
if (event->type == SDL_EVENT_GAMEPAD_ADDED) {
|
||||||
|
std::string name;
|
||||||
|
if (Input::get()->handleGamepadAdded(event->gdevice.which, name)) {
|
||||||
|
Notifications::show(name + " " + Lang::get()->getText(94),
|
||||||
|
Notifications::Palette::SUCCESS,
|
||||||
|
Notifications::LONG_MS);
|
||||||
|
}
|
||||||
|
} else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) {
|
||||||
|
std::string name;
|
||||||
|
if (Input::get()->handleGamepadRemoved(event->gdevice.which, name)) {
|
||||||
|
Notifications::show(name + " " + Lang::get()->getText(95),
|
||||||
|
Notifications::Palette::DANGER,
|
||||||
|
Notifications::LONG_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestiona la visibilidad del cursor según el movimiento del ratón
|
||||||
|
Mouse::handleEvent(*event, Options::video.fullscreen);
|
||||||
|
|
||||||
|
// Reenvía el evento a la sección activa
|
||||||
|
switch (active_section_) {
|
||||||
|
case ActiveSection::LOGO:
|
||||||
|
logo_->handleEvent(event);
|
||||||
|
break;
|
||||||
|
case ActiveSection::INTRO:
|
||||||
|
intro_->handleEvent(event);
|
||||||
|
break;
|
||||||
|
case ActiveSection::TITLE:
|
||||||
|
title_->handleEvent(event);
|
||||||
|
break;
|
||||||
|
case ActiveSection::GAME:
|
||||||
|
game_->handleEvent(event);
|
||||||
|
break;
|
||||||
|
case ActiveSection::NONE:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SDL_APP_CONTINUE;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint> // for uint8_t
|
||||||
|
#include <memory>
|
||||||
|
#include <string> // for string, basic_string
|
||||||
|
class Game;
|
||||||
|
class Intro;
|
||||||
|
class Logo;
|
||||||
|
class Title;
|
||||||
|
struct Section;
|
||||||
|
|
||||||
|
class Director {
|
||||||
|
public:
|
||||||
|
Director(int argc, const char *argv[]); // Constructor
|
||||||
|
~Director(); // Destructor
|
||||||
|
|
||||||
|
Director(const Director &) = delete;
|
||||||
|
auto operator=(const Director &) -> Director & = delete;
|
||||||
|
|
||||||
|
auto iterate() -> SDL_AppResult; // Ejecuta un frame del juego
|
||||||
|
auto handleEvent(SDL_Event *event) -> SDL_AppResult; // Procesa un evento
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Secciones activas del Director
|
||||||
|
enum class ActiveSection : std::uint8_t {
|
||||||
|
NONE,
|
||||||
|
LOGO,
|
||||||
|
INTRO,
|
||||||
|
TITLE,
|
||||||
|
GAME
|
||||||
|
};
|
||||||
|
|
||||||
|
static void initJailAudio(); // Inicializa jail_audio
|
||||||
|
auto initSDL() -> bool; // Arranca SDL y crea la ventana
|
||||||
|
static void initInput(); // Inicializa el objeto input
|
||||||
|
auto setFileList() -> bool; // Crea el indice de ficheros
|
||||||
|
static void checkProgramArguments(int argc, const char *argv[]); // Comprueba los parametros del programa
|
||||||
|
void createSystemFolder(const std::string &folder); // Crea la carpeta del sistema donde guardar datos
|
||||||
|
void handleSectionTransition(); // Gestiona las transiciones entre secciones
|
||||||
|
|
||||||
|
// Objetos y punteros
|
||||||
|
SDL_Window *window_; // La ventana donde dibujamos
|
||||||
|
SDL_Renderer *renderer_; // El renderizador de la ventana
|
||||||
|
Section *section_; // Sección y subsección actual del programa;
|
||||||
|
|
||||||
|
// Secciones del juego
|
||||||
|
ActiveSection active_section_;
|
||||||
|
std::unique_ptr<Logo> logo_;
|
||||||
|
std::unique_ptr<Intro> intro_;
|
||||||
|
std::unique_ptr<Title> title_;
|
||||||
|
std::unique_ptr<Game> game_;
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
std::string executable_path_; // Path del ejecutable
|
||||||
|
std::string system_folder_; // Carpeta del sistema donde guardar datos
|
||||||
|
};
|
||||||
@@ -1,696 +0,0 @@
|
|||||||
#include "director.h"
|
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
#include <errno.h> // for errno, EEXIST, EACCES, ENAMETOO...
|
|
||||||
#include <stdio.h> // for printf, perror
|
|
||||||
#include <string.h> // for strcmp
|
|
||||||
#include <sys/stat.h> // for mkdir, stat, S_IRWXU
|
|
||||||
#include <unistd.h> // for getuid
|
|
||||||
|
|
||||||
#include <cstdlib> // for exit, EXIT_FAILURE, srand
|
|
||||||
#include <fstream> // for basic_ostream, operator<<, basi...
|
|
||||||
#include <iostream> // for cout
|
|
||||||
#include <memory>
|
|
||||||
#include <string> // for basic_string, operator+, char_t...
|
|
||||||
#include <vector> // for vector
|
|
||||||
|
|
||||||
#include "asset.h" // for Asset, assetType
|
|
||||||
#include "const.h" // for SECTION_PROG_LOGO, GAMECANVAS_H...
|
|
||||||
#include "game.h" // for Game
|
|
||||||
#include "input.h" // for Input, inputs_e, INPUT_USE_GAME...
|
|
||||||
#include "intro.h" // for Intro
|
|
||||||
#include "jail_audio.hpp" // for JA_Init
|
|
||||||
#include "lang.h" // for Lang, MAX_LANGUAGES, ba_BA, en_UK
|
|
||||||
#include "logo.h" // for Logo
|
|
||||||
#include "screen.h" // for FILTER_NEAREST, Screen, FILTER_...
|
|
||||||
#include "texture.h" // for Texture
|
|
||||||
#include "title.h" // for Title
|
|
||||||
#include "utils.h" // for options_t, input_t, boolToString
|
|
||||||
|
|
||||||
#ifndef _WIN32
|
|
||||||
#include <pwd.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Constructor
|
|
||||||
Director::Director(int argc, const char *argv[]) {
|
|
||||||
std::cout << "Game start" << std::endl;
|
|
||||||
// Inicializa variables
|
|
||||||
section = new section_t();
|
|
||||||
section->name = SECTION_PROG_LOGO;
|
|
||||||
|
|
||||||
// Inicializa las opciones del programa
|
|
||||||
initOptions();
|
|
||||||
|
|
||||||
// Comprueba los parametros del programa
|
|
||||||
checkProgramArguments(argc, argv);
|
|
||||||
|
|
||||||
// Crea la carpeta del sistema donde guardar datos
|
|
||||||
createSystemFolder("jailgames");
|
|
||||||
#ifndef DEBUG
|
|
||||||
createSystemFolder("jailgames/coffee_crisis");
|
|
||||||
#else
|
|
||||||
createSystemFolder("jailgames/coffee_crisis_debug");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Crea el objeto que controla los ficheros de recursos
|
|
||||||
asset = new Asset(executablePath);
|
|
||||||
asset->setVerbose(options->console);
|
|
||||||
|
|
||||||
// Si falta algún fichero no inicia el programa
|
|
||||||
if (!setFileList()) {
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carga el fichero de configuración
|
|
||||||
loadConfigFile();
|
|
||||||
|
|
||||||
// Inicializa SDL
|
|
||||||
initSDL();
|
|
||||||
|
|
||||||
// Inicializa JailAudio
|
|
||||||
initJailAudio();
|
|
||||||
|
|
||||||
// Establece el modo de escalado de texturas
|
|
||||||
Texture::setGlobalScaleMode(options->filter == FILTER_NEAREST ? SDL_SCALEMODE_NEAREST : SDL_SCALEMODE_LINEAR);
|
|
||||||
|
|
||||||
// Crea los objetos
|
|
||||||
lang = new Lang(asset);
|
|
||||||
lang->setLang(options->language);
|
|
||||||
|
|
||||||
input = new Input(asset->get("gamecontrollerdb.txt"));
|
|
||||||
initInput();
|
|
||||||
|
|
||||||
screen = new Screen(window, renderer, asset, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
Director::~Director() {
|
|
||||||
saveConfigFile();
|
|
||||||
|
|
||||||
delete asset;
|
|
||||||
delete input;
|
|
||||||
delete screen;
|
|
||||||
delete lang;
|
|
||||||
delete options;
|
|
||||||
delete section;
|
|
||||||
|
|
||||||
SDL_DestroyRenderer(renderer);
|
|
||||||
SDL_DestroyWindow(window);
|
|
||||||
|
|
||||||
SDL_Quit();
|
|
||||||
|
|
||||||
std::cout << "\nBye!" << std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inicializa el objeto input
|
|
||||||
void Director::initInput() {
|
|
||||||
// Establece si ha de mostrar mensajes
|
|
||||||
input->setVerbose(options->console);
|
|
||||||
|
|
||||||
// Busca si hay un mando conectado
|
|
||||||
input->discoverGameController();
|
|
||||||
|
|
||||||
// Teclado - Movimiento del jugador
|
|
||||||
input->bindKey(input_up, SDL_SCANCODE_UP);
|
|
||||||
input->bindKey(input_down, SDL_SCANCODE_DOWN);
|
|
||||||
input->bindKey(input_left, SDL_SCANCODE_LEFT);
|
|
||||||
input->bindKey(input_right, SDL_SCANCODE_RIGHT);
|
|
||||||
input->bindKey(input_fire_left, SDL_SCANCODE_Q);
|
|
||||||
input->bindKey(input_fire_center, SDL_SCANCODE_W);
|
|
||||||
input->bindKey(input_fire_right, SDL_SCANCODE_E);
|
|
||||||
|
|
||||||
// Teclado - Otros
|
|
||||||
input->bindKey(input_accept, SDL_SCANCODE_RETURN);
|
|
||||||
input->bindKey(input_cancel, SDL_SCANCODE_ESCAPE);
|
|
||||||
input->bindKey(input_pause, SDL_SCANCODE_ESCAPE);
|
|
||||||
input->bindKey(input_exit, SDL_SCANCODE_ESCAPE);
|
|
||||||
input->bindKey(input_window_dec_size, SDL_SCANCODE_F1);
|
|
||||||
input->bindKey(input_window_inc_size, SDL_SCANCODE_F2);
|
|
||||||
input->bindKey(input_window_fullscreen, SDL_SCANCODE_F3);
|
|
||||||
|
|
||||||
// Mando - Movimiento del jugador
|
|
||||||
input->bindGameControllerButton(input_up, SDL_GAMEPAD_BUTTON_DPAD_UP);
|
|
||||||
input->bindGameControllerButton(input_down, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
|
|
||||||
input->bindGameControllerButton(input_left, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
|
||||||
input->bindGameControllerButton(input_right, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
|
|
||||||
input->bindGameControllerButton(input_fire_left, SDL_GAMEPAD_BUTTON_WEST);
|
|
||||||
input->bindGameControllerButton(input_fire_center, SDL_GAMEPAD_BUTTON_NORTH);
|
|
||||||
input->bindGameControllerButton(input_fire_right, SDL_GAMEPAD_BUTTON_EAST);
|
|
||||||
|
|
||||||
// Mando - Otros
|
|
||||||
input->bindGameControllerButton(input_accept, SDL_GAMEPAD_BUTTON_EAST);
|
|
||||||
input->bindGameControllerButton(input_cancel, SDL_GAMEPAD_BUTTON_SOUTH);
|
|
||||||
#ifdef GAME_CONSOLE
|
|
||||||
input->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_BACK);
|
|
||||||
input->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_START);
|
|
||||||
#else
|
|
||||||
input->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_START);
|
|
||||||
input->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_BACK);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inicializa JailAudio
|
|
||||||
void Director::initJailAudio() {
|
|
||||||
JA_Init(48000, SDL_AUDIO_S16, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arranca SDL y crea la ventana
|
|
||||||
bool Director::initSDL() {
|
|
||||||
// Indicador de éxito
|
|
||||||
bool success = true;
|
|
||||||
|
|
||||||
// Inicializa SDL
|
|
||||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD)) {
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << "SDL could not initialize!\nSDL Error: " << SDL_GetError() << std::endl;
|
|
||||||
}
|
|
||||||
success = false;
|
|
||||||
} else {
|
|
||||||
// Inicia el generador de numeros aleatorios
|
|
||||||
std::srand(static_cast<unsigned int>(SDL_GetTicks()));
|
|
||||||
|
|
||||||
// Crea la ventana
|
|
||||||
int incW = 0;
|
|
||||||
int incH = 0;
|
|
||||||
if (options->borderEnabled) {
|
|
||||||
incW = options->borderWidth * 2;
|
|
||||||
incH = options->borderHeight * 2;
|
|
||||||
}
|
|
||||||
window = SDL_CreateWindow(WINDOW_CAPTION, (options->gameWidth + incW) * options->windowSize, (options->gameHeight + incH) * options->windowSize, 0);
|
|
||||||
if (window == nullptr) {
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << "Window could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
|
|
||||||
}
|
|
||||||
success = false;
|
|
||||||
} else {
|
|
||||||
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
|
||||||
|
|
||||||
// Crea un renderizador para la ventana
|
|
||||||
renderer = SDL_CreateRenderer(window, NULL);
|
|
||||||
|
|
||||||
if (renderer == nullptr) {
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << "Renderer could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
|
|
||||||
}
|
|
||||||
success = false;
|
|
||||||
} else {
|
|
||||||
// Activa vsync si es necesario
|
|
||||||
if (options->vSync) {
|
|
||||||
SDL_SetRenderVSync(renderer, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inicializa el color de renderizado
|
|
||||||
SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF);
|
|
||||||
|
|
||||||
// Establece el tamaño del buffer de renderizado
|
|
||||||
SDL_SetRenderLogicalPresentation(renderer, options->gameWidth, options->gameHeight, SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
|
||||||
|
|
||||||
// Establece el modo de mezcla
|
|
||||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << std::endl;
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crea el indice de ficheros
|
|
||||||
bool Director::setFileList() {
|
|
||||||
#ifdef MACOS_BUNDLE
|
|
||||||
const std::string prefix = "/../Resources";
|
|
||||||
#else
|
|
||||||
const std::string prefix = "";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Ficheros de configuración
|
|
||||||
asset->add(systemFolder + "/config.txt", t_data, false, true);
|
|
||||||
asset->add(systemFolder + "/score.bin", t_data, false, true);
|
|
||||||
asset->add(prefix + "/data/config/demo.bin", t_data);
|
|
||||||
asset->add(prefix + "/data/config/gamecontrollerdb.txt", t_data);
|
|
||||||
|
|
||||||
// Musicas
|
|
||||||
asset->add(prefix + "/data/music/intro.ogg", t_music);
|
|
||||||
asset->add(prefix + "/data/music/playing.ogg", t_music);
|
|
||||||
asset->add(prefix + "/data/music/title.ogg", t_music);
|
|
||||||
|
|
||||||
// Sonidos
|
|
||||||
asset->add(prefix + "/data/sound/balloon.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/bubble1.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/bubble2.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/bubble3.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/bubble4.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/bullet.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/coffeeout.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/hiscore.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/itemdrop.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/itempickup.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/menu_move.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/menu_select.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/player_collision.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/stage_change.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/title.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/clock.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/powerball.wav", t_sound);
|
|
||||||
|
|
||||||
// Texturas
|
|
||||||
asset->add(prefix + "/data/gfx/balloon1.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/balloon1.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/balloon2.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/balloon2.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/balloon3.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/balloon3.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/balloon4.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/balloon4.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/bullet.png", t_bitmap);
|
|
||||||
|
|
||||||
asset->add(prefix + "/data/gfx/game_buildings.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/game_clouds.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/game_grass.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/game_power_meter.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/game_sky_colors.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/game_text.png", t_bitmap);
|
|
||||||
|
|
||||||
asset->add(prefix + "/data/gfx/intro.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/logo.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/menu_game_over.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/menu_game_over_end.png", t_bitmap);
|
|
||||||
|
|
||||||
asset->add(prefix + "/data/gfx/item_points1_disk.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/item_points1_disk.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/item_points2_gavina.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/item_points2_gavina.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/item_points3_pacmar.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/item_points3_pacmar.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/item_clock.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/item_clock.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/item_coffee.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/item_coffee.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/item_coffee_machine.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/item_coffee_machine.ani", t_data);
|
|
||||||
|
|
||||||
asset->add(prefix + "/data/gfx/title_bg_tile.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/title_coffee.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/title_crisis.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/title_dust.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/title_dust.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/title_gradient.png", t_bitmap);
|
|
||||||
|
|
||||||
asset->add(prefix + "/data/gfx/player_head.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/player_body.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/player_legs.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/player_death.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/player_fire.ani", t_data);
|
|
||||||
|
|
||||||
asset->add(prefix + "/data/gfx/player_bal1_head.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/player_bal1_body.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/player_bal1_legs.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/player_bal1_death.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/player_bal1_fire.png", t_bitmap);
|
|
||||||
|
|
||||||
asset->add(prefix + "/data/gfx/player_arounder_head.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/player_arounder_body.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/player_arounder_legs.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/player_arounder_death.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/player_arounder_fire.png", t_bitmap);
|
|
||||||
|
|
||||||
// Fuentes
|
|
||||||
asset->add(prefix + "/data/font/8bithud.png", t_font);
|
|
||||||
asset->add(prefix + "/data/font/8bithud.txt", t_font);
|
|
||||||
asset->add(prefix + "/data/font/nokia.png", t_font);
|
|
||||||
asset->add(prefix + "/data/font/nokia_big2.png", t_font);
|
|
||||||
asset->add(prefix + "/data/font/nokia.txt", t_font);
|
|
||||||
asset->add(prefix + "/data/font/nokia2.png", t_font);
|
|
||||||
asset->add(prefix + "/data/font/nokia2.txt", t_font);
|
|
||||||
asset->add(prefix + "/data/font/nokia_big2.txt", t_font);
|
|
||||||
asset->add(prefix + "/data/font/smb2_big.png", t_font);
|
|
||||||
asset->add(prefix + "/data/font/smb2_big.txt", t_font);
|
|
||||||
asset->add(prefix + "/data/font/smb2.png", t_font);
|
|
||||||
asset->add(prefix + "/data/font/smb2.txt", t_font);
|
|
||||||
|
|
||||||
// Textos
|
|
||||||
asset->add(prefix + "/data/lang/es_ES.txt", t_lang);
|
|
||||||
asset->add(prefix + "/data/lang/en_UK.txt", t_lang);
|
|
||||||
asset->add(prefix + "/data/lang/ba_BA.txt", t_lang);
|
|
||||||
|
|
||||||
// Menus
|
|
||||||
asset->add(prefix + "/data/menu/title.men", t_data);
|
|
||||||
asset->add(prefix + "/data/menu/title_gc.men", t_data);
|
|
||||||
asset->add(prefix + "/data/menu/options.men", t_data);
|
|
||||||
asset->add(prefix + "/data/menu/options_gc.men", t_data);
|
|
||||||
asset->add(prefix + "/data/menu/pause.men", t_data);
|
|
||||||
asset->add(prefix + "/data/menu/gameover.men", t_data);
|
|
||||||
asset->add(prefix + "/data/menu/player_select.men", t_data);
|
|
||||||
|
|
||||||
return asset->check();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inicializa las opciones del programa
|
|
||||||
void Director::initOptions() {
|
|
||||||
// Crea el puntero a la estructura de opciones
|
|
||||||
options = new options_t;
|
|
||||||
|
|
||||||
// Pone unos valores por defecto para las opciones de control
|
|
||||||
options->input.clear();
|
|
||||||
|
|
||||||
input_t inp;
|
|
||||||
inp.id = 0;
|
|
||||||
inp.name = "KEYBOARD";
|
|
||||||
inp.deviceType = INPUT_USE_KEYBOARD;
|
|
||||||
options->input.push_back(inp);
|
|
||||||
|
|
||||||
inp.id = 0;
|
|
||||||
inp.name = "GAME CONTROLLER";
|
|
||||||
inp.deviceType = INPUT_USE_GAMECONTROLLER;
|
|
||||||
options->input.push_back(inp);
|
|
||||||
|
|
||||||
// Opciones de video
|
|
||||||
options->gameWidth = GAMECANVAS_WIDTH;
|
|
||||||
options->gameHeight = GAMECANVAS_HEIGHT;
|
|
||||||
options->videoMode = 0;
|
|
||||||
options->windowSize = 3;
|
|
||||||
options->filter = FILTER_NEAREST;
|
|
||||||
options->vSync = true;
|
|
||||||
options->integerScale = true;
|
|
||||||
options->keepAspect = true;
|
|
||||||
options->borderWidth = 0;
|
|
||||||
options->borderHeight = 0;
|
|
||||||
options->borderEnabled = false;
|
|
||||||
|
|
||||||
// Opciones varios
|
|
||||||
options->playerSelected = 0;
|
|
||||||
options->difficulty = DIFFICULTY_NORMAL;
|
|
||||||
options->language = ba_BA;
|
|
||||||
options->console = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprueba los parametros del programa
|
|
||||||
void Director::checkProgramArguments(int argc, const char *argv[]) {
|
|
||||||
// Establece la ruta del programa
|
|
||||||
executablePath = argv[0];
|
|
||||||
|
|
||||||
// Comprueba el resto de parametros
|
|
||||||
for (int i = 1; i < argc; ++i) {
|
|
||||||
if (strcmp(argv[i], "--console") == 0) {
|
|
||||||
options->console = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crea la carpeta del sistema donde guardar datos
|
|
||||||
void Director::createSystemFolder(const std::string &folder) {
|
|
||||||
#ifdef _WIN32
|
|
||||||
systemFolder = std::string(getenv("APPDATA")) + "/" + folder;
|
|
||||||
#elif __APPLE__
|
|
||||||
struct passwd *pw = getpwuid(getuid());
|
|
||||||
const char *homedir = pw->pw_dir;
|
|
||||||
systemFolder = std::string(homedir) + "/Library/Application Support" + "/" + folder;
|
|
||||||
#elif __linux__
|
|
||||||
struct passwd *pw = getpwuid(getuid());
|
|
||||||
const char *homedir = pw->pw_dir;
|
|
||||||
systemFolder = std::string(homedir) + "/.config/" + folder;
|
|
||||||
|
|
||||||
{
|
|
||||||
// Intenta crear ".config", per si no existeix
|
|
||||||
std::string config_base_folder = std::string(homedir) + "/.config";
|
|
||||||
int ret = mkdir(config_base_folder.c_str(), S_IRWXU);
|
|
||||||
if (ret == -1 && errno != EEXIST) {
|
|
||||||
printf("ERROR CREATING CONFIG BASE FOLDER.");
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct stat st = {0};
|
|
||||||
if (stat(systemFolder.c_str(), &st) == -1) {
|
|
||||||
errno = 0;
|
|
||||||
#ifdef _WIN32
|
|
||||||
int ret = mkdir(systemFolder.c_str());
|
|
||||||
#else
|
|
||||||
int ret = mkdir(systemFolder.c_str(), S_IRWXU);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (ret == -1) {
|
|
||||||
switch (errno) {
|
|
||||||
case EACCES:
|
|
||||||
printf("the parent directory does not allow write");
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
|
|
||||||
case EEXIST:
|
|
||||||
printf("pathname already exists");
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
|
|
||||||
case ENAMETOOLONG:
|
|
||||||
printf("pathname is too long");
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
|
|
||||||
default:
|
|
||||||
perror("mkdir");
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carga el fichero de configuración
|
|
||||||
bool Director::loadConfigFile() {
|
|
||||||
// Indicador de éxito en la carga
|
|
||||||
bool success = true;
|
|
||||||
|
|
||||||
// Variables para manejar el fichero
|
|
||||||
const std::string filePath = "config.txt";
|
|
||||||
std::string line;
|
|
||||||
std::ifstream file(asset->get(filePath));
|
|
||||||
|
|
||||||
// Si el fichero se puede abrir
|
|
||||||
if (file.good()) {
|
|
||||||
// Procesa el fichero linea a linea
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << "Reading file " << filePath << std::endl;
|
|
||||||
}
|
|
||||||
while (std::getline(file, line)) {
|
|
||||||
// Comprueba que la linea no sea un comentario
|
|
||||||
if (line.substr(0, 1) != "#") {
|
|
||||||
// Encuentra la posición del caracter '='
|
|
||||||
int pos = line.find("=");
|
|
||||||
// Procesa las dos subcadenas
|
|
||||||
if (!setOptions(options, line.substr(0, pos), line.substr(pos + 1, line.length()))) {
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << "Warning: file " << filePath << std::endl;
|
|
||||||
std::cout << "Unknown parameter " << line.substr(0, pos).c_str() << std::endl;
|
|
||||||
}
|
|
||||||
success = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cierra el fichero
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << "Closing file " << filePath << std::endl;
|
|
||||||
}
|
|
||||||
file.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// El fichero no existe
|
|
||||||
else { // Crea el fichero con los valores por defecto
|
|
||||||
saveConfigFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normaliza los valores
|
|
||||||
if (options->videoMode != 0 && options->videoMode != SDL_WINDOW_FULLSCREEN) {
|
|
||||||
options->videoMode = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options->windowSize < 1 || options->windowSize > 4) {
|
|
||||||
options->windowSize = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options->language < 0 || options->language > MAX_LANGUAGES) {
|
|
||||||
options->language = en_UK;
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guarda el fichero de configuración
|
|
||||||
bool Director::saveConfigFile() {
|
|
||||||
bool success = true;
|
|
||||||
|
|
||||||
// Crea y abre el fichero de texto
|
|
||||||
std::ofstream file(asset->get("config.txt"));
|
|
||||||
|
|
||||||
if (file.good()) {
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << asset->get("config.txt") << " open for writing" << std::endl;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << asset->get("config.txt") << " can't be opened" << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opciones g´raficas
|
|
||||||
file << "## VISUAL OPTIONS\n";
|
|
||||||
if (options->videoMode == 0) {
|
|
||||||
file << "videoMode=0\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (options->videoMode == SDL_WINDOW_FULLSCREEN) {
|
|
||||||
file << "videoMode=SDL_WINDOW_FULLSCREEN\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
file << "windowSize=" + std::to_string(options->windowSize) + "\n";
|
|
||||||
|
|
||||||
if (options->filter == FILTER_NEAREST) {
|
|
||||||
file << "filter=FILTER_NEAREST\n";
|
|
||||||
} else {
|
|
||||||
file << "filter=FILTER_LINEAL\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
file << "vSync=" + boolToString(options->vSync) + "\n";
|
|
||||||
file << "integerScale=" + boolToString(options->integerScale) + "\n";
|
|
||||||
file << "keepAspect=" + boolToString(options->keepAspect) + "\n";
|
|
||||||
file << "borderEnabled=" + boolToString(options->borderEnabled) + "\n";
|
|
||||||
file << "borderWidth=" + std::to_string(options->borderWidth) + "\n";
|
|
||||||
file << "borderHeight=" + std::to_string(options->borderHeight) + "\n";
|
|
||||||
|
|
||||||
// Otras opciones del programa
|
|
||||||
file << "\n## OTHER OPTIONS\n";
|
|
||||||
file << "language=" + std::to_string(options->language) + "\n";
|
|
||||||
file << "difficulty=" + std::to_string(options->difficulty) + "\n";
|
|
||||||
file << "input0=" + std::to_string(options->input[0].deviceType) + "\n";
|
|
||||||
file << "input1=" + std::to_string(options->input[1].deviceType) + "\n";
|
|
||||||
|
|
||||||
// Cierra el fichero
|
|
||||||
file.close();
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Director::runLogo() {
|
|
||||||
auto logo = std::make_unique<Logo>(renderer, screen, asset, input, section);
|
|
||||||
logo->run();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Director::runIntro() {
|
|
||||||
auto intro = std::make_unique<Intro>(renderer, screen, asset, input, lang, section);
|
|
||||||
intro->run();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Director::runTitle() {
|
|
||||||
auto title = std::make_unique<Title>(renderer, screen, input, asset, options, lang, section);
|
|
||||||
title->run();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Director::runGame() {
|
|
||||||
const int numPlayers = section->subsection == SUBSECTION_GAME_PLAY_1P ? 1 : 2;
|
|
||||||
auto game = std::make_unique<Game>(numPlayers, 0, renderer, screen, asset, lang, input, false, options, section);
|
|
||||||
game->run();
|
|
||||||
}
|
|
||||||
|
|
||||||
int Director::run() {
|
|
||||||
// Bucle principal
|
|
||||||
while (section->name != SECTION_PROG_QUIT) {
|
|
||||||
switch (section->name) {
|
|
||||||
case SECTION_PROG_LOGO:
|
|
||||||
runLogo();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SECTION_PROG_INTRO:
|
|
||||||
runIntro();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SECTION_PROG_TITLE:
|
|
||||||
runTitle();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SECTION_PROG_GAME:
|
|
||||||
runGame();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Asigna variables a partir de dos cadenas
|
|
||||||
bool Director::setOptions(options_t *options, std::string var, std::string value) {
|
|
||||||
// Indicador de éxito en la asignación
|
|
||||||
bool success = true;
|
|
||||||
|
|
||||||
// Opciones de video
|
|
||||||
if (var == "videoMode") {
|
|
||||||
if (value == "SDL_WINDOW_FULLSCREEN" || value == "SDL_WINDOW_FULLSCREEN_DESKTOP") {
|
|
||||||
options->videoMode = SDL_WINDOW_FULLSCREEN;
|
|
||||||
} else {
|
|
||||||
options->videoMode = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "windowSize") {
|
|
||||||
options->windowSize = std::stoi(value);
|
|
||||||
if ((options->windowSize < 1) || (options->windowSize > 4)) {
|
|
||||||
options->windowSize = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "filter") {
|
|
||||||
if (value == "FILTER_LINEAL") {
|
|
||||||
options->filter = FILTER_LINEAL;
|
|
||||||
} else {
|
|
||||||
options->filter = FILTER_NEAREST;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "vSync") {
|
|
||||||
options->vSync = stringToBool(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "integerScale") {
|
|
||||||
options->integerScale = stringToBool(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "keepAspect") {
|
|
||||||
options->keepAspect = stringToBool(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "borderEnabled") {
|
|
||||||
options->borderEnabled = stringToBool(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "borderWidth") {
|
|
||||||
options->borderWidth = std::stoi(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "borderHeight") {
|
|
||||||
options->borderHeight = std::stoi(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opciones varias
|
|
||||||
else if (var == "language") {
|
|
||||||
options->language = std::stoi(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "difficulty") {
|
|
||||||
options->difficulty = std::stoi(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "input0") {
|
|
||||||
options->input[0].deviceType = std::stoi(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "input1") {
|
|
||||||
options->input[1].deviceType = std::stoi(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lineas vacias o que empiezan por comentario
|
|
||||||
else if (var == "" || var.substr(0, 1) == "#") {
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
success = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
|
|
||||||
#include <string> // for string, basic_string
|
|
||||||
class Asset;
|
|
||||||
class Game;
|
|
||||||
class Input;
|
|
||||||
class Intro;
|
|
||||||
class Lang;
|
|
||||||
class Logo;
|
|
||||||
class Screen;
|
|
||||||
class Title;
|
|
||||||
struct options_t;
|
|
||||||
struct section_t;
|
|
||||||
|
|
||||||
// Textos
|
|
||||||
constexpr const char *WINDOW_CAPTION = "© 2020 Coffee Crisis — JailDesigner";
|
|
||||||
|
|
||||||
class Director {
|
|
||||||
private:
|
|
||||||
// Objetos y punteros
|
|
||||||
SDL_Window *window; // La ventana donde dibujamos
|
|
||||||
SDL_Renderer *renderer; // El renderizador de la ventana
|
|
||||||
Screen *screen; // Objeto encargado de dibujar en pantalla
|
|
||||||
Input *input; // Objeto Input para gestionar las entradas
|
|
||||||
Lang *lang; // Objeto para gestionar los textos en diferentes idiomas
|
|
||||||
Asset *asset; // Objeto que gestiona todos los ficheros de recursos
|
|
||||||
section_t *section; // Sección y subsección actual del programa;
|
|
||||||
|
|
||||||
// Variables
|
|
||||||
struct options_t *options; // Variable con todas las opciones del programa
|
|
||||||
std::string executablePath; // Path del ejecutable
|
|
||||||
std::string systemFolder; // Carpeta del sistema donde guardar datos
|
|
||||||
|
|
||||||
// Inicializa jail_audio
|
|
||||||
void initJailAudio();
|
|
||||||
|
|
||||||
// Arranca SDL y crea la ventana
|
|
||||||
bool initSDL();
|
|
||||||
|
|
||||||
// Inicializa el objeto input
|
|
||||||
void initInput();
|
|
||||||
|
|
||||||
// Inicializa las opciones del programa
|
|
||||||
void initOptions();
|
|
||||||
|
|
||||||
// Asigna variables a partir de dos cadenas
|
|
||||||
bool setOptions(options_t *options, std::string var, std::string value);
|
|
||||||
|
|
||||||
// Crea el indice de ficheros
|
|
||||||
bool setFileList();
|
|
||||||
|
|
||||||
// Carga el fichero de configuración
|
|
||||||
bool loadConfigFile();
|
|
||||||
|
|
||||||
// Guarda el fichero de configuración
|
|
||||||
bool saveConfigFile();
|
|
||||||
|
|
||||||
// Comprueba los parametros del programa
|
|
||||||
void checkProgramArguments(int argc, const char *argv[]);
|
|
||||||
|
|
||||||
// Crea la carpeta del sistema donde guardar datos
|
|
||||||
void createSystemFolder(const std::string &folder);
|
|
||||||
|
|
||||||
// Ejecuta la seccion de juego con el logo
|
|
||||||
void runLogo();
|
|
||||||
|
|
||||||
// Ejecuta la seccion de juego de la introducción
|
|
||||||
void runIntro();
|
|
||||||
|
|
||||||
// Ejecuta la seccion de juego con el titulo y los menus
|
|
||||||
void runTitle();
|
|
||||||
|
|
||||||
// Ejecuta la seccion de juego donde se juega
|
|
||||||
void runGame();
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Constructor
|
|
||||||
Director(int argc, const char *argv[]);
|
|
||||||
|
|
||||||
// Destructor
|
|
||||||
~Director();
|
|
||||||
|
|
||||||
// Bucle principal
|
|
||||||
int run();
|
|
||||||
};
|
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
DisableFormat: true
|
||||||
|
SortIncludes: Never
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
# source/external/.clang-tidy
|
||||||
|
Checks: '-*'
|
||||||
|
WarningsAsErrors: ''
|
||||||
|
HeaderFilterRegex: ''
|
||||||
Vendored
+14726
File diff suppressed because it is too large
Load Diff
+68
-49
@@ -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/
|
// http://nothings.org/stb_vorbis/
|
||||||
//
|
//
|
||||||
// Original version written by Sean Barrett in 2007.
|
// Original version written by Sean Barrett in 2007.
|
||||||
@@ -29,12 +29,15 @@
|
|||||||
// Bernhard Wodo Evan Balster github:alxprd
|
// Bernhard Wodo Evan Balster github:alxprd
|
||||||
// Tom Beaumont Ingo Leitgeb Nicolas Guillemot
|
// Tom Beaumont Ingo Leitgeb Nicolas Guillemot
|
||||||
// Phillip Bennefall Rohit Thiago Goulart
|
// Phillip Bennefall Rohit Thiago Goulart
|
||||||
// github:manxorist saga musix github:infatum
|
// github:manxorist Saga Musix github:infatum
|
||||||
// Timur Gagiev Maxwell Koo Peter Waller
|
// Timur Gagiev Maxwell Koo Peter Waller
|
||||||
// github:audinowho Dougall Johnson David Reid
|
// github:audinowho Dougall Johnson David Reid
|
||||||
// github:Clownacy Pedro J. Estebanez Remi Verschelde
|
// github:Clownacy Pedro J. Estebanez Remi Verschelde
|
||||||
|
// AnthoFoxo github:morlat Gabriel Ravier
|
||||||
//
|
//
|
||||||
// Partial history:
|
// 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.20 - 2020-07-11 - several small fixes
|
||||||
// 1.19 - 2020-02-05 - warnings
|
// 1.19 - 2020-02-05 - warnings
|
||||||
// 1.18 - 2020-02-02 - fix seek bugs; parse header comments; misc warnings etc.
|
// 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
|
// 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 first channel, and (*output)[1][0] contains the first sample from
|
||||||
// the second channel.
|
// 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);
|
extern void stb_vorbis_flush_pushdata(stb_vorbis *f);
|
||||||
// inform stb_vorbis that your next datablock will not be contiguous with
|
// inform stb_vorbis that your next datablock will not be contiguous with
|
||||||
@@ -579,7 +588,7 @@ enum STBVorbisError
|
|||||||
#if defined(_MSC_VER) || defined(__MINGW32__)
|
#if defined(_MSC_VER) || defined(__MINGW32__)
|
||||||
#include <malloc.h>
|
#include <malloc.h>
|
||||||
#endif
|
#endif
|
||||||
#if defined(__linux__) || defined(__linux) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__)
|
#if defined(__linux__) || defined(__linux) || defined(__sun__) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__)
|
||||||
#include <alloca.h>
|
#include <alloca.h>
|
||||||
#endif
|
#endif
|
||||||
#else // STB_VORBIS_NO_CRT
|
#else // STB_VORBIS_NO_CRT
|
||||||
@@ -646,6 +655,12 @@ typedef signed int int32;
|
|||||||
|
|
||||||
typedef float codetype;
|
typedef float codetype;
|
||||||
|
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
#define STBV_NOTUSED(v) (void)(v)
|
||||||
|
#else
|
||||||
|
#define STBV_NOTUSED(v) (void)sizeof(v)
|
||||||
|
#endif
|
||||||
|
|
||||||
// @NOTE
|
// @NOTE
|
||||||
//
|
//
|
||||||
// Some arrays below are tagged "//varies", which means it's actually
|
// 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 sign = x & 0x80000000;
|
||||||
uint32 exp = (x & 0x7fe00000) >> 21;
|
uint32 exp = (x & 0x7fe00000) >> 21;
|
||||||
double res = sign ? -(double)mantissa : (double)mantissa;
|
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
|
// find the first entry
|
||||||
for (k=0; k < n; ++k) if (len[k] < NO_CODE) break;
|
for (k=0; k < n; ++k) if (len[k] < NO_CODE) break;
|
||||||
if (k == n) { assert(c->sorted_entries == 0); return TRUE; }
|
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 to the list
|
||||||
add_entry(c, 0, k, m++, len[k], values);
|
add_entry(c, 0, k, m++, len[k], values);
|
||||||
// add all available leaves
|
// add all available leaves
|
||||||
@@ -1090,6 +1106,7 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
|
|||||||
uint32 res;
|
uint32 res;
|
||||||
int z = len[i], y;
|
int z = len[i], y;
|
||||||
if (z == NO_CODE) continue;
|
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,
|
// find lowest available leaf (should always be earliest,
|
||||||
// which is what the specification calls for)
|
// which is what the specification calls for)
|
||||||
// note that this property, and the fact we can never have
|
// 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;
|
while (z > 0 && !available[z]) --z;
|
||||||
if (z == 0) { return FALSE; }
|
if (z == 0) { return FALSE; }
|
||||||
res = available[z];
|
res = available[z];
|
||||||
assert(z >= 0 && z < 32);
|
|
||||||
available[z] = 0;
|
available[z] = 0;
|
||||||
add_entry(c, bit_reverse(res), i, m++, len[i], values);
|
add_entry(c, bit_reverse(res), i, m++, len[i], values);
|
||||||
// propagate availability up the tree
|
// propagate availability up the tree
|
||||||
if (z != len[i]) {
|
if (z != len[i]) {
|
||||||
assert(len[i] >= 0 && len[i] < 32);
|
|
||||||
for (y=len[i]; y > z; --y) {
|
for (y=len[i]; y > z; --y) {
|
||||||
assert(available[y] == 0);
|
assert(available[y] == 0);
|
||||||
available[y] = res + (1 << (32-y));
|
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) {
|
while (z > base) {
|
||||||
float k00,k11;
|
float k00,k11;
|
||||||
|
float l00,l11;
|
||||||
|
|
||||||
k00 = z[-0] - z[-8];
|
k00 = z[-0] - z[ -8];
|
||||||
k11 = z[-1] - z[-9];
|
k11 = z[-1] - z[ -9];
|
||||||
z[-0] = z[-0] + z[-8];
|
l00 = z[-2] - z[-10];
|
||||||
z[-1] = z[-1] + z[-9];
|
l11 = z[-3] - z[-11];
|
||||||
z[-8] = k00;
|
z[ -0] = z[-0] + z[ -8];
|
||||||
z[-9] = k11 ;
|
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];
|
k00 = z[ -4] - z[-12];
|
||||||
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
|
|
||||||
k11 = z[ -5] - z[-13];
|
k11 = z[ -5] - z[-13];
|
||||||
|
l00 = z[ -6] - z[-14];
|
||||||
|
l11 = z[ -7] - z[-15];
|
||||||
z[ -4] = z[ -4] + z[-12];
|
z[ -4] = z[ -4] + z[-12];
|
||||||
z[ -5] = z[ -5] + z[-13];
|
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[ -6] = z[ -6] + z[-14];
|
||||||
z[ -7] = z[ -7] + z[-15];
|
z[ -7] = z[ -7] + z[-15];
|
||||||
z[-14] = (k00+k11) * A2;
|
z[-12] = k11;
|
||||||
z[-15] = (k00-k11) * A2;
|
z[-13] = -k00;
|
||||||
|
z[-14] = (l11-l00) * A2;
|
||||||
|
z[-15] = (l00+l11) * -A2;
|
||||||
|
|
||||||
iter_54(z);
|
iter_54(z);
|
||||||
iter_54(z-8);
|
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) {
|
for (q=1; q < g->values; ++q) {
|
||||||
j = g->sorted_order[q];
|
j = g->sorted_order[q];
|
||||||
#ifndef STB_VORBIS_NO_DEFER_FLOOR
|
#ifndef STB_VORBIS_NO_DEFER_FLOOR
|
||||||
|
STBV_NOTUSED(step2_flag);
|
||||||
if (finalY[j] >= 0)
|
if (finalY[j] >= 0)
|
||||||
#else
|
#else
|
||||||
if (step2_flag[j])
|
if (step2_flag[j])
|
||||||
@@ -3171,6 +3186,7 @@ static int vorbis_decode_packet_rest(vorb *f, int *len, Mode *m, int left_start,
|
|||||||
|
|
||||||
// WINDOWING
|
// WINDOWING
|
||||||
|
|
||||||
|
STBV_NOTUSED(left_end);
|
||||||
n = f->blocksize[m->blockflag];
|
n = f->blocksize[m->blockflag];
|
||||||
map = &f->mapping[m->mapping];
|
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
|
// this isn't to spec, but spec would require us to read ahead
|
||||||
// and decode the size of all current frames--could be done,
|
// and decode the size of all current frames--could be done,
|
||||||
// but presumably it's not a commonly used feature
|
// 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,
|
// we might have to discard samples "from" the next frame too,
|
||||||
// if we're lapping a large block then a small at the start?
|
// if we're lapping a large block then a small at the start?
|
||||||
f->discard_samples_deferred = n - right_end;
|
f->discard_samples_deferred = n - right_end;
|
||||||
@@ -3642,9 +3658,11 @@ static int start_decoder(vorb *f)
|
|||||||
f->vendor[len] = (char)'\0';
|
f->vendor[len] = (char)'\0';
|
||||||
//user comments
|
//user comments
|
||||||
f->comment_list_length = get32_packet(f);
|
f->comment_list_length = get32_packet(f);
|
||||||
if (f->comment_list_length > 0) {
|
f->comment_list = NULL;
|
||||||
f->comment_list = (char**)setup_malloc(f, sizeof(char*) * (f->comment_list_length));
|
if (f->comment_list_length > 0)
|
||||||
if (f->comment_list == NULL) return error(f, VORBIS_outofmem);
|
{
|
||||||
|
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) {
|
for(i=0; i < f->comment_list_length; ++i) {
|
||||||
@@ -3867,8 +3885,7 @@ static int start_decoder(vorb *f)
|
|||||||
unsigned int div=1;
|
unsigned int div=1;
|
||||||
for (k=0; k < c->dimensions; ++k) {
|
for (k=0; k < c->dimensions; ++k) {
|
||||||
int off = (z / div) % c->lookup_values;
|
int off = (z / div) % c->lookup_values;
|
||||||
float val = mults[off];
|
float val = mults[off]*c->delta_value + c->minimum_value + last;
|
||||||
val = mults[off]*c->delta_value + c->minimum_value + last;
|
|
||||||
c->multiplicands[j*c->dimensions + k] = val;
|
c->multiplicands[j*c->dimensions + k] = val;
|
||||||
if (c->sequence_p)
|
if (c->sequence_p)
|
||||||
last = val;
|
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);
|
if (g->class_masterbooks[j] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
|
||||||
}
|
}
|
||||||
for (k=0; k < 1 << g->class_subclasses[j]; ++k) {
|
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);
|
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;
|
*error = VORBIS_need_more_data;
|
||||||
else
|
else
|
||||||
*error = p.error;
|
*error = p.error;
|
||||||
|
vorbis_deinit(&p);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
f = vorbis_alloc(&p);
|
f = vorbis_alloc(&p);
|
||||||
@@ -4566,7 +4584,7 @@ static uint32 vorbis_find_page(stb_vorbis *f, uint32 *end, uint32 *last)
|
|||||||
header[i] = get8(f);
|
header[i] = get8(f);
|
||||||
if (f->eof) return 0;
|
if (f->eof) return 0;
|
||||||
if (header[4] != 0) goto invalid;
|
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)
|
for (i=22; i < 26; ++i)
|
||||||
header[i] = 0;
|
header[i] = 0;
|
||||||
crc = 0;
|
crc = 0;
|
||||||
@@ -4970,7 +4988,7 @@ unsigned int stb_vorbis_stream_length_in_samples(stb_vorbis *f)
|
|||||||
// set. whoops!
|
// set. whoops!
|
||||||
break;
|
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);
|
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 * stb_vorbis_open_memory(const unsigned char *data, int len, int *error, const stb_vorbis_alloc *alloc)
|
||||||
{
|
{
|
||||||
stb_vorbis *f, p;
|
stb_vorbis *f, p;
|
||||||
if (data == NULL) return NULL;
|
if (!data) {
|
||||||
|
if (error) *error = VORBIS_unexpected_eof;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
vorbis_init(&p, alloc);
|
vorbis_init(&p, alloc);
|
||||||
p.stream = (uint8 *) data;
|
p.stream = (uint8 *) data;
|
||||||
p.stream_end = (uint8 *) data + len;
|
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)
|
static void compute_samples(int mask, short *output, int num_c, float **data, int d_offset, int len)
|
||||||
{
|
{
|
||||||
#define BUFFER_SIZE 32
|
#define STB_BUFFER_SIZE 32
|
||||||
float buffer[BUFFER_SIZE];
|
float buffer[STB_BUFFER_SIZE];
|
||||||
int i,j,o,n = BUFFER_SIZE;
|
int i,j,o,n = STB_BUFFER_SIZE;
|
||||||
check_endianness();
|
check_endianness();
|
||||||
for (o = 0; o < len; o += BUFFER_SIZE) {
|
for (o = 0; o < len; o += STB_BUFFER_SIZE) {
|
||||||
memset(buffer, 0, sizeof(buffer));
|
memset(buffer, 0, sizeof(buffer));
|
||||||
if (o + n > len) n = len - o;
|
if (o + n > len) n = len - o;
|
||||||
for (j=0; j < num_c; ++j) {
|
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;
|
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)
|
static void compute_stereo_samples(short *output, int num_c, float **data, int d_offset, int len)
|
||||||
{
|
{
|
||||||
#define BUFFER_SIZE 32
|
#define STB_BUFFER_SIZE 32
|
||||||
float buffer[BUFFER_SIZE];
|
float buffer[STB_BUFFER_SIZE];
|
||||||
int i,j,o,n = BUFFER_SIZE >> 1;
|
int i,j,o,n = STB_BUFFER_SIZE >> 1;
|
||||||
// o is the offset in the source data
|
// o is the offset in the source data
|
||||||
check_endianness();
|
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
|
// o2 is the offset in the output data
|
||||||
int o2 = o << 1;
|
int o2 = o << 1;
|
||||||
memset(buffer, 0, sizeof(buffer));
|
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;
|
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)
|
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;
|
float **outputs;
|
||||||
int len = num_shorts / channels;
|
int len = num_shorts / channels;
|
||||||
int n=0;
|
int n=0;
|
||||||
int z = f->channels;
|
|
||||||
if (z > channels) z = channels;
|
|
||||||
while (n < len) {
|
while (n < len) {
|
||||||
int k = f->channel_buffer_end - f->channel_buffer_start;
|
int k = f->channel_buffer_end - f->channel_buffer_start;
|
||||||
if (n+k >= len) k = len - n;
|
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;
|
float **outputs;
|
||||||
int n=0;
|
int n=0;
|
||||||
int z = f->channels;
|
|
||||||
if (z > channels) z = channels;
|
|
||||||
while (n < len) {
|
while (n < len) {
|
||||||
int k = f->channel_buffer_end - f->channel_buffer_start;
|
int k = f->channel_buffer_end - f->channel_buffer_start;
|
||||||
if (n+k >= len) k = len - n;
|
if (n+k >= len) k = len - n;
|
||||||
-158
@@ -1,158 +0,0 @@
|
|||||||
#include "fade.h"
|
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
#include <stdlib.h> // for rand
|
|
||||||
|
|
||||||
#include <iostream> // for char_traits, basic_ostream, operator<<
|
|
||||||
|
|
||||||
#include "const.h" // for GAMECANVAS_HEIGHT, GAMECANVAS_WIDTH
|
|
||||||
|
|
||||||
// Constructor
|
|
||||||
Fade::Fade(SDL_Renderer *renderer) {
|
|
||||||
mRenderer = renderer;
|
|
||||||
|
|
||||||
mBackbuffer = SDL_CreateTexture(mRenderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT);
|
|
||||||
if (mBackbuffer != nullptr) {
|
|
||||||
SDL_SetTextureScaleMode(mBackbuffer, SDL_SCALEMODE_NEAREST);
|
|
||||||
}
|
|
||||||
if (mBackbuffer == nullptr) {
|
|
||||||
std::cout << "Error: textTexture could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destructor
|
|
||||||
Fade::~Fade() {
|
|
||||||
SDL_DestroyTexture(mBackbuffer);
|
|
||||||
mBackbuffer = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inicializa las variables
|
|
||||||
void Fade::init(Uint8 r, Uint8 g, Uint8 b) {
|
|
||||||
mFadeType = FADE_CENTER;
|
|
||||||
mEnabled = false;
|
|
||||||
mFinished = false;
|
|
||||||
mCounter = 0;
|
|
||||||
mR = r;
|
|
||||||
mG = g;
|
|
||||||
mB = b;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pinta una transición en pantalla
|
|
||||||
void Fade::render() {
|
|
||||||
if (mEnabled && !mFinished) {
|
|
||||||
switch (mFadeType) {
|
|
||||||
case FADE_FULLSCREEN: {
|
|
||||||
SDL_FRect fRect1 = {0, 0, (float)GAMECANVAS_WIDTH, (float)GAMECANVAS_HEIGHT};
|
|
||||||
|
|
||||||
for (int i = 0; i < 256; i += 4) {
|
|
||||||
// Dibujamos sobre el renderizador
|
|
||||||
SDL_SetRenderTarget(mRenderer, nullptr);
|
|
||||||
|
|
||||||
// Copia el backbuffer con la imagen que había al renderizador
|
|
||||||
SDL_RenderTexture(mRenderer, mBackbuffer, nullptr, nullptr);
|
|
||||||
|
|
||||||
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, i);
|
|
||||||
SDL_RenderFillRect(mRenderer, &fRect1);
|
|
||||||
|
|
||||||
// Vuelca el renderizador en pantalla
|
|
||||||
SDL_RenderPresent(mRenderer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deja todos los buffers del mismo color
|
|
||||||
SDL_SetRenderTarget(mRenderer, mBackbuffer);
|
|
||||||
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 255);
|
|
||||||
SDL_RenderClear(mRenderer);
|
|
||||||
|
|
||||||
SDL_SetRenderTarget(mRenderer, nullptr);
|
|
||||||
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 255);
|
|
||||||
SDL_RenderClear(mRenderer);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case FADE_CENTER: {
|
|
||||||
SDL_FRect fR1 = {0, 0, (float)GAMECANVAS_WIDTH, 0};
|
|
||||||
SDL_FRect fR2 = {0, 0, (float)GAMECANVAS_WIDTH, 0};
|
|
||||||
|
|
||||||
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 64);
|
|
||||||
|
|
||||||
for (int i = 0; i < mCounter; i++) {
|
|
||||||
fR1.h = fR2.h = (float)(i * 4);
|
|
||||||
fR2.y = (float)(GAMECANVAS_HEIGHT - (i * 4));
|
|
||||||
|
|
||||||
SDL_RenderFillRect(mRenderer, &fR1);
|
|
||||||
SDL_RenderFillRect(mRenderer, &fR2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((mCounter * 4) > GAMECANVAS_HEIGHT)
|
|
||||||
mFinished = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case FADE_RANDOM_SQUARE: {
|
|
||||||
SDL_FRect fRs = {0, 0, 32, 32};
|
|
||||||
|
|
||||||
for (Uint16 i = 0; i < 50; i++) {
|
|
||||||
// Crea un color al azar
|
|
||||||
mR = 255 * (rand() % 2);
|
|
||||||
mG = 255 * (rand() % 2);
|
|
||||||
mB = 255 * (rand() % 2);
|
|
||||||
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 64);
|
|
||||||
|
|
||||||
// Dibujamos sobre el backbuffer
|
|
||||||
SDL_SetRenderTarget(mRenderer, mBackbuffer);
|
|
||||||
|
|
||||||
fRs.x = (float)(rand() % (GAMECANVAS_WIDTH - 32));
|
|
||||||
fRs.y = (float)(rand() % (GAMECANVAS_HEIGHT - 32));
|
|
||||||
SDL_RenderFillRect(mRenderer, &fRs);
|
|
||||||
|
|
||||||
// Volvemos a usar el renderizador de forma normal
|
|
||||||
SDL_SetRenderTarget(mRenderer, nullptr);
|
|
||||||
|
|
||||||
// Copiamos el backbuffer al renderizador
|
|
||||||
SDL_RenderTexture(mRenderer, mBackbuffer, nullptr, nullptr);
|
|
||||||
|
|
||||||
// Volcamos el renderizador en pantalla
|
|
||||||
SDL_RenderPresent(mRenderer);
|
|
||||||
SDL_Delay(100);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mFinished) {
|
|
||||||
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 255);
|
|
||||||
SDL_RenderClear(mRenderer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actualiza las variables internas
|
|
||||||
void Fade::update() {
|
|
||||||
if (mEnabled)
|
|
||||||
mCounter++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activa el fade
|
|
||||||
void Fade::activateFade() {
|
|
||||||
mEnabled = true;
|
|
||||||
mFinished = false;
|
|
||||||
mCounter = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprueba si está activo
|
|
||||||
bool Fade::isEnabled() {
|
|
||||||
return mEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprueba si ha terminado la transicion
|
|
||||||
bool Fade::hasEnded() {
|
|
||||||
return mFinished;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el tipo de fade
|
|
||||||
void Fade::setFadeType(Uint8 fadeType) {
|
|
||||||
mFadeType = fadeType;
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
|
|
||||||
// Tipos de fundido
|
|
||||||
constexpr int FADE_FULLSCREEN = 0;
|
|
||||||
constexpr int FADE_CENTER = 1;
|
|
||||||
constexpr int FADE_RANDOM_SQUARE = 2;
|
|
||||||
|
|
||||||
// Clase Fade
|
|
||||||
class Fade {
|
|
||||||
private:
|
|
||||||
SDL_Renderer *mRenderer; // El renderizador de la ventana
|
|
||||||
SDL_Texture *mBackbuffer; // Textura para usar como backbuffer
|
|
||||||
Uint8 mFadeType; // Tipo de fade a realizar
|
|
||||||
Uint16 mCounter; // Contador interno
|
|
||||||
bool mEnabled; // Indica si el fade está activo
|
|
||||||
bool mFinished; // Indica si ha terminado la transición
|
|
||||||
Uint8 mR, mG, mB; // Colores para el fade
|
|
||||||
SDL_Rect mRect1; // Rectangulo usado para crear los efectos de transición
|
|
||||||
SDL_Rect mRect2; // Rectangulo usado para crear los efectos de transición
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Constructor
|
|
||||||
Fade(SDL_Renderer *renderer);
|
|
||||||
|
|
||||||
// Destructor
|
|
||||||
~Fade();
|
|
||||||
|
|
||||||
// Inicializa las variables
|
|
||||||
void init(Uint8 r, Uint8 g, Uint8 b);
|
|
||||||
|
|
||||||
// Pinta una transición en pantalla
|
|
||||||
void render();
|
|
||||||
|
|
||||||
// Actualiza las variables internas
|
|
||||||
void update();
|
|
||||||
|
|
||||||
// Activa el fade
|
|
||||||
void activateFade();
|
|
||||||
|
|
||||||
// Comprueba si ha terminado la transicion
|
|
||||||
bool hasEnded();
|
|
||||||
|
|
||||||
// Comprueba si está activo
|
|
||||||
bool isEnabled();
|
|
||||||
|
|
||||||
// Establece el tipo de fade
|
|
||||||
void setFadeType(Uint8 fadeType);
|
|
||||||
};
|
|
||||||
-3436
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user