Compare commits
105 Commits
a035fecb04
...
v2.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
+2
-2
@@ -2,8 +2,8 @@ BasedOnStyle: Google
|
||||
IndentWidth: 4
|
||||
NamespaceIndentation: All
|
||||
IndentAccessModifiers: false
|
||||
ColumnLimit: 0 # Sin limite de longitud de linea
|
||||
BreakBeforeBraces: Attach # Llaves en la misma linea
|
||||
ColumnLimit: 0 # Sin límite de longitud de línea
|
||||
BreakBeforeBraces: Attach # Llaves en la misma línea
|
||||
AllowShortIfStatementsOnASingleLine: true
|
||||
AllowShortBlocksOnASingleLine: true
|
||||
AllowShortFunctionsOnASingleLine: All
|
||||
|
||||
+76
-53
@@ -2,83 +2,106 @@ Checks:
|
||||
- readability-*
|
||||
- modernize-*
|
||||
- performance-*
|
||||
- bugprone-unchecked-optional-access
|
||||
- bugprone-sizeof-expression
|
||||
- bugprone-suspicious-missing-comma
|
||||
- bugprone-suspicious-index
|
||||
- bugprone-undefined-memory-manipulation
|
||||
- bugprone-use-after-move
|
||||
- bugprone-out-of-bound-access
|
||||
- bugprone-*
|
||||
- -readability-identifier-length
|
||||
- -readability-magic-numbers
|
||||
- -bugprone-narrowing-conversions
|
||||
- -performance-enum-size
|
||||
- -performance-inefficient-string-concatenation
|
||||
- -bugprone-integer-division
|
||||
- -bugprone-easily-swappable-parameters
|
||||
- -modernize-avoid-c-arrays,-warnings-as-errors
|
||||
- -bugprone-narrowing-conversions
|
||||
- -modernize-avoid-c-arrays
|
||||
|
||||
WarningsAsErrors: '*'
|
||||
# Excluye jail_audio.hpp, stb_image.h y stb_vorbis.c del analisis
|
||||
HeaderFilterRegex: 'source/(?!jail_audio\.hpp|stb_image\.h|stb_vorbis\.c).*'
|
||||
# Headers nostres (excloem source/external/ que conté dependències de tercers no editables)
|
||||
HeaderFilterRegex: 'source/(core|game|utils)/'
|
||||
FormatStyle: file
|
||||
|
||||
CheckOptions:
|
||||
# Variables locales en snake_case
|
||||
# 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.LocalVariableCase, value: lower_case }
|
||||
|
||||
# Miembros privados en snake_case con sufijo _
|
||||
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
|
||||
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
|
||||
# Parámetros de función
|
||||
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
|
||||
|
||||
# Miembros protegidos en snake_case con sufijo _
|
||||
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
|
||||
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
|
||||
|
||||
# Miembros 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
|
||||
# Variables estáticas no-const (static locales, static file-scope,
|
||||
# y static members no-const de clase como el instance_ de un Singleton).
|
||||
# Sufijo _ para marcar que tienen storage estático.
|
||||
- { key: readability-identifier-naming.StaticVariableCase, value: lower_case }
|
||||
- { key: readability-identifier-naming.StaticVariableSuffix, value: _ }
|
||||
|
||||
# Constantes 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
|
||||
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
|
||||
# Protegidos: snake_case con sufijo _
|
||||
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
|
||||
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
|
||||
|
||||
# Variables constexpr globales en UPPER_CASE
|
||||
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE }
|
||||
# Públicos: snake_case sin sufijo
|
||||
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
|
||||
|
||||
# Constantes locales en UPPER_CASE
|
||||
- { key: readability-identifier-naming.LocalConstantCase, value: UPPER_CASE }
|
||||
|
||||
# Constexpr miembros en UPPER_CASE (sin sufijo)
|
||||
- { key: readability-identifier-naming.ConstexprMemberCase, value: UPPER_CASE }
|
||||
|
||||
# Constexpr miembros privados/protegidos con sufijo _
|
||||
- { key: readability-identifier-naming.ConstexprMethodCase, value: UPPER_CASE }
|
||||
|
||||
# Clases, structs y enums en CamelCase
|
||||
# =====================================================================
|
||||
# TIPOS
|
||||
# =====================================================================
|
||||
- { key: readability-identifier-naming.ClassCase, value: CamelCase }
|
||||
- { key: readability-identifier-naming.StructCase, value: CamelCase }
|
||||
- { key: readability-identifier-naming.EnumCase, value: CamelCase }
|
||||
- { key: readability-identifier-naming.UnionCase, value: CamelCase }
|
||||
- { key: readability-identifier-naming.TypeAliasCase, value: CamelCase }
|
||||
- { key: readability-identifier-naming.TypedefCase, value: CamelCase }
|
||||
- { key: readability-identifier-naming.TemplateParameterCase, value: CamelCase }
|
||||
|
||||
# Valores de enums en UPPER_CASE
|
||||
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
|
||||
# Namespaces
|
||||
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
|
||||
|
||||
# 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.PrivateMethodCase, value: camelBack }
|
||||
- { key: readability-identifier-naming.ProtectedMethodCase, value: camelBack }
|
||||
- { key: readability-identifier-naming.PublicMethodCase, value: camelBack }
|
||||
|
||||
# Funciones en camelBack
|
||||
- { key: readability-identifier-naming.FunctionCase, value: camelBack }
|
||||
|
||||
# Parametros en lower_case
|
||||
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
|
||||
- { key: readability-identifier-naming.ConstexprMethodCase, value: camelBack }
|
||||
|
||||
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
|
||||
build/
|
||||
compile_commands.json
|
||||
dist/
|
||||
data/config/config.txt
|
||||
*.DS_Store
|
||||
thumbs.db
|
||||
*.exe
|
||||
@@ -14,3 +14,6 @@ thumbs.db
|
||||
coffee_crisis
|
||||
coffee_crisis_debug
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
# Linux
|
||||
make linux # Release build → ./coffee_crisis
|
||||
make linux_debug # Debug build (defines DEBUG and PAUSE) → ./coffee_crisis_debug
|
||||
|
||||
# macOS
|
||||
make macos # Release build with clang++
|
||||
make macos_debug # Debug build
|
||||
|
||||
# Windows (MinGW)
|
||||
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
|
||||
make # Release build
|
||||
make debug # Debug build (defines DEBUG)
|
||||
make release # Empaqueta .tar.gz / .dmg / .zip segons SO
|
||||
make pack # Regenera resources.pack
|
||||
make compile_shaders # Compila shaders GLSL → headers SPIR-V (requereix glslc)
|
||||
make controllerdb # Descarga gamecontrollerdb.txt
|
||||
make format # clang-format -i
|
||||
make tidy # clang-tidy
|
||||
make cppcheck # cppcheck
|
||||
```
|
||||
|
||||
There is also a CMakeLists.txt available as an alternative build system.
|
||||
|
||||
There are no tests or linter configured.
|
||||
|
||||
## 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.
|
||||
- **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.
|
||||
- **Input** (`input.h/cpp`): Abstracts keyboard and gamepad input.
|
||||
- **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.
|
||||
- **Lang** (`lang.h/cpp`): i18n system loading text strings from files in `data/lang/`.
|
||||
```
|
||||
source/
|
||||
├── main.cpp
|
||||
├── core/
|
||||
│ ├── audio/ jail_audio.hpp
|
||||
│ ├── 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
|
||||
- **AnimatedSprite** → extends Sprite with frame-based animation (loaded from `.ani` files)
|
||||
- **MovingSprite** → sprite with movement
|
||||
- **SmartSprite** → sprite with autonomous behavior (score popups, thrown items)
|
||||
- **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.
|
||||
- **Screen** (`core/rendering/screen.h`): abstracció de render. Canvas virtual 256×192 escalat a la finestra. Fullscreen/windowed, borders, fades.
|
||||
- **Input** (`core/input/input.h`): abstracció de teclat i gamepad.
|
||||
- **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
|
||||
- **Balloon** (`balloon.h/cpp`): Enemy entities with multiple types and split-on-pop behavior
|
||||
- **Bullet** (`bullet.h/cpp`): Projectiles fired by the player (left/center/right)
|
||||
- **Item** (`item.h/cpp`): Collectible items (points, clock, coffee, power-ups)
|
||||
- **Sprite** → base per dibuixar des d'un spritesheet PNG
|
||||
- **AnimatedSprite** → afegeix animació per frames (arxius `.ani`)
|
||||
- **MovingSprite** → sprite amb posició/velocitat
|
||||
- **SmartSprite** → sprite autònom (score popups, objectes llençats)
|
||||
|
||||
### 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
|
||||
|
||||
@@ -72,8 +95,9 @@ Defined in `const.h`: block size (8px), virtual canvas (256×192), play area bou
|
||||
- `data/font/` — bitmap font files
|
||||
- `data/music/` and `data/sound/` — audio assets
|
||||
- `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/shaders/` — fonts GLSL per al post-procesado SDL3 GPU (no van al pack; s'empotren al binari via headers SPIR-V)
|
||||
|
||||
## Language
|
||||
|
||||
|
||||
+261
-34
@@ -1,12 +1,23 @@
|
||||
# CMakeLists.txt
|
||||
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(coffee_crisis VERSION 1.00)
|
||||
|
||||
# Configuración de compilador para MinGW en Windows
|
||||
if(WIN32 AND NOT CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
|
||||
set(CMAKE_CXX_COMPILER "g++")
|
||||
set(CMAKE_C_COMPILER "gcc")
|
||||
# La versió de l'app es defineix una sola vegada a source/utils/defines.hpp
|
||||
# (Defines::VERSION). El Makefile ja la grepeja per als noms de release; aqui
|
||||
# l'extreiem perque project(... VERSION ...) i tots els consumidors interns
|
||||
# de CMake (CPack, install, etc.) usin la mateixa font de veritat.
|
||||
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/source/utils/defines.hpp" _DEFINES_CONTENT)
|
||||
string(REGEX MATCH "VERSION = \"([0-9]+\\.[0-9]+\\.[0-9]+)\"" _ "${_DEFINES_CONTENT}")
|
||||
set(APP_VERSION "${CMAKE_MATCH_1}")
|
||||
if(APP_VERSION STREQUAL "")
|
||||
message(FATAL_ERROR "No s'ha pogut extreure VERSION de source/utils/defines.hpp")
|
||||
endif()
|
||||
|
||||
project(coffee_crisis VERSION ${APP_VERSION})
|
||||
|
||||
# Tipus de build per defecte (Debug) si no se n'ha especificat cap
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
|
||||
endif()
|
||||
|
||||
# Establecer estándar de C++
|
||||
@@ -14,20 +25,96 @@ set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# Configuración global de flags de compilación
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall")
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Os -ffunction-sections -fdata-sections")
|
||||
# --- GENERACIÓN DE VERSIÓN AUTOMÁTICA ---
|
||||
# Si GIT_HASH se ha pasado desde fuera (p.ej. desde el Makefile via -DGIT_HASH=xxx),
|
||||
# lo usamos tal cual. Esto evita problemas con Docker/emscripten, donde git aborta por
|
||||
# "dubious ownership" en el volumen montado. En builds locales sin -DGIT_HASH, se
|
||||
# resuelve aquí ejecutando git directamente.
|
||||
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
|
||||
find_package(Git QUIET)
|
||||
if(GIT_FOUND)
|
||||
execute_process(
|
||||
COMMAND ${GIT_EXECUTABLE} rev-parse --short=7 HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_HASH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET
|
||||
)
|
||||
endif()
|
||||
if(NOT DEFINED GIT_HASH OR GIT_HASH STREQUAL "")
|
||||
set(GIT_HASH "unknown")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Configurar archivo de versión
|
||||
configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/version.h @ONLY)
|
||||
|
||||
# Define el directorio de los archivos fuente
|
||||
set(DIR_SOURCES "${CMAKE_SOURCE_DIR}/source")
|
||||
|
||||
# Cargar todos los archivos fuente en DIR_SOURCES
|
||||
file(GLOB SOURCES "${DIR_SOURCES}/*.cpp")
|
||||
# --- LISTA EXPLÍCITA DE FUENTES ---
|
||||
set(APP_SOURCES
|
||||
source/main.cpp
|
||||
|
||||
# Verificar si se encontraron archivos fuente
|
||||
if(NOT SOURCES)
|
||||
message(FATAL_ERROR "No se encontraron archivos fuente en ${DIR_SOURCES}.")
|
||||
endif()
|
||||
# --- core/audio ---
|
||||
source/core/audio/audio.cpp
|
||||
source/core/audio/audio_adapter.cpp
|
||||
|
||||
# --- core/input ---
|
||||
source/core/input/global_inputs.cpp
|
||||
source/core/input/input.cpp
|
||||
source/core/input/mouse.cpp
|
||||
|
||||
# --- core/locale ---
|
||||
source/core/locale/lang.cpp
|
||||
|
||||
# --- core/rendering ---
|
||||
source/core/rendering/animatedsprite.cpp
|
||||
source/core/rendering/fade.cpp
|
||||
source/core/rendering/movingsprite.cpp
|
||||
source/core/rendering/notifications.cpp
|
||||
source/core/rendering/screen.cpp
|
||||
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
|
||||
source/core/rendering/smartsprite.cpp
|
||||
source/core/rendering/sprite.cpp
|
||||
source/core/rendering/text.cpp
|
||||
source/core/rendering/texture.cpp
|
||||
source/core/rendering/writer.cpp
|
||||
|
||||
# --- core/resources ---
|
||||
source/core/resources/asset.cpp
|
||||
source/core/resources/resource.cpp
|
||||
source/core/resources/resource_helper.cpp
|
||||
source/core/resources/resource_loader.cpp
|
||||
source/core/resources/resource_pack.cpp
|
||||
|
||||
# --- core/system ---
|
||||
source/core/system/delta_time.cpp
|
||||
source/core/system/demo.cpp
|
||||
source/core/system/director.cpp
|
||||
|
||||
# --- game ---
|
||||
source/game/game.cpp
|
||||
source/game/options.cpp
|
||||
|
||||
# --- game/entities ---
|
||||
source/game/entities/balloon.cpp
|
||||
source/game/entities/bullet.cpp
|
||||
source/game/entities/item.cpp
|
||||
source/game/entities/player.cpp
|
||||
|
||||
# --- game/scenes ---
|
||||
source/game/scenes/instructions.cpp
|
||||
source/game/scenes/intro.cpp
|
||||
source/game/scenes/logo.cpp
|
||||
source/game/scenes/title.cpp
|
||||
|
||||
# --- game/ui ---
|
||||
source/game/ui/menu.cpp
|
||||
|
||||
# --- utils ---
|
||||
source/utils/utils.cpp
|
||||
)
|
||||
|
||||
# Configuración de SDL3
|
||||
if(EMSCRIPTEN)
|
||||
@@ -36,7 +123,7 @@ if(EMSCRIPTEN)
|
||||
FetchContent_Declare(
|
||||
SDL3
|
||||
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
|
||||
GIT_TAG release-3.2.12
|
||||
GIT_TAG release-3.4.4
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
|
||||
@@ -49,18 +136,83 @@ else()
|
||||
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
|
||||
endif()
|
||||
|
||||
# Configuración de salida de ejecutables
|
||||
if(NOT EMSCRIPTEN)
|
||||
# En desktop, el ejecutable va a la raíz del proyecto
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||
# --- SHADER COMPILATION (Linux/Windows only - macOS uses Metal, Emscripten no soporta SDL3 GPU) ---
|
||||
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
|
||||
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
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
||||
$<$<CONFIG:DEBUG>:DEBUG PAUSE>
|
||||
$<$<CONFIG:DEBUG>:DEBUG>
|
||||
$<$<CONFIG:RELEASE>:RELEASE_BUILD>
|
||||
)
|
||||
|
||||
@@ -86,9 +238,14 @@ elseif(APPLE)
|
||||
)
|
||||
endif()
|
||||
elseif(EMSCRIPTEN)
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE EMSCRIPTEN_BUILD)
|
||||
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
|
||||
--preload-file ${CMAKE_SOURCE_DIR}/data@/data
|
||||
"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
|
||||
)
|
||||
@@ -104,6 +261,7 @@ endif()
|
||||
|
||||
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
|
||||
find_program(CLANG_FORMAT_EXE NAMES clang-format)
|
||||
find_program(CPPCHECK_EXE NAMES cppcheck)
|
||||
|
||||
# Recopilar todos los archivos fuente para analisis
|
||||
file(GLOB_RECURSE ALL_SOURCE_FILES
|
||||
@@ -111,17 +269,12 @@ file(GLOB_RECURSE ALL_SOURCE_FILES
|
||||
"${CMAKE_SOURCE_DIR}/source/*.h"
|
||||
)
|
||||
|
||||
# Excluir stb_image.h y stb_vorbis.c del analisis
|
||||
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
|
||||
set(FORMAT_SOURCES ${ALL_SOURCE_FILES})
|
||||
list(FILTER FORMAT_SOURCES EXCLUDE REGEX ".*stb_image\\.h$")
|
||||
list(FILTER FORMAT_SOURCES EXCLUDE REGEX ".*stb_vorbis\\.c$")
|
||||
list(FILTER FORMAT_SOURCES EXCLUDE REGEX ".*jail_audio\\.hpp$")
|
||||
# Para cppcheck, pasar solo .cpp (los headers se procesan transitivamente).
|
||||
set(CPPCHECK_SOURCES ${ALL_SOURCE_FILES})
|
||||
list(FILTER CPPCHECK_SOURCES INCLUDE REGEX ".*\\.cpp$")
|
||||
list(FILTER CPPCHECK_SOURCES EXCLUDE REGEX ".*/source/external/.*")
|
||||
|
||||
# Targets de clang-tidy
|
||||
if(CLANG_TIDY_EXE)
|
||||
@@ -150,7 +303,7 @@ if(CLANG_FORMAT_EXE)
|
||||
add_custom_target(format
|
||||
COMMAND ${CLANG_FORMAT_EXE}
|
||||
-i
|
||||
${FORMAT_SOURCES}
|
||||
${ALL_SOURCE_FILES}
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
COMMENT "Running clang-format..."
|
||||
)
|
||||
@@ -159,10 +312,84 @@ if(CLANG_FORMAT_EXE)
|
||||
COMMAND ${CLANG_FORMAT_EXE}
|
||||
--dry-run
|
||||
--Werror
|
||||
${FORMAT_SOURCES}
|
||||
${ALL_SOURCE_FILES}
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
COMMENT "Checking clang-format..."
|
||||
)
|
||||
else()
|
||||
message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles")
|
||||
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
|
||||
# ==============================================================================
|
||||
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
|
||||
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
|
||||
BUILDDIR := build
|
||||
|
||||
# ==============================================================================
|
||||
# TARGET NAMES
|
||||
# ==============================================================================
|
||||
TARGET_NAME := coffee_crisis
|
||||
TARGET_FILE := $(DIR_BIN)$(TARGET_NAME)
|
||||
TARGET_FILE := $(BUILDDIR)/$(TARGET_NAME)
|
||||
APP_NAME := Coffee Crisis
|
||||
VERSION := v2.3.3
|
||||
DIST_DIR := dist
|
||||
RELEASE_FOLDER := dist/_tmp
|
||||
RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
|
||||
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
|
||||
# ==============================================================================
|
||||
@@ -50,47 +72,99 @@ endif
|
||||
# WINDOWS-SPECIFIC VARIABLES
|
||||
# ==============================================================================
|
||||
ifeq ($(OS),Windows_NT)
|
||||
WIN_TARGET_FILE := $(DIR_BIN)$(APP_NAME)
|
||||
WIN_TARGET_FILE := $(BUILDDIR)/$(APP_NAME)
|
||||
WIN_RELEASE_FILE := $(RELEASE_FOLDER)/$(APP_NAME)
|
||||
# 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
|
||||
WIN_TARGET_FILE := $(TARGET_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
|
||||
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN CON CMAKE
|
||||
# ==============================================================================
|
||||
all:
|
||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||
@cmake --build build
|
||||
|
||||
debug:
|
||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH)
|
||||
@cmake --build build
|
||||
|
||||
run: all
|
||||
@./$(TARGET_FILE)
|
||||
|
||||
run-debug: debug
|
||||
@./$(TARGET_FILE)
|
||||
|
||||
clean:
|
||||
@rm -rf $(BUILDDIR)
|
||||
|
||||
rebuild: clean all
|
||||
|
||||
# ==============================================================================
|
||||
# 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:
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@"$(MAKE)" windows_release
|
||||
@"$(MAKE)" _windows-release
|
||||
else
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
@$(MAKE) macos_release
|
||||
@$(MAKE) _macos-release
|
||||
else
|
||||
@$(MAKE) linux_release
|
||||
@$(MAKE) _linux-release
|
||||
endif
|
||||
endif
|
||||
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN PARA WINDOWS (RELEASE)
|
||||
# ==============================================================================
|
||||
windows_release:
|
||||
_windows-release:
|
||||
@$(MAKE) pack
|
||||
@echo off
|
||||
@echo Creando release para Windows - Version: $(VERSION)
|
||||
|
||||
# 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
|
||||
|
||||
# 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}"
|
||||
|
||||
# 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 'README.md' -Destination '$(RELEASE_FOLDER)'"
|
||||
@powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
|
||||
@powershell -Command "Copy-Item -Path '$(TARGET_FILE).exe' -Destination '$(WIN_RELEASE_FILE).exe'"
|
||||
@powershell -Command "Copy-Item -Path '$(TARGET_FILE).exe' -Destination '$(WIN_RELEASE_FILE_PS).exe'"
|
||||
strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
|
||||
|
||||
# Crea el fichero .zip
|
||||
@@ -117,15 +192,32 @@ windows_release:
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN PARA MACOS (RELEASE)
|
||||
# ==============================================================================
|
||||
macos_release:
|
||||
_macos-release:
|
||||
@$(MAKE) pack
|
||||
@echo "Creando release para macOS - Version: $(VERSION)"
|
||||
|
||||
# Verificar e instalar create-dmg si es necesario
|
||||
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg)
|
||||
|
||||
# Compila la versión para procesadores Intel con cmake
|
||||
@cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON
|
||||
@cmake --build build/intel
|
||||
# Verifica dependencias necesarias (create-dmg). Si falta, intenta instalarla
|
||||
# con brew; si brew tampoco está, indica el comando exacto al usuario.
|
||||
@command -v create-dmg >/dev/null 2>&1 || { \
|
||||
echo ""; \
|
||||
echo "============================================"; \
|
||||
echo " Falta la dependencia: create-dmg"; \
|
||||
echo "============================================"; \
|
||||
if command -v brew >/dev/null 2>&1; then \
|
||||
echo " Instalando con: brew install create-dmg"; \
|
||||
brew install create-dmg || { \
|
||||
echo ""; \
|
||||
echo " ERROR: 'brew install create-dmg' ha fallado."; \
|
||||
echo " Ejecuta el comando manualmente y vuelve a probar."; \
|
||||
exit 1; \
|
||||
}; \
|
||||
else \
|
||||
echo " Homebrew no está instalado."; \
|
||||
echo " Instálalo desde https://brew.sh y luego ejecuta:"; \
|
||||
echo " brew install create-dmg"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
}
|
||||
|
||||
# Elimina datos de compilaciones anteriores
|
||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||
@@ -140,7 +232,8 @@ macos_release:
|
||||
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||
|
||||
# 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 release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||
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>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
|
||||
|
||||
# Copia el ejecutable Intel al bundle
|
||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
||||
|
||||
# Firma la aplicación
|
||||
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
|
||||
|
||||
# Empaqueta el .dmg de la versión Intel con create-dmg
|
||||
@echo "Creando DMG Intel..."
|
||||
create-dmg \
|
||||
--volname "$(APP_NAME)" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 720 300 \
|
||||
--icon-size 96 \
|
||||
--text-size 12 \
|
||||
--icon "$(APP_NAME).app" 278 102 \
|
||||
--icon "LICENSE" 441 102 \
|
||||
--icon "README.md" 604 102 \
|
||||
--app-drop-link 115 102 \
|
||||
--hide-extension "$(APP_NAME).app" \
|
||||
"$(MACOS_INTEL_RELEASE)" \
|
||||
"$(RELEASE_FOLDER)" || true
|
||||
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"
|
||||
# Compila y empaqueta la versión Intel (best-effort: si falla, se omite el
|
||||
# DMG Intel y continúa con la build de Apple Silicon).
|
||||
@echo ""
|
||||
@echo "============================================"
|
||||
@echo " Compilando version Intel (x86_64)"
|
||||
@echo "============================================"
|
||||
@if cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \
|
||||
-DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH) \
|
||||
&& cmake --build build/intel; then \
|
||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"; \
|
||||
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"; \
|
||||
echo "Creando DMG Intel..."; \
|
||||
create-dmg \
|
||||
--volname "$(APP_NAME)" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 720 300 \
|
||||
--icon-size 96 \
|
||||
--text-size 12 \
|
||||
--icon "$(APP_NAME).app" 278 102 \
|
||||
--icon "LICENSE" 441 102 \
|
||||
--icon "README.md" 604 102 \
|
||||
--app-drop-link 115 102 \
|
||||
--hide-extension "$(APP_NAME).app" \
|
||||
"$(MACOS_INTEL_RELEASE)" \
|
||||
"$(RELEASE_FOLDER)" || true; \
|
||||
echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"; \
|
||||
else \
|
||||
echo ""; \
|
||||
echo "============================================"; \
|
||||
echo " WARNING: la build Intel ha fallado."; \
|
||||
echo " Se omite el DMG Intel y se continúa con"; \
|
||||
echo " la build de Apple Silicon."; \
|
||||
echo "============================================"; \
|
||||
echo ""; \
|
||||
fi
|
||||
|
||||
# Compila la versión para procesadores Apple Silicon con cmake
|
||||
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON
|
||||
@echo ""
|
||||
@echo "============================================"
|
||||
@echo " Compilando version Apple Silicon (arm64)"
|
||||
@echo "============================================"
|
||||
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH)
|
||||
@cmake --build build/arm
|
||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
||||
|
||||
@@ -210,11 +322,12 @@ macos_release:
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN PARA LINUX (RELEASE)
|
||||
# ==============================================================================
|
||||
linux_release:
|
||||
_linux-release:
|
||||
@$(MAKE) pack
|
||||
@echo "Creando release para Linux - Version: $(VERSION)"
|
||||
|
||||
# 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
|
||||
|
||||
# Elimina carpeta temporal previa y la recrea
|
||||
@@ -222,7 +335,8 @@ linux_release:
|
||||
$(MKDIR) "$(RELEASE_FOLDER)"
|
||||
|
||||
# Copia ficheros
|
||||
cp -R data "$(RELEASE_FOLDER)"
|
||||
cp build/resources.pack "$(RELEASE_FOLDER)"
|
||||
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
|
||||
cp LICENSE "$(RELEASE_FOLDER)"
|
||||
cp README.md "$(RELEASE_FOLDER)"
|
||||
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
|
||||
@@ -240,23 +354,90 @@ linux_release:
|
||||
# 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 && cmake --build build/wasm"
|
||||
bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm"
|
||||
$(MKDIR) "$(DIST_DIR)/wasm"
|
||||
cp build/wasm/$(TARGET_NAME).html $(DIST_DIR)/wasm/
|
||||
cp build/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/
|
||||
cp build/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/
|
||||
cp build/wasm/$(TARGET_NAME).data $(DIST_DIR)/wasm/
|
||||
@echo "Output: $(DIST_DIR)/wasm/"
|
||||
scp $(DIST_DIR)/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/$(TARGET_NAME).data \
|
||||
maverick:/home/sergio/gitea/web_jailgames/static/games/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
|
||||
# ==============================================================================
|
||||
show_version:
|
||||
show-version:
|
||||
@echo "Version actual: $(VERSION)"
|
||||
|
||||
help:
|
||||
@@ -267,15 +448,32 @@ help:
|
||||
@echo " make - Compilar con cmake (Release)"
|
||||
@echo " make debug - Compilar con cmake (Debug)"
|
||||
@echo ""
|
||||
@echo " Ejecucion:"
|
||||
@echo " make run - Compilar (Release) y ejecutar"
|
||||
@echo " make run-debug - Compilar (Debug) y ejecutar"
|
||||
@echo ""
|
||||
@echo " Release:"
|
||||
@echo " make release - Crear release (detecta SO automaticamente)"
|
||||
@echo " make windows_release - Crear release para Windows"
|
||||
@echo " make linux_release - Crear release para Linux"
|
||||
@echo " make macos_release - Crear release para macOS"
|
||||
@echo " make wasm - Compilar para WebAssembly (requiere Docker)"
|
||||
@echo " make wasm - Compilar para WebAssembly (requiere Docker) y deploy a maverick"
|
||||
@echo " make wasm-debug - Compilar WebAssembly Debug local (sin deploy)"
|
||||
@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 " Otros:"
|
||||
@echo " make show_version - Mostrar version actual ($(VERSION))"
|
||||
@echo " make clean - Borrar carpeta $(BUILDDIR)/"
|
||||
@echo " make rebuild - clean + all"
|
||||
@echo " make show-version - Mostrar version actual ($(VERSION))"
|
||||
@echo " make hooks-install - Activar git hooks del proyecto"
|
||||
@echo " make help - Mostrar esta ayuda"
|
||||
|
||||
.PHONY: all debug release windows_release macos_release linux_release wasm 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 |
+20
-2
@@ -140,7 +140,7 @@ CONTINUAR?
|
||||
CONTINUAR
|
||||
|
||||
## 47 - MENU DE PAUSA
|
||||
EIXIR DEL JOC
|
||||
TORNAR AL TITOL
|
||||
|
||||
## 48 - MENU GAME OVER
|
||||
SI
|
||||
@@ -284,4 +284,22 @@ TAULER DE PUNTS
|
||||
CONNECTAT
|
||||
|
||||
## 95 - NOTIFICACIO COMANDAMENT
|
||||
DESCONNECTAT
|
||||
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
|
||||
+20
-2
@@ -140,7 +140,7 @@ CONTINUE?
|
||||
CONTINUE
|
||||
|
||||
## 47 - MENU DE PAUSA
|
||||
LEAVE GAME
|
||||
BACK TO TITLE
|
||||
|
||||
## 48 - MENU GAME OVER
|
||||
YES
|
||||
@@ -284,4 +284,22 @@ HISCORE TABLE
|
||||
CONNECTED
|
||||
|
||||
## 95 - GAMEPAD NOTIFICATION
|
||||
DISCONNECTED
|
||||
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
|
||||
+20
-2
@@ -140,7 +140,7 @@ CONTINUAR?
|
||||
CONTINUAR
|
||||
|
||||
## 47 - MENU DE PAUSA
|
||||
SALIR DEL JUEGO
|
||||
VOLVER AL TITULO
|
||||
|
||||
## 48 - MENU GAME OVER
|
||||
SI
|
||||
@@ -284,4 +284,22 @@ TABLA DE PUNTUACIONES
|
||||
CONECTADO
|
||||
|
||||
## 95 - NOTIFICACION MANDO
|
||||
DESCONECTADO
|
||||
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];
|
||||
}
|
||||
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,212 @@
|
||||
#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
|
||||
#include "external/stb_vorbis.h"
|
||||
// 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
|
||||
@@ -3,9 +3,9 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace Mouse {
|
||||
extern Uint32 cursorHideTime; // Tiempo en milisegundos para ocultar el cursor por inactividad
|
||||
extern Uint32 lastMouseMoveTime; // Última vez que el ratón se movió
|
||||
extern bool cursorVisible; // Estado del cursor
|
||||
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.
|
||||
@@ -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,48 @@
|
||||
#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
|
||||
SDL_Rect rect1_{}; // Rectangulo usado para crear los efectos de transición
|
||||
SDL_Rect rect2_{}; // Rectangulo usado para crear los efectos de transición
|
||||
};
|
||||
@@ -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)
|
||||
systemFolder = "/config/" + folder;
|
||||
#elif _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;
|
||||
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(systemFolder.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,821 +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
|
||||
#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 <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 "mouse.hpp" // for Mouse::handleEvent, Mouse::upda...
|
||||
#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
|
||||
|
||||
#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
|
||||
#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);
|
||||
|
||||
activeSection = ActiveSection::None;
|
||||
}
|
||||
|
||||
Director::~Director() {
|
||||
saveConfigFile();
|
||||
|
||||
// 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();
|
||||
|
||||
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
|
||||
// SOUTH queda sin asignar para evitar salidas accidentales: pausa/cancel se hace con START/BACK.
|
||||
input->bindGameControllerButton(input_accept, SDL_GAMEPAD_BUTTON_EAST);
|
||||
#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;
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// En Emscripten la ventana la gestiona el navegador
|
||||
options->windowSize = 1;
|
||||
options->videoMode = 0;
|
||||
options->integerScale = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// 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 __EMSCRIPTEN__
|
||||
// En Emscripten usamos una carpeta en MEMFS (no persistente)
|
||||
systemFolder = "/config/" + folder;
|
||||
#elif _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
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// En Emscripten no necesitamos crear carpetas (MEMFS las crea automáticamente)
|
||||
(void)folder;
|
||||
#else
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Gestiona las transiciones entre secciones
|
||||
void Director::handleSectionTransition() {
|
||||
// Determina qué sección debería estar activa
|
||||
ActiveSection targetSection = ActiveSection::None;
|
||||
switch (section->name) {
|
||||
case SECTION_PROG_LOGO:
|
||||
targetSection = ActiveSection::Logo;
|
||||
break;
|
||||
case SECTION_PROG_INTRO:
|
||||
targetSection = ActiveSection::Intro;
|
||||
break;
|
||||
case SECTION_PROG_TITLE:
|
||||
targetSection = ActiveSection::Title;
|
||||
break;
|
||||
case SECTION_PROG_GAME:
|
||||
targetSection = ActiveSection::Game;
|
||||
break;
|
||||
}
|
||||
|
||||
// Si no ha cambiado, no hay nada que hacer
|
||||
if (targetSection == activeSection) return;
|
||||
|
||||
// Destruye la sección anterior
|
||||
logo.reset();
|
||||
intro.reset();
|
||||
title.reset();
|
||||
game.reset();
|
||||
|
||||
// Crea la nueva sección
|
||||
activeSection = targetSection;
|
||||
switch (activeSection) {
|
||||
case ActiveSection::Logo:
|
||||
logo = std::make_unique<Logo>(renderer, screen, asset, input, section);
|
||||
break;
|
||||
case ActiveSection::Intro:
|
||||
intro = std::make_unique<Intro>(renderer, screen, asset, input, lang, section);
|
||||
break;
|
||||
case ActiveSection::Title:
|
||||
title = std::make_unique<Title>(renderer, screen, input, asset, options, lang, section);
|
||||
break;
|
||||
case ActiveSection::Game: {
|
||||
const int numPlayers = section->subsection == SUBSECTION_GAME_PLAY_1P ? 1 : 2;
|
||||
game = std::make_unique<Game>(numPlayers, 0, renderer, screen, asset, lang, input, false, options, section);
|
||||
break;
|
||||
}
|
||||
case ActiveSection::None:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ejecuta un frame del juego
|
||||
SDL_AppResult Director::iterate() {
|
||||
#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->videoMode != 0);
|
||||
|
||||
// Gestiona las transiciones entre secciones
|
||||
handleSectionTransition();
|
||||
|
||||
// Ejecuta un frame de la sección activa
|
||||
switch (activeSection) {
|
||||
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
|
||||
SDL_AppResult Director::handleEvent(SDL_Event *event) {
|
||||
#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->handleGamepadAdded(event->gdevice.which, name)) {
|
||||
screen->notify(name + " " + lang->getText(94),
|
||||
color_t{0x40, 0xFF, 0x40},
|
||||
color_t{0, 0, 0},
|
||||
2500);
|
||||
}
|
||||
} else if (event->type == SDL_EVENT_GAMEPAD_REMOVED) {
|
||||
std::string name;
|
||||
if (input->handleGamepadRemoved(event->gdevice.which, name)) {
|
||||
screen->notify(name + " " + lang->getText(95),
|
||||
color_t{0xFF, 0x50, 0x50},
|
||||
color_t{0, 0, 0},
|
||||
2500);
|
||||
}
|
||||
}
|
||||
|
||||
// Gestiona la visibilidad del cursor según el movimiento del ratón
|
||||
Mouse::handleEvent(*event, options->videoMode != 0);
|
||||
|
||||
// Reenvía el evento a la sección activa
|
||||
switch (activeSection) {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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,96 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <memory>
|
||||
#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";
|
||||
|
||||
// Secciones activas del Director
|
||||
enum class ActiveSection { None,
|
||||
Logo,
|
||||
Intro,
|
||||
Title,
|
||||
Game };
|
||||
|
||||
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;
|
||||
|
||||
// Secciones del juego
|
||||
ActiveSection activeSection;
|
||||
std::unique_ptr<Logo> logo;
|
||||
std::unique_ptr<Intro> intro;
|
||||
std::unique_ptr<Title> title;
|
||||
std::unique_ptr<Game> game;
|
||||
|
||||
// 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);
|
||||
|
||||
// Gestiona las transiciones entre secciones
|
||||
void handleSectionTransition();
|
||||
|
||||
public:
|
||||
// Constructor
|
||||
Director(int argc, const char *argv[]);
|
||||
|
||||
// Destructor
|
||||
~Director();
|
||||
|
||||
// Ejecuta un frame del juego
|
||||
SDL_AppResult iterate();
|
||||
|
||||
// Procesa un evento
|
||||
SDL_AppResult handleEvent(SDL_Event *event);
|
||||
};
|
||||
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/
|
||||
//
|
||||
// Original version written by Sean Barrett in 2007.
|
||||
@@ -29,12 +29,15 @@
|
||||
// Bernhard Wodo Evan Balster github:alxprd
|
||||
// Tom Beaumont Ingo Leitgeb Nicolas Guillemot
|
||||
// Phillip Bennefall Rohit Thiago Goulart
|
||||
// github:manxorist saga musix github:infatum
|
||||
// github:manxorist Saga Musix github:infatum
|
||||
// Timur Gagiev Maxwell Koo Peter Waller
|
||||
// github:audinowho Dougall Johnson David Reid
|
||||
// github:Clownacy Pedro J. Estebanez Remi Verschelde
|
||||
// AnthoFoxo github:morlat Gabriel Ravier
|
||||
//
|
||||
// Partial history:
|
||||
// 1.22 - 2021-07-11 - various small fixes
|
||||
// 1.21 - 2021-07-02 - fix bug for files with no comments
|
||||
// 1.20 - 2020-07-11 - several small fixes
|
||||
// 1.19 - 2020-02-05 - warnings
|
||||
// 1.18 - 2020-02-02 - fix seek bugs; parse header comments; misc warnings etc.
|
||||
@@ -220,6 +223,12 @@ extern int stb_vorbis_decode_frame_pushdata(
|
||||
// channel. In other words, (*output)[0][0] contains the first sample from
|
||||
// the first channel, and (*output)[1][0] contains the first sample from
|
||||
// the second channel.
|
||||
//
|
||||
// *output points into stb_vorbis's internal output buffer storage; these
|
||||
// buffers are owned by stb_vorbis and application code should not free
|
||||
// them or modify their contents. They are transient and will be overwritten
|
||||
// once you ask for more data to get decoded, so be sure to grab any data
|
||||
// you need before then.
|
||||
|
||||
extern void stb_vorbis_flush_pushdata(stb_vorbis *f);
|
||||
// inform stb_vorbis that your next datablock will not be contiguous with
|
||||
@@ -579,7 +588,7 @@ enum STBVorbisError
|
||||
#if defined(_MSC_VER) || defined(__MINGW32__)
|
||||
#include <malloc.h>
|
||||
#endif
|
||||
#if defined(__linux__) || defined(__linux) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__)
|
||||
#if defined(__linux__) || defined(__linux) || defined(__sun__) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__)
|
||||
#include <alloca.h>
|
||||
#endif
|
||||
#else // STB_VORBIS_NO_CRT
|
||||
@@ -646,6 +655,12 @@ typedef signed int int32;
|
||||
|
||||
typedef float codetype;
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#define STBV_NOTUSED(v) (void)(v)
|
||||
#else
|
||||
#define STBV_NOTUSED(v) (void)sizeof(v)
|
||||
#endif
|
||||
|
||||
// @NOTE
|
||||
//
|
||||
// Some arrays below are tagged "//varies", which means it's actually
|
||||
@@ -1046,7 +1061,7 @@ static float float32_unpack(uint32 x)
|
||||
uint32 sign = x & 0x80000000;
|
||||
uint32 exp = (x & 0x7fe00000) >> 21;
|
||||
double res = sign ? -(double)mantissa : (double)mantissa;
|
||||
return (float) ldexp((float)res, exp-788);
|
||||
return (float) ldexp((float)res, (int)exp-788);
|
||||
}
|
||||
|
||||
|
||||
@@ -1077,6 +1092,7 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
|
||||
// find the first entry
|
||||
for (k=0; k < n; ++k) if (len[k] < NO_CODE) break;
|
||||
if (k == n) { assert(c->sorted_entries == 0); return TRUE; }
|
||||
assert(len[k] < 32); // no error return required, code reading lens checks this
|
||||
// add to the list
|
||||
add_entry(c, 0, k, m++, len[k], values);
|
||||
// add all available leaves
|
||||
@@ -1090,6 +1106,7 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
|
||||
uint32 res;
|
||||
int z = len[i], y;
|
||||
if (z == NO_CODE) continue;
|
||||
assert(z < 32); // no error return required, code reading lens checks this
|
||||
// find lowest available leaf (should always be earliest,
|
||||
// which is what the specification calls for)
|
||||
// note that this property, and the fact we can never have
|
||||
@@ -1099,12 +1116,10 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
|
||||
while (z > 0 && !available[z]) --z;
|
||||
if (z == 0) { return FALSE; }
|
||||
res = available[z];
|
||||
assert(z >= 0 && z < 32);
|
||||
available[z] = 0;
|
||||
add_entry(c, bit_reverse(res), i, m++, len[i], values);
|
||||
// propagate availability up the tree
|
||||
if (z != len[i]) {
|
||||
assert(len[i] >= 0 && len[i] < 32);
|
||||
for (y=len[i]; y > z; --y) {
|
||||
assert(available[y] == 0);
|
||||
available[y] = res + (1 << (32-y));
|
||||
@@ -2577,34 +2592,33 @@ static void imdct_step3_inner_s_loop_ld654(int n, float *e, int i_off, float *A,
|
||||
|
||||
while (z > base) {
|
||||
float k00,k11;
|
||||
float l00,l11;
|
||||
|
||||
k00 = z[-0] - z[-8];
|
||||
k11 = z[-1] - z[-9];
|
||||
z[-0] = z[-0] + z[-8];
|
||||
z[-1] = z[-1] + z[-9];
|
||||
z[-8] = k00;
|
||||
z[-9] = k11 ;
|
||||
k00 = z[-0] - z[ -8];
|
||||
k11 = z[-1] - z[ -9];
|
||||
l00 = z[-2] - z[-10];
|
||||
l11 = z[-3] - z[-11];
|
||||
z[ -0] = z[-0] + z[ -8];
|
||||
z[ -1] = z[-1] + z[ -9];
|
||||
z[ -2] = z[-2] + z[-10];
|
||||
z[ -3] = z[-3] + z[-11];
|
||||
z[ -8] = k00;
|
||||
z[ -9] = k11;
|
||||
z[-10] = (l00+l11) * A2;
|
||||
z[-11] = (l11-l00) * A2;
|
||||
|
||||
k00 = z[ -2] - z[-10];
|
||||
k11 = z[ -3] - z[-11];
|
||||
z[ -2] = z[ -2] + z[-10];
|
||||
z[ -3] = z[ -3] + z[-11];
|
||||
z[-10] = (k00+k11) * A2;
|
||||
z[-11] = (k11-k00) * A2;
|
||||
|
||||
k00 = z[-12] - z[ -4]; // reverse to avoid a unary negation
|
||||
k00 = z[ -4] - z[-12];
|
||||
k11 = z[ -5] - z[-13];
|
||||
l00 = z[ -6] - z[-14];
|
||||
l11 = z[ -7] - z[-15];
|
||||
z[ -4] = z[ -4] + z[-12];
|
||||
z[ -5] = z[ -5] + z[-13];
|
||||
z[-12] = k11;
|
||||
z[-13] = k00;
|
||||
|
||||
k00 = z[-14] - z[ -6]; // reverse to avoid a unary negation
|
||||
k11 = z[ -7] - z[-15];
|
||||
z[ -6] = z[ -6] + z[-14];
|
||||
z[ -7] = z[ -7] + z[-15];
|
||||
z[-14] = (k00+k11) * A2;
|
||||
z[-15] = (k00-k11) * A2;
|
||||
z[-12] = k11;
|
||||
z[-13] = -k00;
|
||||
z[-14] = (l11-l00) * A2;
|
||||
z[-15] = (l00+l11) * -A2;
|
||||
|
||||
iter_54(z);
|
||||
iter_54(z-8);
|
||||
@@ -3069,6 +3083,7 @@ static int do_floor(vorb *f, Mapping *map, int i, int n, float *target, YTYPE *f
|
||||
for (q=1; q < g->values; ++q) {
|
||||
j = g->sorted_order[q];
|
||||
#ifndef STB_VORBIS_NO_DEFER_FLOOR
|
||||
STBV_NOTUSED(step2_flag);
|
||||
if (finalY[j] >= 0)
|
||||
#else
|
||||
if (step2_flag[j])
|
||||
@@ -3171,6 +3186,7 @@ static int vorbis_decode_packet_rest(vorb *f, int *len, Mode *m, int left_start,
|
||||
|
||||
// WINDOWING
|
||||
|
||||
STBV_NOTUSED(left_end);
|
||||
n = f->blocksize[m->blockflag];
|
||||
map = &f->mapping[m->mapping];
|
||||
|
||||
@@ -3368,7 +3384,7 @@ static int vorbis_decode_packet_rest(vorb *f, int *len, Mode *m, int left_start,
|
||||
// this isn't to spec, but spec would require us to read ahead
|
||||
// and decode the size of all current frames--could be done,
|
||||
// but presumably it's not a commonly used feature
|
||||
f->current_loc = -n2; // start of first frame is positioned for discard
|
||||
f->current_loc = 0u - n2; // start of first frame is positioned for discard (NB this is an intentional unsigned overflow/wrap-around)
|
||||
// we might have to discard samples "from" the next frame too,
|
||||
// if we're lapping a large block then a small at the start?
|
||||
f->discard_samples_deferred = n - right_end;
|
||||
@@ -3642,9 +3658,11 @@ static int start_decoder(vorb *f)
|
||||
f->vendor[len] = (char)'\0';
|
||||
//user comments
|
||||
f->comment_list_length = get32_packet(f);
|
||||
if (f->comment_list_length > 0) {
|
||||
f->comment_list = (char**)setup_malloc(f, sizeof(char*) * (f->comment_list_length));
|
||||
if (f->comment_list == NULL) return error(f, VORBIS_outofmem);
|
||||
f->comment_list = NULL;
|
||||
if (f->comment_list_length > 0)
|
||||
{
|
||||
f->comment_list = (char**) setup_malloc(f, sizeof(char*) * (f->comment_list_length));
|
||||
if (f->comment_list == NULL) return error(f, VORBIS_outofmem);
|
||||
}
|
||||
|
||||
for(i=0; i < f->comment_list_length; ++i) {
|
||||
@@ -3867,8 +3885,7 @@ static int start_decoder(vorb *f)
|
||||
unsigned int div=1;
|
||||
for (k=0; k < c->dimensions; ++k) {
|
||||
int off = (z / div) % c->lookup_values;
|
||||
float val = mults[off];
|
||||
val = mults[off]*c->delta_value + c->minimum_value + last;
|
||||
float val = mults[off]*c->delta_value + c->minimum_value + last;
|
||||
c->multiplicands[j*c->dimensions + k] = val;
|
||||
if (c->sequence_p)
|
||||
last = val;
|
||||
@@ -3951,7 +3968,7 @@ static int start_decoder(vorb *f)
|
||||
if (g->class_masterbooks[j] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
|
||||
}
|
||||
for (k=0; k < 1 << g->class_subclasses[j]; ++k) {
|
||||
g->subclass_books[j][k] = get_bits(f,8)-1;
|
||||
g->subclass_books[j][k] = (int16)get_bits(f,8)-1;
|
||||
if (g->subclass_books[j][k] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
|
||||
}
|
||||
}
|
||||
@@ -4509,6 +4526,7 @@ stb_vorbis *stb_vorbis_open_pushdata(
|
||||
*error = VORBIS_need_more_data;
|
||||
else
|
||||
*error = p.error;
|
||||
vorbis_deinit(&p);
|
||||
return NULL;
|
||||
}
|
||||
f = vorbis_alloc(&p);
|
||||
@@ -4566,7 +4584,7 @@ static uint32 vorbis_find_page(stb_vorbis *f, uint32 *end, uint32 *last)
|
||||
header[i] = get8(f);
|
||||
if (f->eof) return 0;
|
||||
if (header[4] != 0) goto invalid;
|
||||
goal = header[22] + (header[23] << 8) + (header[24]<<16) + (header[25]<<24);
|
||||
goal = header[22] + (header[23] << 8) + (header[24]<<16) + ((uint32)header[25]<<24);
|
||||
for (i=22; i < 26; ++i)
|
||||
header[i] = 0;
|
||||
crc = 0;
|
||||
@@ -4970,7 +4988,7 @@ unsigned int stb_vorbis_stream_length_in_samples(stb_vorbis *f)
|
||||
// set. whoops!
|
||||
break;
|
||||
}
|
||||
previous_safe = last_page_loc+1;
|
||||
//previous_safe = last_page_loc+1; // NOTE: not used after this point, but note for debugging
|
||||
last_page_loc = stb_vorbis_get_file_offset(f);
|
||||
}
|
||||
|
||||
@@ -5081,7 +5099,10 @@ stb_vorbis * stb_vorbis_open_filename(const char *filename, int *error, const st
|
||||
stb_vorbis * stb_vorbis_open_memory(const unsigned char *data, int len, int *error, const stb_vorbis_alloc *alloc)
|
||||
{
|
||||
stb_vorbis *f, p;
|
||||
if (data == NULL) return NULL;
|
||||
if (!data) {
|
||||
if (error) *error = VORBIS_unexpected_eof;
|
||||
return NULL;
|
||||
}
|
||||
vorbis_init(&p, alloc);
|
||||
p.stream = (uint8 *) data;
|
||||
p.stream_end = (uint8 *) data + len;
|
||||
@@ -5156,11 +5177,11 @@ static void copy_samples(short *dest, float *src, int len)
|
||||
|
||||
static void compute_samples(int mask, short *output, int num_c, float **data, int d_offset, int len)
|
||||
{
|
||||
#define BUFFER_SIZE 32
|
||||
float buffer[BUFFER_SIZE];
|
||||
int i,j,o,n = BUFFER_SIZE;
|
||||
#define STB_BUFFER_SIZE 32
|
||||
float buffer[STB_BUFFER_SIZE];
|
||||
int i,j,o,n = STB_BUFFER_SIZE;
|
||||
check_endianness();
|
||||
for (o = 0; o < len; o += BUFFER_SIZE) {
|
||||
for (o = 0; o < len; o += STB_BUFFER_SIZE) {
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
if (o + n > len) n = len - o;
|
||||
for (j=0; j < num_c; ++j) {
|
||||
@@ -5177,16 +5198,17 @@ static void compute_samples(int mask, short *output, int num_c, float **data, in
|
||||
output[o+i] = v;
|
||||
}
|
||||
}
|
||||
#undef STB_BUFFER_SIZE
|
||||
}
|
||||
|
||||
static void compute_stereo_samples(short *output, int num_c, float **data, int d_offset, int len)
|
||||
{
|
||||
#define BUFFER_SIZE 32
|
||||
float buffer[BUFFER_SIZE];
|
||||
int i,j,o,n = BUFFER_SIZE >> 1;
|
||||
#define STB_BUFFER_SIZE 32
|
||||
float buffer[STB_BUFFER_SIZE];
|
||||
int i,j,o,n = STB_BUFFER_SIZE >> 1;
|
||||
// o is the offset in the source data
|
||||
check_endianness();
|
||||
for (o = 0; o < len; o += BUFFER_SIZE >> 1) {
|
||||
for (o = 0; o < len; o += STB_BUFFER_SIZE >> 1) {
|
||||
// o2 is the offset in the output data
|
||||
int o2 = o << 1;
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
@@ -5216,6 +5238,7 @@ static void compute_stereo_samples(short *output, int num_c, float **data, int d
|
||||
output[o2+i] = v;
|
||||
}
|
||||
}
|
||||
#undef STB_BUFFER_SIZE
|
||||
}
|
||||
|
||||
static void convert_samples_short(int buf_c, short **buffer, int b_offset, int data_c, float **data, int d_offset, int samples)
|
||||
@@ -5288,8 +5311,6 @@ int stb_vorbis_get_samples_short_interleaved(stb_vorbis *f, int channels, short
|
||||
float **outputs;
|
||||
int len = num_shorts / channels;
|
||||
int n=0;
|
||||
int z = f->channels;
|
||||
if (z > channels) z = channels;
|
||||
while (n < len) {
|
||||
int k = f->channel_buffer_end - f->channel_buffer_start;
|
||||
if (n+k >= len) k = len - n;
|
||||
@@ -5308,8 +5329,6 @@ int stb_vorbis_get_samples_short(stb_vorbis *f, int channels, short **buffer, in
|
||||
{
|
||||
float **outputs;
|
||||
int n=0;
|
||||
int z = f->channels;
|
||||
if (z > channels) z = channels;
|
||||
while (n < len) {
|
||||
int k = f->channel_buffer_end - f->channel_buffer_start;
|
||||
if (n+k >= len) k = len - n;
|
||||
-180
@@ -1,180 +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;
|
||||
mROriginal = r;
|
||||
mGOriginal = g;
|
||||
mBOriginal = b;
|
||||
mLastSquareTicks = 0;
|
||||
mSquaresDrawn = 0;
|
||||
mFullscreenDone = false;
|
||||
}
|
||||
|
||||
// Pinta una transición en pantalla
|
||||
void Fade::render() {
|
||||
if (mEnabled && !mFinished) {
|
||||
switch (mFadeType) {
|
||||
case FADE_FULLSCREEN: {
|
||||
if (!mFullscreenDone) {
|
||||
SDL_FRect fRect1 = {0, 0, (float)GAMECANVAS_WIDTH, (float)GAMECANVAS_HEIGHT};
|
||||
|
||||
int alpha = mCounter * 4;
|
||||
if (alpha >= 255) {
|
||||
alpha = 255;
|
||||
mFullscreenDone = true;
|
||||
|
||||
// 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);
|
||||
|
||||
mFinished = true;
|
||||
} else {
|
||||
// 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, alpha);
|
||||
SDL_RenderFillRect(mRenderer, &fRect1);
|
||||
}
|
||||
}
|
||||
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: {
|
||||
Uint32 now = SDL_GetTicks();
|
||||
if (mSquaresDrawn < 50 && now - mLastSquareTicks >= 100) {
|
||||
mLastSquareTicks = now;
|
||||
|
||||
SDL_FRect fRs = {0, 0, 32, 32};
|
||||
|
||||
// Crea un color al azar
|
||||
Uint8 r = 255 * (rand() % 2);
|
||||
Uint8 g = 255 * (rand() % 2);
|
||||
Uint8 b = 255 * (rand() % 2);
|
||||
SDL_SetRenderDrawColor(mRenderer, r, g, b, 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);
|
||||
|
||||
mSquaresDrawn++;
|
||||
}
|
||||
|
||||
// Copiamos el backbuffer al renderizador
|
||||
SDL_RenderTexture(mRenderer, mBackbuffer, nullptr, nullptr);
|
||||
|
||||
if (mSquaresDrawn >= 50) {
|
||||
mFinished = true;
|
||||
}
|
||||
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;
|
||||
mSquaresDrawn = 0;
|
||||
mLastSquareTicks = 0;
|
||||
mFullscreenDone = false;
|
||||
mR = mROriginal;
|
||||
mG = mGOriginal;
|
||||
mB = mBOriginal;
|
||||
}
|
||||
|
||||
// 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,54 +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
|
||||
Uint8 mROriginal, mGOriginal, mBOriginal; // Colores originales para FADE_RANDOM_SQUARE
|
||||
Uint32 mLastSquareTicks; // Ticks del último cuadrado dibujado (FADE_RANDOM_SQUARE)
|
||||
Uint16 mSquaresDrawn; // Número de cuadrados dibujados (FADE_RANDOM_SQUARE)
|
||||
bool mFullscreenDone; // Indica si el fade fullscreen ha terminado la fase de fundido
|
||||
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);
|
||||
};
|
||||
-3522
File diff suppressed because it is too large
Load Diff
-568
@@ -1,568 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <string> // for string, basic_string
|
||||
#include <vector> // for vector
|
||||
|
||||
#include "utils.h" // for demoKeys_t, color_t
|
||||
class Asset;
|
||||
class Balloon;
|
||||
class Bullet;
|
||||
class Fade;
|
||||
class Input;
|
||||
class Item;
|
||||
class Lang;
|
||||
class Menu;
|
||||
class MovingSprite;
|
||||
class Player;
|
||||
class Screen;
|
||||
class SmartSprite;
|
||||
class Sprite;
|
||||
class Text;
|
||||
class Texture;
|
||||
struct JA_Music_t;
|
||||
struct JA_Sound_t;
|
||||
|
||||
// Cantidad de elementos a escribir en los ficheros de datos
|
||||
constexpr int TOTAL_SCORE_DATA = 3;
|
||||
constexpr int TOTAL_DEMO_DATA = 2000;
|
||||
|
||||
// Contadores
|
||||
constexpr int STAGE_COUNTER = 200;
|
||||
constexpr int SHAKE_COUNTER = 10;
|
||||
constexpr int HELP_COUNTER = 1000;
|
||||
constexpr int GAME_COMPLETED_START_FADE = 500;
|
||||
constexpr int GAME_COMPLETED_END = 700;
|
||||
|
||||
// Formaciones enemigas
|
||||
constexpr int NUMBER_OF_ENEMY_FORMATIONS = 100;
|
||||
constexpr int MAX_NUMBER_OF_ENEMIES_IN_A_FORMATION = 50;
|
||||
|
||||
// Porcentaje de aparición de los objetos
|
||||
constexpr int ITEM_POINTS_1_DISK_ODDS = 10;
|
||||
constexpr int ITEM_POINTS_2_GAVINA_ODDS = 6;
|
||||
constexpr int ITEM_POINTS_3_PACMAR_ODDS = 3;
|
||||
constexpr int ITEM_CLOCK_ODDS = 5;
|
||||
constexpr int ITEM_COFFEE_ODDS = 5;
|
||||
constexpr int ITEM_POWER_BALL_ODDS = 0;
|
||||
constexpr int ITEM_COFFEE_MACHINE_ODDS = 4;
|
||||
|
||||
// Valores para las variables asociadas a los objetos
|
||||
constexpr int TIME_STOPPED_COUNTER = 300;
|
||||
|
||||
// Clase Game
|
||||
class Game {
|
||||
private:
|
||||
struct enemyInits_t {
|
||||
int x; // Posición en el eje X donde crear al enemigo
|
||||
int y; // Posición en el eje Y donde crear al enemigo
|
||||
float velX; // Velocidad inicial en el eje X
|
||||
Uint8 kind; // Tipo de enemigo
|
||||
Uint16 creationCounter; // Temporizador para la creación del enemigo
|
||||
};
|
||||
|
||||
struct enemyFormation_t // Contiene la información de una formación enemiga
|
||||
{
|
||||
Uint8 numberOfEnemies; // Cantidad de enemigos que forman la formación
|
||||
enemyInits_t init[MAX_NUMBER_OF_ENEMIES_IN_A_FORMATION]; // Vector con todas las inicializaciones de los enemigos de la formación
|
||||
};
|
||||
|
||||
struct enemyPool_t {
|
||||
enemyFormation_t *set[10]; // Conjunto de formaciones enemigas
|
||||
};
|
||||
|
||||
struct stage_t // Contiene todas las variables relacionadas con una fase
|
||||
{
|
||||
enemyPool_t *enemyPool; // El conjunto de formaciones enemigas de la fase
|
||||
Uint16 currentPower; // Cantidad actual de poder
|
||||
Uint16 powerToComplete; // Cantidad de poder que se necesita para completar la fase
|
||||
Uint8 maxMenace; // Umbral máximo de amenaza de la fase
|
||||
Uint8 minMenace; // Umbral mínimo de amenaza de la fase
|
||||
Uint8 number; // Numero de fase
|
||||
};
|
||||
|
||||
struct effect_t {
|
||||
bool flash; // Indica si se ha de pintar la pantalla de blanco
|
||||
bool shake; // Indica si se ha de agitar la pantalla
|
||||
Uint8 shakeCounter; // Contador para medir el tiempo que dura el efecto
|
||||
};
|
||||
|
||||
// Estado para el efecto de agitación intensa (muerte del jugador)
|
||||
struct deathShake_t {
|
||||
bool active; // Indica si el efecto está activo
|
||||
Uint8 step; // Paso actual del efecto (0-7)
|
||||
Uint32 lastStepTicks; // Ticks del último paso
|
||||
};
|
||||
|
||||
// Fases de la secuencia de muerte del jugador
|
||||
enum class DeathPhase { None,
|
||||
Shaking,
|
||||
Waiting,
|
||||
Done };
|
||||
|
||||
// Estado de la secuencia de muerte del jugador
|
||||
struct deathSequence_t {
|
||||
DeathPhase phase; // Fase actual
|
||||
Uint32 phaseStartTicks; // Ticks del inicio de la fase actual
|
||||
Player *player; // Jugador que está muriendo
|
||||
};
|
||||
|
||||
struct helper_t {
|
||||
bool needCoffee; // Indica si se necesitan cafes
|
||||
bool needCoffeeMachine; // Indica si se necesita PowerUp
|
||||
bool needPowerBall; // Indica si se necesita una PowerBall
|
||||
int counter; // Contador para no dar ayudas consecutivas
|
||||
int itemPoints1Odds; // Probabilidad de aparición del objeto
|
||||
int itemPoints2Odds; // Probabilidad de aparición del objeto
|
||||
int itemPoints3Odds; // Probabilidad de aparición del objeto
|
||||
int itemClockOdds; // Probabilidad de aparición del objeto
|
||||
int itemCoffeeOdds; // Probabilidad de aparición del objeto
|
||||
int itemCoffeeMachineOdds; // Probabilidad de aparición del objeto
|
||||
};
|
||||
|
||||
struct demo_t {
|
||||
bool enabled; // Indica si está activo el modo demo
|
||||
bool recording; // Indica si está activado el modo para grabar la demo
|
||||
Uint16 counter; // Contador para el modo demo
|
||||
demoKeys_t keys; // Variable con las pulsaciones de teclas del modo demo
|
||||
demoKeys_t dataFile[TOTAL_DEMO_DATA]; // Datos del fichero con los movimientos para la demo
|
||||
};
|
||||
|
||||
// Objetos y punteros
|
||||
SDL_Renderer *renderer; // El renderizador de la ventana
|
||||
Screen *screen; // Objeto encargado de dibujar en pantalla
|
||||
Asset *asset; // Objeto que gestiona todos los ficheros de recursos
|
||||
Lang *lang; // Objeto para gestionar los textos en diferentes idiomas
|
||||
Input *input; // Manejador de entrada
|
||||
section_t *section; // Seccion actual dentro del juego
|
||||
|
||||
std::vector<Player *> players; // Vector con los jugadores
|
||||
std::vector<Balloon *> balloons; // Vector con los globos
|
||||
std::vector<Bullet *> bullets; // Vector con las balas
|
||||
std::vector<Item *> items; // Vector con los items
|
||||
std::vector<SmartSprite *> smartSprites; // Vector con los smartsprites
|
||||
|
||||
Texture *bulletTexture; // Textura para las balas
|
||||
std::vector<Texture *> itemTextures; // Vector con las texturas de los items
|
||||
std::vector<Texture *> balloonTextures; // Vector con las texturas de los globos
|
||||
std::vector<Texture *> player1Textures; // Vector con las texturas del jugador
|
||||
std::vector<Texture *> player2Textures; // Vector con las texturas del jugador
|
||||
std::vector<std::vector<Texture *>> playerTextures; // Vector con todas las texturas de los jugadores;
|
||||
|
||||
Texture *gameBuildingsTexture; // Textura con los edificios de fondo
|
||||
Texture *gameCloudsTexture; // Textura con las nubes de fondo
|
||||
Texture *gameGrassTexture; // Textura con la hierba del suelo
|
||||
Texture *gamePowerMeterTexture; // Textura con el marcador de poder de la fase
|
||||
Texture *gameSkyColorsTexture; // Textura con los diferentes colores de fondo del juego
|
||||
Texture *gameTextTexture; // Textura para los sprites con textos
|
||||
Texture *gameOverTexture; // Textura para la pantalla de game over
|
||||
Texture *gameOverEndTexture; // Textura para la pantalla de game over de acabar el juego
|
||||
|
||||
std::vector<std::vector<std::string> *> itemAnimations; // Vector con las animaciones de los items
|
||||
std::vector<std::vector<std::string> *> playerAnimations; // Vector con las animaciones del jugador
|
||||
std::vector<std::vector<std::string> *> balloonAnimations; // Vector con las animaciones de los globos
|
||||
|
||||
Text *text; // Fuente para los textos del juego
|
||||
Text *textBig; // Fuente de texto grande
|
||||
Text *textScoreBoard; // Fuente para el marcador del juego
|
||||
Text *textNokia2; // Otra fuente de texto para mensajes
|
||||
Text *textNokiaBig2; // Y la versión en grande
|
||||
|
||||
Menu *gameOverMenu; // Menú de la pantalla de game over
|
||||
Menu *pauseMenu; // Menú de la pantalla de pausa
|
||||
|
||||
Fade *fade; // Objeto para renderizar fades
|
||||
SDL_Event *eventHandler; // Manejador de eventos
|
||||
|
||||
MovingSprite *clouds1A; // Sprite para las nubes superiores
|
||||
MovingSprite *clouds1B; // Sprite para las nubes superiores
|
||||
MovingSprite *clouds2A; // Sprite para las nubes inferiores
|
||||
MovingSprite *clouds2B; // Sprite para las nubes inferiores
|
||||
SmartSprite *n1000Sprite; // Sprite con el texto 1.000
|
||||
SmartSprite *n2500Sprite; // Sprite con el texto 2.500
|
||||
SmartSprite *n5000Sprite; // Sprite con el texto 5.000
|
||||
|
||||
Sprite *buildingsSprite; // Sprite con los edificios de fondo
|
||||
Sprite *skyColorsSprite; // Sprite con los graficos del degradado de color de fondo
|
||||
Sprite *grassSprite; // Sprite para la hierba
|
||||
Sprite *powerMeterSprite; // Sprite para el medidor de poder de la fase
|
||||
Sprite *gameOverSprite; // Sprite para dibujar los graficos del game over
|
||||
Sprite *gameOverEndSprite; // Sprite para dibujar los graficos del game over de acabar el juego
|
||||
|
||||
JA_Sound_t *balloonSound; // Sonido para la explosión del globo
|
||||
JA_Sound_t *bulletSound; // Sonido para los disparos
|
||||
JA_Sound_t *playerCollisionSound; // Sonido para la colisión del jugador con un enemigo
|
||||
JA_Sound_t *hiScoreSound; // Sonido para cuando se alcanza la máxima puntuación
|
||||
JA_Sound_t *itemDropSound; // Sonido para cuando se genera un item
|
||||
JA_Sound_t *itemPickUpSound; // Sonido para cuando se recoge un item
|
||||
JA_Sound_t *coffeeOutSound; // Sonido para cuando el jugador pierde el café al recibir un impacto
|
||||
JA_Sound_t *stageChangeSound; // Sonido para cuando se cambia de fase
|
||||
JA_Sound_t *bubble1Sound; // Sonido para cuando el jugador muere
|
||||
JA_Sound_t *bubble2Sound; // Sonido para cuando el jugador muere
|
||||
JA_Sound_t *bubble3Sound; // Sonido para cuando el jugador muere
|
||||
JA_Sound_t *bubble4Sound; // Sonido para cuando el jugador muere
|
||||
JA_Sound_t *clockSound; // Sonido para cuando se detiene el tiempo con el item reloj
|
||||
JA_Sound_t *powerBallSound; // Sonido para cuando se explota una Power Ball
|
||||
JA_Sound_t *coffeeMachineSound; // Sonido para cuando la máquina de café toca el suelo
|
||||
|
||||
JA_Music_t *gameMusic; // Musica de fondo
|
||||
|
||||
// Variables
|
||||
int numPlayers; // Numero de jugadores
|
||||
Uint32 ticks; // Contador de ticks para ajustar la velocidad del programa
|
||||
Uint8 ticksSpeed; // Velocidad a la que se repiten los bucles del programa
|
||||
Uint32 hiScore; // Puntuación máxima
|
||||
bool hiScoreAchieved; // Indica si se ha superado la puntuación máxima
|
||||
std::string hiScoreName; // Nombre del jugador que ostenta la máxima puntuación
|
||||
stage_t stage[10]; // Variable con los datos de cada pantalla
|
||||
Uint8 currentStage; // Indica la fase actual
|
||||
Uint8 stageBitmapCounter; // Contador para el tiempo visible del texto de Stage
|
||||
float stageBitmapPath[STAGE_COUNTER]; // Vector con los puntos Y por donde se desplaza el texto
|
||||
float getReadyBitmapPath[STAGE_COUNTER]; // Vector con los puntos X por donde se desplaza el texto
|
||||
Uint16 deathCounter; // Contador para la animación de muerte del jugador
|
||||
Uint8 menaceCurrent; // Nivel de amenaza actual
|
||||
Uint8 menaceThreshold; // Umbral del nivel de amenaza. Si el nivel de amenaza cae por debajo del umbral, se generan más globos. Si el umbral aumenta, aumenta el numero de globos
|
||||
bool timeStopped; // Indica si el tiempo está detenido
|
||||
Uint16 timeStoppedCounter; // Temporizador para llevar la cuenta del tiempo detenido
|
||||
Uint32 counter; // Contador para el juego
|
||||
Uint32 scoreDataFile[TOTAL_SCORE_DATA]; // Datos del fichero de puntos
|
||||
SDL_Rect skyColorsRect[4]; // Vector con las coordenadas de los 4 colores de cielo
|
||||
Uint16 balloonsPopped; // Lleva la cuenta de los globos explotados
|
||||
Uint8 lastEnemyDeploy; // Guarda cual ha sido la última formación desplegada para no repetir;
|
||||
int enemyDeployCounter; // Cuando se lanza una formación, se le da un valor y no sale otra hasta que llegue a cero
|
||||
float enemySpeed; // Velocidad a la que se mueven los enemigos
|
||||
float defaultEnemySpeed; // Velocidad base de los enemigos, sin incrementar
|
||||
effect_t effect; // Variable para gestionar los efectos visuales
|
||||
deathShake_t deathShake; // Variable para gestionar el efecto de agitación intensa
|
||||
deathSequence_t deathSequence; // Variable para gestionar la secuencia de muerte
|
||||
helper_t helper; // Variable para gestionar las ayudas
|
||||
bool powerBallEnabled; // Indica si hay una powerball ya activa
|
||||
Uint8 powerBallCounter; // Contador de formaciones enemigas entre la aparicion de una PowerBall y otra
|
||||
bool coffeeMachineEnabled; // Indica si hay una máquina de café en el terreno de juego
|
||||
bool gameCompleted; // Indica si se ha completado la partida, llegando al final de la ultima pantalla
|
||||
int gameCompletedCounter; // Contador para el tramo final, cuando se ha completado la partida y ya no aparecen más enemigos
|
||||
Uint8 difficulty; // Dificultad del juego
|
||||
float difficultyScoreMultiplier; // Multiplicador de puntos en función de la dificultad
|
||||
color_t difficultyColor; // Color asociado a la dificultad
|
||||
struct options_t *options; // Variable con todas las variables de las opciones del programa
|
||||
Uint8 onePlayerControl; // Variable para almacenar el valor de las opciones
|
||||
enemyFormation_t enemyFormation[NUMBER_OF_ENEMY_FORMATIONS]; // Vector con todas las formaciones enemigas
|
||||
enemyPool_t enemyPool[10]; // Variable con los diferentes conjuntos de formaciones enemigas
|
||||
Uint8 lastStageReached; // Contiene el numero de la última pantalla que se ha alcanzado
|
||||
demo_t demo; // Variable con todas las variables relacionadas con el modo demo
|
||||
int totalPowerToCompleteGame; // La suma del poder necesario para completar todas las fases
|
||||
int cloudsSpeed; // Velocidad a la que se desplazan las nubes
|
||||
int pauseCounter; // Contador para salir del menu de pausa y volver al juego
|
||||
bool leavingPauseMenu; // Indica si esta saliendo del menu de pausa para volver al juego
|
||||
bool pauseInitialized; // Indica si la pausa ha sido inicializada
|
||||
bool gameOverInitialized; // Indica si el game over ha sido inicializado
|
||||
int gameOverPostFade; // Opción a realizar cuando termina el fundido del game over
|
||||
#ifdef PAUSE
|
||||
bool pause;
|
||||
#endif
|
||||
|
||||
// Actualiza el juego
|
||||
void update();
|
||||
|
||||
// Dibuja el juego
|
||||
void render();
|
||||
|
||||
// Comprueba los eventos que hay en cola
|
||||
void checkEvents();
|
||||
|
||||
// Inicializa las variables necesarias para la sección 'Game'
|
||||
void init();
|
||||
|
||||
// Carga los recursos necesarios para la sección 'Game'
|
||||
void loadMedia();
|
||||
|
||||
// Carga el fichero de puntos
|
||||
bool loadScoreFile();
|
||||
|
||||
// Carga el fichero de datos para la demo
|
||||
bool loadDemoFile();
|
||||
|
||||
// Guarda el fichero de puntos
|
||||
bool saveScoreFile();
|
||||
|
||||
// Guarda el fichero de datos para la demo
|
||||
bool saveDemoFile();
|
||||
|
||||
// Inicializa las formaciones enemigas
|
||||
void initEnemyFormations();
|
||||
|
||||
// Inicializa los conjuntos de formaciones
|
||||
void initEnemyPools();
|
||||
|
||||
// Inicializa las fases del juego
|
||||
void initGameStages();
|
||||
|
||||
// Crea una formación de enemigos
|
||||
void deployEnemyFormation();
|
||||
|
||||
// Aumenta el poder de la fase
|
||||
void increaseStageCurrentPower(Uint8 power);
|
||||
|
||||
// Establece el valor de la variable
|
||||
void setHiScore(Uint32 score);
|
||||
|
||||
// Actualiza el valor de HiScore en caso necesario
|
||||
void updateHiScore();
|
||||
|
||||
// Transforma un valor numérico en una cadena de 6 cifras
|
||||
std::string updateScoreText(Uint32 num);
|
||||
|
||||
// Pinta el marcador en pantalla usando un objeto texto
|
||||
void renderScoreBoard();
|
||||
|
||||
// Actualiza las variables del jugador
|
||||
void updatePlayers();
|
||||
|
||||
// Dibuja a los jugadores
|
||||
void renderPlayers();
|
||||
|
||||
// Actualiza las variables de la fase
|
||||
void updateStage();
|
||||
|
||||
// Actualiza el estado de muerte
|
||||
void updateDeath();
|
||||
|
||||
// Renderiza el fade final cuando se acaba la partida
|
||||
void renderDeathFade(int counter);
|
||||
|
||||
// Actualiza los globos
|
||||
void updateBalloons();
|
||||
|
||||
// Pinta en pantalla todos los globos activos
|
||||
void renderBalloons();
|
||||
|
||||
// Crea un globo nuevo en el vector de globos
|
||||
Uint8 createBalloon(float x, int y, Uint8 kind, float velx, float speed, Uint16 stoppedcounter);
|
||||
|
||||
// Crea una PowerBall
|
||||
void createPowerBall();
|
||||
|
||||
// Establece la velocidad de los globos
|
||||
void setBalloonSpeed(float speed);
|
||||
|
||||
// Incrementa la velocidad de los globos
|
||||
void incBalloonSpeed();
|
||||
|
||||
// Decrementa la velocidad de los globos
|
||||
void decBalloonSpeed();
|
||||
|
||||
// Actualiza la velocidad de los globos en funcion del poder acumulado de la fase
|
||||
void updateBalloonSpeed();
|
||||
|
||||
// Explosiona un globo. Lo destruye y crea otros dos si es el caso
|
||||
void popBalloon(Balloon *balloon);
|
||||
|
||||
// Explosiona un globo. Lo destruye
|
||||
void destroyBalloon(Balloon *balloon);
|
||||
|
||||
// Explosiona todos los globos
|
||||
void popAllBalloons();
|
||||
|
||||
// Destruye todos los globos
|
||||
void destroyAllBalloons();
|
||||
|
||||
// Detiene todos los globos
|
||||
void stopAllBalloons(Uint16 time);
|
||||
|
||||
// Pone en marcha todos los globos
|
||||
void startAllBalloons();
|
||||
|
||||
// Obtiene el numero de globos activos
|
||||
Uint8 countBalloons();
|
||||
|
||||
// Vacia el vector de globos
|
||||
void freeBalloons();
|
||||
|
||||
// Comprueba la colisión entre el jugador y los globos activos
|
||||
bool checkPlayerBalloonCollision(Player *player);
|
||||
|
||||
// Comprueba la colisión entre el jugador y los items
|
||||
void checkPlayerItemCollision(Player *player);
|
||||
|
||||
// Comprueba la colisión entre las balas y los globos
|
||||
void checkBulletBalloonCollision();
|
||||
|
||||
// Mueve las balas activas
|
||||
void moveBullets();
|
||||
|
||||
// Pinta las balas activas
|
||||
void renderBullets();
|
||||
|
||||
// Crea un objeto bala
|
||||
void createBullet(int x, int y, Uint8 kind, bool poweredUp, int owner);
|
||||
|
||||
// Vacia el vector de balas
|
||||
void freeBullets();
|
||||
|
||||
// Actualiza los items
|
||||
void updateItems();
|
||||
|
||||
// Pinta los items activos
|
||||
void renderItems();
|
||||
|
||||
// Devuelve un item en función del azar
|
||||
Uint8 dropItem();
|
||||
|
||||
// Crea un objeto item
|
||||
void createItem(Uint8 kind, float x, float y);
|
||||
|
||||
// Vacia el vector de items
|
||||
void freeItems();
|
||||
|
||||
// Crea un objeto SmartSprite
|
||||
void createItemScoreSprite(int x, int y, SmartSprite *sprite);
|
||||
|
||||
// Vacia el vector de smartsprites
|
||||
void freeSmartSprites();
|
||||
|
||||
// Dibuja el efecto de flash
|
||||
void renderFlashEffect();
|
||||
|
||||
// Actualiza el efecto de agitar la pantalla
|
||||
void updateShakeEffect();
|
||||
|
||||
// Crea un SmartSprite para arrojar el item café al recibir un impacto
|
||||
void throwCoffee(int x, int y);
|
||||
|
||||
// Actualiza los SmartSprites
|
||||
void updateSmartSprites();
|
||||
|
||||
// Pinta los SmartSprites activos
|
||||
void renderSmartSprites();
|
||||
|
||||
// Acciones a realizar cuando el jugador muere
|
||||
void killPlayer(Player *player);
|
||||
|
||||
// Calcula y establece el valor de amenaza en funcion de los globos activos
|
||||
void evaluateAndSetMenace();
|
||||
|
||||
// Obtiene el valor de la variable
|
||||
Uint8 getMenace();
|
||||
|
||||
// Establece el valor de la variable
|
||||
void setTimeStopped(bool value);
|
||||
|
||||
// Obtiene el valor de la variable
|
||||
bool isTimeStopped();
|
||||
|
||||
// Establece el valor de la variable
|
||||
void setTimeStoppedCounter(Uint16 value);
|
||||
|
||||
// Incrementa el valor de la variable
|
||||
void incTimeStoppedCounter(Uint16 value);
|
||||
|
||||
// Actualiza la variable EnemyDeployCounter
|
||||
void updateEnemyDeployCounter();
|
||||
|
||||
// Actualiza y comprueba el valor de la variable
|
||||
void updateTimeStoppedCounter();
|
||||
|
||||
// Gestiona el nivel de amenaza
|
||||
void updateMenace();
|
||||
|
||||
// Actualiza el fondo
|
||||
void updateBackground();
|
||||
|
||||
// Dibuja el fondo
|
||||
void renderBackground();
|
||||
|
||||
// Gestiona la entrada durante el juego
|
||||
void checkGameInput();
|
||||
|
||||
// Pinta diferentes mensajes en la pantalla
|
||||
void renderMessages();
|
||||
|
||||
// Habilita el efecto del item de detener el tiempo
|
||||
void enableTimeStopItem();
|
||||
|
||||
// Deshabilita el efecto del item de detener el tiempo
|
||||
void disableTimeStopItem();
|
||||
|
||||
// Inicia el efecto de agitación intensa de la pantalla
|
||||
void shakeScreen();
|
||||
|
||||
// Actualiza el efecto de agitación intensa
|
||||
void updateDeathShake();
|
||||
|
||||
// Indica si el efecto de agitación intensa está activo
|
||||
bool isDeathShaking();
|
||||
|
||||
// Actualiza la secuencia de muerte del jugador
|
||||
void updateDeathSequence();
|
||||
|
||||
// Actualiza las variables del menu de pausa del juego
|
||||
void updatePausedGame();
|
||||
|
||||
// Dibuja el menu de pausa del juego
|
||||
void renderPausedGame();
|
||||
|
||||
// Inicializa el estado de pausa del juego
|
||||
void enterPausedGame();
|
||||
|
||||
// Actualiza los elementos de la pantalla de game over
|
||||
void updateGameOverScreen();
|
||||
|
||||
// Dibuja los elementos de la pantalla de game over
|
||||
void renderGameOverScreen();
|
||||
|
||||
// Inicializa el estado de game over
|
||||
void enterGameOverScreen();
|
||||
|
||||
// Comprueba los eventos de la pantalla de game over
|
||||
void checkGameOverEvents();
|
||||
|
||||
// Indica si se puede crear una powerball
|
||||
bool canPowerBallBeCreated();
|
||||
|
||||
// Calcula el poder actual de los globos en pantalla
|
||||
int calculateScreenPower();
|
||||
|
||||
// Inicializa las variables que contienen puntos de ruta para mover objetos
|
||||
void initPaths();
|
||||
|
||||
// Actualiza el tramo final de juego, una vez completado
|
||||
void updateGameCompleted();
|
||||
|
||||
// Actualiza las variables de ayuda
|
||||
void updateHelper();
|
||||
|
||||
// Comprueba si todos los jugadores han muerto
|
||||
bool allPlayersAreDead();
|
||||
|
||||
// Carga las animaciones
|
||||
void loadAnimations(std::string filePath, std::vector<std::string> *buffer);
|
||||
|
||||
// Elimina todos los objetos contenidos en vectores
|
||||
void deleteAllVectorObjects();
|
||||
|
||||
// Recarga las texturas
|
||||
void reloadTextures();
|
||||
|
||||
// Establece la máxima puntuación desde fichero o desde las puntuaciones online
|
||||
void setHiScore();
|
||||
|
||||
public:
|
||||
// Constructor
|
||||
Game(int numPlayers, int currentStage, SDL_Renderer *renderer, Screen *screen, Asset *asset, Lang *lang, Input *input, bool demo, options_t *options, section_t *section);
|
||||
|
||||
// Destructor
|
||||
~Game();
|
||||
|
||||
// Bucle para el juego
|
||||
void run();
|
||||
|
||||
// Ejecuta un frame del juego
|
||||
void iterate();
|
||||
|
||||
// Indica si el juego ha terminado
|
||||
bool hasFinished() const;
|
||||
|
||||
// Procesa un evento
|
||||
void handleEvent(SDL_Event *event);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user