Compare commits
99 Commits
v2.3.3
..
time-based
| Author | SHA1 | Date | |
|---|---|---|---|
| b558ea0b4c | |||
| 635662d65d | |||
| 2a69eaf041 | |||
| 4f7333ba46 | |||
| 54ef4c85eb | |||
| 36d50ade82 | |||
| 91c5b9d2b2 | |||
| 93af6dd58d | |||
| 13875e7b8c | |||
| eac2d42a1b | |||
| c920f99c82 | |||
| fe240c750e | |||
| 2b57bfa4dd | |||
| f1a6636222 | |||
| 081a7e02c7 | |||
| 77718d4515 | |||
| 3ac495f444 | |||
| a8c0386355 | |||
| ebfcad6f22 | |||
| a40931c7ca | |||
| 659e37e5a1 | |||
| 7006207b7e | |||
| 415ce17f3b | |||
| 6b0337b750 | |||
| 4c7f28d746 | |||
| e57944398a | |||
| e887b77dcb | |||
| 91add6f2fe | |||
| 169a5ea7aa | |||
| f10be8c277 | |||
| 7d8aac6121 | |||
| 76d0c72b85 | |||
| 6c6643b890 | |||
| 97977d19e8 | |||
| d9004caa2a | |||
| cc12ef6590 | |||
| 1e6cb3bb24 | |||
| 40e1140734 | |||
| d72630523a | |||
| 479d9d941a | |||
| 37cb3c782a | |||
| be95b8afab | |||
| 9f6d38cf48 | |||
| ee2dd0bc2c | |||
| 3421f34a84 | |||
| 18cd287808 | |||
| b1392d0c00 | |||
| be18f51735 | |||
| 48af959814 | |||
| 0bc55f5732 | |||
| 9a2da460cc | |||
| 0ee117135c | |||
| dc622c7bae | |||
| 1912200b21 | |||
| 88fa3f296f | |||
| ceb5324d23 | |||
| ce8eee07ff | |||
| 2282377ae7 | |||
| 118626dff6 | |||
| e2bc6aa5c0 | |||
| c86e020312 | |||
| 285b094dad | |||
| cf436f0014 | |||
| 7a09c0aa89 | |||
| 6f9bdcbeb6 | |||
| 6bdb5c207c | |||
| 6246b5d89d | |||
| 34a41ad25c | |||
| 20b9a95619 | |||
| 513eacf356 | |||
| 5889df2a47 | |||
| 7f703390f9 | |||
| 1bb0ebdef8 | |||
| 5fec0110b3 | |||
| 55caef3210 | |||
| 007c1d3554 | |||
| 28606a9fe1 | |||
| 294e665b11 | |||
| 0faa605ad9 | |||
| c3534ace9c | |||
| 517bc2caa1 | |||
| f9b0f64b81 | |||
| e0498d642d | |||
| ccdf9732d1 | |||
| 1451327fcc | |||
| a035fecb04 | |||
| 9d70138855 | |||
| dfe0a3d4e6 | |||
| 66c3e0089c | |||
| 86323a0e56 | |||
| 58cacf7bda | |||
| 978cbcc9fc | |||
| fb023df1e1 | |||
| 555f347375 | |||
| 85a47c1a2b | |||
| 06d4712493 | |||
| 18c4d6032d | |||
| 9365f80e8b | |||
| 4bd07216f3 |
+2
-2
@@ -2,8 +2,8 @@ BasedOnStyle: Google
|
|||||||
IndentWidth: 4
|
IndentWidth: 4
|
||||||
NamespaceIndentation: All
|
NamespaceIndentation: All
|
||||||
IndentAccessModifiers: false
|
IndentAccessModifiers: false
|
||||||
ColumnLimit: 0 # Sin limite de longitud de linea
|
ColumnLimit: 0 # Sin límite de longitud de línea
|
||||||
BreakBeforeBraces: Attach # Llaves en la misma linea
|
BreakBeforeBraces: Attach # Llaves en la misma línea
|
||||||
AllowShortIfStatementsOnASingleLine: true
|
AllowShortIfStatementsOnASingleLine: true
|
||||||
AllowShortBlocksOnASingleLine: true
|
AllowShortBlocksOnASingleLine: true
|
||||||
AllowShortFunctionsOnASingleLine: All
|
AllowShortFunctionsOnASingleLine: All
|
||||||
|
|||||||
+76
-53
@@ -2,83 +2,106 @@ Checks:
|
|||||||
- readability-*
|
- readability-*
|
||||||
- modernize-*
|
- modernize-*
|
||||||
- performance-*
|
- performance-*
|
||||||
- bugprone-unchecked-optional-access
|
- bugprone-*
|
||||||
- bugprone-sizeof-expression
|
|
||||||
- bugprone-suspicious-missing-comma
|
|
||||||
- bugprone-suspicious-index
|
|
||||||
- bugprone-undefined-memory-manipulation
|
|
||||||
- bugprone-use-after-move
|
|
||||||
- bugprone-out-of-bound-access
|
|
||||||
- -readability-identifier-length
|
- -readability-identifier-length
|
||||||
- -readability-magic-numbers
|
- -readability-magic-numbers
|
||||||
- -bugprone-narrowing-conversions
|
|
||||||
- -performance-enum-size
|
|
||||||
- -performance-inefficient-string-concatenation
|
|
||||||
- -bugprone-integer-division
|
- -bugprone-integer-division
|
||||||
- -bugprone-easily-swappable-parameters
|
- -bugprone-easily-swappable-parameters
|
||||||
- -modernize-avoid-c-arrays,-warnings-as-errors
|
- -bugprone-narrowing-conversions
|
||||||
|
- -modernize-avoid-c-arrays
|
||||||
|
|
||||||
WarningsAsErrors: '*'
|
WarningsAsErrors: '*'
|
||||||
# Excluye jail_audio.hpp, stb_image.h y stb_vorbis.c del analisis
|
# Headers nostres (excloem source/external/ que conté dependències de tercers no editables)
|
||||||
HeaderFilterRegex: 'source/(?!jail_audio\.hpp|stb_image\.h|stb_vorbis\.c).*'
|
HeaderFilterRegex: 'source/(core|game|utils)/'
|
||||||
FormatStyle: file
|
FormatStyle: file
|
||||||
|
|
||||||
CheckOptions:
|
CheckOptions:
|
||||||
# Variables locales en snake_case
|
# bugprone-empty-catch: aceptar catches vacíos marcados con @INTENTIONAL en un comentario
|
||||||
|
- { key: bugprone-empty-catch.IgnoreCatchWithKeywords, value: '@INTENTIONAL' }
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# CONSTANTES → UPPER_CASE (compile-time y runtime, en cualquier scope)
|
||||||
|
# =====================================================================
|
||||||
|
# Todo lo que sea const o constexpr se identifica visualmente en UPPER_CASE,
|
||||||
|
# sin importar si es global, local, miembro o static.
|
||||||
|
|
||||||
|
# constexpr en cualquier scope (globales y locales)
|
||||||
|
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Constantes globales (const no-constexpr)
|
||||||
|
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Constantes locales (const en función)
|
||||||
|
- { key: readability-identifier-naming.LocalConstantCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Static const a nivel de archivo/namespace
|
||||||
|
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Miembros static const/constexpr de clase (p.ej. static constexpr int MAX = 100;)
|
||||||
|
- { key: readability-identifier-naming.ClassConstantCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Miembros const no-static de clase (p.ej. const int limit;)
|
||||||
|
- { key: readability-identifier-naming.ConstantMemberCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# Valores de enums
|
||||||
|
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
|
||||||
|
|
||||||
|
# NOTA: Los parámetros const NO se tratan como constantes aquí.
|
||||||
|
# Un parámetro sigue siendo un parámetro aunque sea const → hereda ParameterCase.
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# VARIABLES NO-CONST
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
# Variables locales
|
||||||
- { key: readability-identifier-naming.VariableCase, value: lower_case }
|
- { key: readability-identifier-naming.VariableCase, value: lower_case }
|
||||||
|
- { key: readability-identifier-naming.LocalVariableCase, value: lower_case }
|
||||||
|
|
||||||
# Miembros privados en snake_case con sufijo _
|
# Parámetros de función
|
||||||
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
|
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
|
||||||
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
|
|
||||||
|
|
||||||
# Miembros protegidos en snake_case con sufijo _
|
# Variables estáticas no-const (static locales, static file-scope,
|
||||||
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
|
# y static members no-const de clase como el instance_ de un Singleton).
|
||||||
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
|
# Sufijo _ para marcar que tienen storage estático.
|
||||||
|
|
||||||
# Miembros publicos en snake_case (sin sufijo)
|
|
||||||
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
|
|
||||||
|
|
||||||
# Namespaces en CamelCase
|
|
||||||
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
|
|
||||||
|
|
||||||
# Variables estaticas privadas como miembros privados
|
|
||||||
- { key: readability-identifier-naming.StaticVariableCase, value: lower_case }
|
- { key: readability-identifier-naming.StaticVariableCase, value: lower_case }
|
||||||
- { key: readability-identifier-naming.StaticVariableSuffix, value: _ }
|
- { key: readability-identifier-naming.StaticVariableSuffix, value: _ }
|
||||||
|
|
||||||
# Constantes estaticas sin sufijo
|
# =====================================================================
|
||||||
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }
|
# MIEMBROS DE CLASE NO-CONST
|
||||||
|
# =====================================================================
|
||||||
|
# Privados: snake_case con sufijo _
|
||||||
|
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
|
||||||
|
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
|
||||||
|
|
||||||
# Constantes globales en UPPER_CASE
|
# Protegidos: snake_case con sufijo _
|
||||||
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
|
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
|
||||||
|
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
|
||||||
|
|
||||||
# Variables constexpr globales en UPPER_CASE
|
# Públicos: snake_case sin sufijo
|
||||||
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE }
|
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
|
||||||
|
|
||||||
# Constantes locales en UPPER_CASE
|
# =====================================================================
|
||||||
- { key: readability-identifier-naming.LocalConstantCase, value: UPPER_CASE }
|
# TIPOS
|
||||||
|
# =====================================================================
|
||||||
# Constexpr miembros en UPPER_CASE (sin sufijo)
|
|
||||||
- { key: readability-identifier-naming.ConstexprMemberCase, value: UPPER_CASE }
|
|
||||||
|
|
||||||
# Constexpr miembros privados/protegidos con sufijo _
|
|
||||||
- { key: readability-identifier-naming.ConstexprMethodCase, value: UPPER_CASE }
|
|
||||||
|
|
||||||
# Clases, structs y enums en CamelCase
|
|
||||||
- { key: readability-identifier-naming.ClassCase, value: CamelCase }
|
- { key: readability-identifier-naming.ClassCase, value: CamelCase }
|
||||||
- { key: readability-identifier-naming.StructCase, value: CamelCase }
|
- { key: readability-identifier-naming.StructCase, value: CamelCase }
|
||||||
- { key: readability-identifier-naming.EnumCase, value: CamelCase }
|
- { key: readability-identifier-naming.EnumCase, value: CamelCase }
|
||||||
|
- { key: readability-identifier-naming.UnionCase, value: CamelCase }
|
||||||
|
- { key: readability-identifier-naming.TypeAliasCase, value: CamelCase }
|
||||||
|
- { key: readability-identifier-naming.TypedefCase, value: CamelCase }
|
||||||
|
- { key: readability-identifier-naming.TemplateParameterCase, value: CamelCase }
|
||||||
|
|
||||||
# Valores de enums en UPPER_CASE
|
# Namespaces
|
||||||
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
|
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
|
||||||
|
|
||||||
# Metodos en camelBack (sin sufijos)
|
# =====================================================================
|
||||||
|
# FUNCIONES Y MÉTODOS (incluyendo constexpr)
|
||||||
|
# =====================================================================
|
||||||
|
# Un método/función constexpr es un invocable, no una constante → camelBack.
|
||||||
|
- { key: readability-identifier-naming.FunctionCase, value: camelBack }
|
||||||
|
- { key: readability-identifier-naming.ConstexprFunctionCase, value: camelBack }
|
||||||
- { key: readability-identifier-naming.MethodCase, value: camelBack }
|
- { key: readability-identifier-naming.MethodCase, value: camelBack }
|
||||||
- { key: readability-identifier-naming.PrivateMethodCase, value: camelBack }
|
- { key: readability-identifier-naming.PrivateMethodCase, value: camelBack }
|
||||||
- { key: readability-identifier-naming.ProtectedMethodCase, value: camelBack }
|
- { key: readability-identifier-naming.ProtectedMethodCase, value: camelBack }
|
||||||
- { key: readability-identifier-naming.PublicMethodCase, value: camelBack }
|
- { key: readability-identifier-naming.PublicMethodCase, value: camelBack }
|
||||||
|
- { key: readability-identifier-naming.ConstexprMethodCase, value: camelBack }
|
||||||
# Funciones en camelBack
|
|
||||||
- { key: readability-identifier-naming.FunctionCase, value: camelBack }
|
|
||||||
|
|
||||||
# Parametros en lower_case
|
|
||||||
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
|
|
||||||
|
|||||||
Executable
+92
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Pre-commit hook: aplica clang-format als fitxers C++ staged abans del commit.
|
||||||
|
# - Només toca fitxers staged dins source/ (exclou source/external/).
|
||||||
|
# - Avorta el commit si hi ha canvis NO staged en aquests fitxers (per no incloure'ls sense voler).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if ! command -v clang-format >/dev/null 2>&1; then
|
||||||
|
echo "pre-commit: clang-format no trobat — saltant format check" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t STAGED < <(git diff --cached --name-only --diff-filter=ACMR \
|
||||||
|
| grep -E '^source/.*\.(cpp|hpp|h)$' \
|
||||||
|
| grep -vE '^source/external/' || true)
|
||||||
|
|
||||||
|
if [ ${#STAGED[@]} -eq 0 ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
UNSTAGED_DIRTY=()
|
||||||
|
for f in "${STAGED[@]}"; do
|
||||||
|
if ! git diff --quiet -- "$f"; then
|
||||||
|
UNSTAGED_DIRTY+=("$f")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#UNSTAGED_DIRTY[@]} -gt 0 ]; then
|
||||||
|
echo "pre-commit: aquests fitxers tenen canvis NO staged i estan al commit." >&2
|
||||||
|
echo " Fes 'git add' o 'git stash' abans de continuar:" >&2
|
||||||
|
printf ' %s\n' "${UNSTAGED_DIRTY[@]}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
clang-format -i "${STAGED[@]}"
|
||||||
|
git add -- "${STAGED[@]}"
|
||||||
|
|
||||||
|
# --- clang-tidy només sobre els fitxers staged ---
|
||||||
|
if ! command -v clang-tidy >/dev/null 2>&1; then
|
||||||
|
echo "pre-commit: clang-tidy no trobat — saltant tidy" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||||
|
BUILD_DIR="$REPO_ROOT/build"
|
||||||
|
|
||||||
|
if [ ! -f "$BUILD_DIR/compile_commands.json" ]; then
|
||||||
|
echo "pre-commit: generant compile_commands.json (build dir buit)..." >&2
|
||||||
|
cmake -S "$REPO_ROOT" -B "$BUILD_DIR" >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "pre-commit: clang-tidy sobre ${#STAGED[@]} fitxer(s)..." >&2
|
||||||
|
if ! clang-tidy -p "$BUILD_DIR" --quiet "${STAGED[@]}"; then
|
||||||
|
echo "pre-commit: clang-tidy ha trobat errors — commit avortat" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- cppcheck només sobre els .cpp staged ---
|
||||||
|
if ! command -v cppcheck >/dev/null 2>&1; then
|
||||||
|
echo "pre-commit: cppcheck no trobat — saltant cppcheck" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
CPP_STAGED=()
|
||||||
|
for f in "${STAGED[@]}"; do
|
||||||
|
[[ "$f" == *.cpp ]] && CPP_STAGED+=("$f")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#CPP_STAGED[@]} -eq 0 ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "pre-commit: cppcheck sobre ${#CPP_STAGED[@]} fitxer(s)..." >&2
|
||||||
|
if ! cppcheck \
|
||||||
|
--enable=warning,style,performance,portability \
|
||||||
|
--std=c++20 \
|
||||||
|
--language=c++ \
|
||||||
|
--inline-suppr \
|
||||||
|
--suppress=missingIncludeSystem \
|
||||||
|
--suppress=toomanyconfigs \
|
||||||
|
--suppress='*:*source/external/*' \
|
||||||
|
--suppress='*:*source/core/rendering/sdl3gpu/spv/*' \
|
||||||
|
--suppress=normalCheckLevelMaxBranches \
|
||||||
|
-D_DEBUG \
|
||||||
|
-DLINUX_BUILD \
|
||||||
|
--quiet \
|
||||||
|
--error-exitcode=1 \
|
||||||
|
-I "$REPO_ROOT/source" \
|
||||||
|
"${CPP_STAGED[@]}"; then
|
||||||
|
echo "pre-commit: cppcheck ha trobat errors — commit avortat" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
+4
-1
@@ -1,7 +1,7 @@
|
|||||||
.vscode
|
.vscode
|
||||||
build/
|
build/
|
||||||
|
compile_commands.json
|
||||||
dist/
|
dist/
|
||||||
data/config/config.txt
|
|
||||||
*.DS_Store
|
*.DS_Store
|
||||||
thumbs.db
|
thumbs.db
|
||||||
*.exe
|
*.exe
|
||||||
@@ -14,3 +14,6 @@ thumbs.db
|
|||||||
coffee_crisis
|
coffee_crisis
|
||||||
coffee_crisis_debug
|
coffee_crisis_debug
|
||||||
release/windows/coffee.res
|
release/windows/coffee.res
|
||||||
|
resources.pack
|
||||||
|
tools/pack_resources/pack_resources
|
||||||
|
.cache/
|
||||||
@@ -4,67 +4,90 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
Coffee Crisis is a C++20 arcade game built with SDL2. The player controls a character defending the UPV (university) from bouncing coffee-ball enemies across 10 stages. Supports 1-2 players, keyboard and gamepad input, and multiple languages (Spanish, Basque, English).
|
Coffee Crisis is a C++20 arcade game built with SDL3. The player controls a character defending the UPV (university) from bouncing coffee-ball enemies across 10 stages. Supports 1-2 players, keyboard and gamepad input, and multiple languages (Spanish, Basque, English).
|
||||||
|
|
||||||
## Build Commands
|
## Build Commands
|
||||||
|
|
||||||
Dependencies: `libsdl2-dev` and `g++` (Linux) or `clang++` (macOS).
|
Dependencies: `libsdl3-dev` and `g++` (Linux) or `clang++` (macOS). Build system is CMake (driven by `Makefile` wrappers).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Linux
|
make # Release build
|
||||||
make linux # Release build → ./coffee_crisis
|
make debug # Debug build (defines DEBUG)
|
||||||
make linux_debug # Debug build (defines DEBUG and PAUSE) → ./coffee_crisis_debug
|
make release # Empaqueta .tar.gz / .dmg / .zip segons SO
|
||||||
|
make pack # Regenera resources.pack
|
||||||
# macOS
|
make compile_shaders # Compila shaders GLSL → headers SPIR-V (requereix glslc)
|
||||||
make macos # Release build with clang++
|
make controllerdb # Descarga gamecontrollerdb.txt
|
||||||
make macos_debug # Debug build
|
make format # clang-format -i
|
||||||
|
make tidy # clang-tidy
|
||||||
# Windows (MinGW)
|
make cppcheck # cppcheck
|
||||||
make windows # Release build → coffee_crisis.exe
|
|
||||||
make windows_debug # Debug build
|
|
||||||
|
|
||||||
# Release packaging
|
|
||||||
make linux_release # Builds and creates .tar.gz
|
|
||||||
make macos_release # Builds Intel + Apple Silicon .dmg files
|
|
||||||
make windows_release # Builds and creates .zip
|
|
||||||
```
|
```
|
||||||
|
|
||||||
There is also a CMakeLists.txt available as an alternative build system.
|
|
||||||
|
|
||||||
There are no tests or linter configured.
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
All source code is in `source/`. The game uses a section-based architecture controlled by the **Director** class:
|
Source layout (alineat amb la resta de projectes germans):
|
||||||
|
|
||||||
- **Director** (`director.h/cpp`): Top-level controller. Initializes SDL, manages the window/renderer, and runs sections in sequence: Logo → Intro → Title → Game → Quit. Owns all shared objects (Screen, Input, Lang, Asset).
|
```
|
||||||
- **Game** (`game.h/cpp`): Core gameplay logic. Manages players, balloons (enemies), bullets, items, stages, menace level, and collision detection. Contains its own update/render loop plus sub-loops for pause and game over screens.
|
source/
|
||||||
- **Screen** (`screen.h/cpp`): Rendering abstraction. Manages a virtual canvas (256×192) that gets scaled to the actual window. Handles fullscreen/windowed modes, border rendering, and fade effects.
|
├── main.cpp
|
||||||
- **Input** (`input.h/cpp`): Abstracts keyboard and gamepad input.
|
├── core/
|
||||||
- **Asset** (`asset.h/cpp`): Resource file index. Files are registered with `add()` and retrieved by name with `get()`. All paths are relative to the executable.
|
│ ├── audio/ jail_audio.hpp
|
||||||
- **Lang** (`lang.h/cpp`): i18n system loading text strings from files in `data/lang/`.
|
│ ├── input/ input.*, mouse.*
|
||||||
|
│ ├── locale/ lang.*
|
||||||
|
│ ├── rendering/ screen, fade, text, writer, texture, sprite + animated/moving/smart
|
||||||
|
│ │ ├── shader_backend.hpp (interfície abstracta de post-procesado)
|
||||||
|
│ │ └── sdl3gpu/ (pipeline SDL3 GPU)
|
||||||
|
│ │ ├── sdl3gpu_shader.* (implementació del backend GPU)
|
||||||
|
│ │ └── spv/ (headers SPIR-V generats — protegits amb dummies `.clang-*`)
|
||||||
|
│ ├── resources/ asset, resource, resource_pack, resource_loader, resource_helper
|
||||||
|
│ └── system/ director
|
||||||
|
├── game/
|
||||||
|
│ ├── defaults.hpp (constants de gameplay: block size, canvas, áreas, colors)
|
||||||
|
│ ├── game.* (hub de gameplay)
|
||||||
|
│ ├── entities/ player, balloon, bullet, item
|
||||||
|
│ ├── scenes/ logo, intro, title, instructions
|
||||||
|
│ └── ui/ menu
|
||||||
|
├── utils/
|
||||||
|
│ ├── defines.hpp (macros de build)
|
||||||
|
│ └── utils.* (helpers, enum de dificultat, circle_t, ...)
|
||||||
|
└── external/ (stb_image, stb_vorbis — protegits amb dummies `.clang-*`)
|
||||||
|
```
|
||||||
|
|
||||||
### Sprite hierarchy
|
Flux general controlat per la classe **Director** (`core/system/director.h`): inicialitza SDL, finestra/renderer i executa seccions en seqüència **Logo → Intro → Title → Game → Quit**. Les classes principals:
|
||||||
|
|
||||||
- **Sprite** → base class for drawing from a PNG spritesheet
|
- **Game** (`game/game.h`): gameplay nuclear. Gestiona jugadors, balloons, bullets, items, stages, nivell d'amenaça i col·lisions. Té el seu bucle d'update/render i sub-bucles per pausa i game-over.
|
||||||
- **AnimatedSprite** → extends Sprite with frame-based animation (loaded from `.ani` files)
|
- **Screen** (`core/rendering/screen.h`): abstracció de render. Canvas virtual 256×192 escalat a la finestra. Fullscreen/windowed, borders, fades.
|
||||||
- **MovingSprite** → sprite with movement
|
- **Input** (`core/input/input.h`): abstracció de teclat i gamepad.
|
||||||
- **SmartSprite** → sprite with autonomous behavior (score popups, thrown items)
|
- **Asset** (`core/resources/asset.h`): índex de fitxers de recurs (`add`/`get` per nom).
|
||||||
|
- **Lang** (`core/locale/lang.h`): i18n, carrega strings des de `data/lang/`.
|
||||||
|
|
||||||
### Game entities
|
### Sprite hierarchy (`core/rendering/`)
|
||||||
|
|
||||||
- **Player** (`player.h/cpp`): Player character state and rendering
|
- **Sprite** → base per dibuixar des d'un spritesheet PNG
|
||||||
- **Balloon** (`balloon.h/cpp`): Enemy entities with multiple types and split-on-pop behavior
|
- **AnimatedSprite** → afegeix animació per frames (arxius `.ani`)
|
||||||
- **Bullet** (`bullet.h/cpp`): Projectiles fired by the player (left/center/right)
|
- **MovingSprite** → sprite amb posició/velocitat
|
||||||
- **Item** (`item.h/cpp`): Collectible items (points, clock, coffee, power-ups)
|
- **SmartSprite** → sprite autònom (score popups, objectes llençats)
|
||||||
|
|
||||||
### Audio
|
### Audio
|
||||||
|
|
||||||
**jail_audio** (`jail_audio.h/cpp`): Custom audio library wrapping SDL2 audio. Uses stb_vorbis for OGG decoding. Provides `JA_*` functions for music and sound effects with channel-based mixing.
|
**jail_audio** (`core/audio/jail_audio.hpp`): wrapper audio SDL3 first-party. Usa stb_vorbis per OGG. API `JA_*` per música i efectes amb mesclat per canals.
|
||||||
|
|
||||||
### Key constants
|
### GPU / shaders (post-procesado)
|
||||||
|
|
||||||
Defined in `const.h`: block size (8px), virtual canvas (256×192), play area bounds, section/subsection IDs, and color definitions.
|
Pipeline SDL3 GPU portat de `coffee_crisis_arcade_edition`. El canvas 256×192 es pot passar per un backend GPU que aplica PostFX (vinyeta, scanlines, chroma, gamma, mask, curvatura, bleeding, flicker) o CrtPi (scanlines continues amb bloom). Fallback transparent al `SDL_Renderer` clàssic si la GPU falla o si es desactiva.
|
||||||
|
|
||||||
|
- **Interfície**: `core/rendering/shader_backend.hpp` (`Rendering::ShaderBackend`).
|
||||||
|
- **Implementació**: `core/rendering/sdl3gpu/sdl3gpu_shader.*` + shaders GLSL a `data/shaders/` compilats a `spv/*_spv.h` via `glslc` (o precompilats si no hi ha `glslc`).
|
||||||
|
- **Emscripten**: compile-time `NO_SHADERS` → sempre ruta clàssica.
|
||||||
|
- **macOS**: shaders Metal (MSL) inline dins `sdl3gpu_shader.cpp`; no SPIR-V.
|
||||||
|
- **Opcions persistents** a `config.txt` (migració a YAML pendent):
|
||||||
|
- `videoGpuAcceleration` (bool)
|
||||||
|
- `videoGpuPreferredDriver` (string, buit = auto)
|
||||||
|
- `videoShaderEnabled` (bool)
|
||||||
|
- `videoShaderType` (0=POSTFX, 1=CRTPI)
|
||||||
|
- **Hotkeys** (provisionals fins que hi hagi menú d'opcions): `F4` activa/desactiva post-procesado · `F5` alterna POSTFX ↔ CRTPI (només si està actiu) · `F6` següent preset (només si està actiu). No hi ha tecla per a preset anterior.
|
||||||
|
- **API** a `Screen`: `setGpuAcceleration`/`toggleGpuAcceleration`/`isGpuAccelerated`, `setShaderEnabled`/`toggleShaderEnabled`/`isShaderEnabled`, `setActiveShader`/`toggleActiveShader`/`getActiveShader`.
|
||||||
|
|
||||||
|
Presets PostFX/CrtPi i cicle de presets encara **no** estan implementats — arribaran amb la migració a YAML. Per ara, valors per defecte hardcoded.
|
||||||
|
|
||||||
## Data Directory
|
## Data Directory
|
||||||
|
|
||||||
@@ -72,8 +95,9 @@ Defined in `const.h`: block size (8px), virtual canvas (256×192), play area bou
|
|||||||
- `data/font/` — bitmap font files
|
- `data/font/` — bitmap font files
|
||||||
- `data/music/` and `data/sound/` — audio assets
|
- `data/music/` and `data/sound/` — audio assets
|
||||||
- `data/lang/` — language files (es_ES, ba_BA, en_UK)
|
- `data/lang/` — language files (es_ES, ba_BA, en_UK)
|
||||||
- `data/config/` — gamecontroller DB, demo recording data
|
- `data/demo/` — demo recording data (gamecontrollerdb.txt vive en la raíz del proyecto, fuera del pack)
|
||||||
- `data/menu/` — menu definition files
|
- `data/menu/` — menu definition files
|
||||||
|
- `data/shaders/` — fonts GLSL per al post-procesado SDL3 GPU (no van al pack; s'empotren al binari via headers SPIR-V)
|
||||||
|
|
||||||
## Language
|
## Language
|
||||||
|
|
||||||
|
|||||||
+184
-15
@@ -3,6 +3,11 @@
|
|||||||
cmake_minimum_required(VERSION 3.10)
|
cmake_minimum_required(VERSION 3.10)
|
||||||
project(coffee_crisis VERSION 1.00)
|
project(coffee_crisis VERSION 1.00)
|
||||||
|
|
||||||
|
# Tipus de build per defecte (Debug) si no se n'ha especificat cap
|
||||||
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Configuración de compilador para MinGW en Windows
|
# Configuración de compilador para MinGW en Windows
|
||||||
if(WIN32 AND NOT CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
|
if(WIN32 AND NOT CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
|
||||||
set(CMAKE_CXX_COMPILER "g++")
|
set(CMAKE_CXX_COMPILER "g++")
|
||||||
@@ -21,8 +26,15 @@ set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Os -ffunction-sections
|
|||||||
# Define el directorio de los archivos fuente
|
# Define el directorio de los archivos fuente
|
||||||
set(DIR_SOURCES "${CMAKE_SOURCE_DIR}/source")
|
set(DIR_SOURCES "${CMAKE_SOURCE_DIR}/source")
|
||||||
|
|
||||||
# Cargar todos los archivos fuente en DIR_SOURCES
|
# Cargar todos los archivos fuente en DIR_SOURCES (recursivo, sin external/)
|
||||||
file(GLOB SOURCES "${DIR_SOURCES}/*.cpp")
|
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "${DIR_SOURCES}/*.cpp")
|
||||||
|
list(FILTER SOURCES EXCLUDE REGEX "${DIR_SOURCES}/external/.*")
|
||||||
|
|
||||||
|
# En Emscripten no compilamos sdl3gpu_shader (SDL3 GPU no está soportado en WebGL2).
|
||||||
|
# Define NO_SHADERS más abajo y filtra el fuente aquí.
|
||||||
|
if(EMSCRIPTEN)
|
||||||
|
list(REMOVE_ITEM SOURCES "${DIR_SOURCES}/core/rendering/sdl3gpu/sdl3gpu_shader.cpp")
|
||||||
|
endif()
|
||||||
|
|
||||||
# Verificar si se encontraron archivos fuente
|
# Verificar si se encontraron archivos fuente
|
||||||
if(NOT SOURCES)
|
if(NOT SOURCES)
|
||||||
@@ -30,18 +42,90 @@ if(NOT SOURCES)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Configuración de SDL3
|
# Configuración de SDL3
|
||||||
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
|
if(EMSCRIPTEN)
|
||||||
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
|
# En Emscripten, SDL3 se compila desde source con FetchContent
|
||||||
|
include(FetchContent)
|
||||||
|
FetchContent_Declare(
|
||||||
|
SDL3
|
||||||
|
GIT_REPOSITORY https://github.com/libsdl-org/SDL.git
|
||||||
|
GIT_TAG release-3.4.4
|
||||||
|
GIT_SHALLOW TRUE
|
||||||
|
)
|
||||||
|
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_STATIC ON CACHE BOOL "" FORCE)
|
||||||
|
set(SDL_TEST_LIBRARY OFF CACHE BOOL "" FORCE)
|
||||||
|
FetchContent_MakeAvailable(SDL3)
|
||||||
|
message(STATUS "SDL3 compilado desde source para Emscripten")
|
||||||
|
else()
|
||||||
|
find_package(SDL3 REQUIRED CONFIG REQUIRED COMPONENTS SDL3)
|
||||||
|
message(STATUS "SDL3 encontrado: ${SDL3_INCLUDE_DIRS}")
|
||||||
|
endif()
|
||||||
|
|
||||||
# Configuración común de salida de ejecutables en el directorio raíz
|
# --- SHADER COMPILATION (Linux/Windows only - macOS uses Metal, Emscripten no soporta SDL3 GPU) ---
|
||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
|
if(NOT APPLE AND NOT EMSCRIPTEN)
|
||||||
|
find_program(GLSLC_EXE NAMES glslc)
|
||||||
|
|
||||||
|
set(SHADERS_DIR "${CMAKE_SOURCE_DIR}/data/shaders")
|
||||||
|
set(HEADERS_DIR "${CMAKE_SOURCE_DIR}/source/core/rendering/sdl3gpu/spv")
|
||||||
|
|
||||||
|
set(ALL_SHADER_HEADERS
|
||||||
|
"${HEADERS_DIR}/postfx_vert_spv.h"
|
||||||
|
"${HEADERS_DIR}/postfx_frag_spv.h"
|
||||||
|
"${HEADERS_DIR}/crtpi_frag_spv.h"
|
||||||
|
)
|
||||||
|
set(ALL_SHADER_SOURCES
|
||||||
|
"${SHADERS_DIR}/postfx.vert"
|
||||||
|
"${SHADERS_DIR}/postfx.frag"
|
||||||
|
"${SHADERS_DIR}/crtpi_frag.glsl"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(GLSLC_EXE)
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${ALL_SHADER_HEADERS}
|
||||||
|
COMMAND ${CMAKE_COMMAND}
|
||||||
|
-D GLSLC=${GLSLC_EXE}
|
||||||
|
-D SHADERS_DIR=${SHADERS_DIR}
|
||||||
|
-D HEADERS_DIR=${HEADERS_DIR}
|
||||||
|
-P ${CMAKE_SOURCE_DIR}/tools/shaders/compile_spirv.cmake
|
||||||
|
DEPENDS ${ALL_SHADER_SOURCES}
|
||||||
|
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
|
||||||
|
COMMENT "Compilando shaders SPIR-V..."
|
||||||
|
)
|
||||||
|
add_custom_target(shaders DEPENDS ${ALL_SHADER_HEADERS})
|
||||||
|
message(STATUS "glslc encontrado: shaders se compilarán automáticamente")
|
||||||
|
else()
|
||||||
|
foreach(HDR ${ALL_SHADER_HEADERS})
|
||||||
|
if(NOT EXISTS "${HDR}")
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"glslc no encontrado y header SPIR-V no existe: ${HDR}\n"
|
||||||
|
" Instala glslc: sudo apt install glslang-tools (Linux)\n"
|
||||||
|
" choco install vulkan-sdk (Windows)"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
message(STATUS "glslc no encontrado - usando headers SPIR-V precompilados")
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
if(EMSCRIPTEN)
|
||||||
|
message(STATUS "Emscripten: shaders SPIR-V omitidos (SDL3 GPU no soportado en WebGL2)")
|
||||||
|
else()
|
||||||
|
message(STATUS "macOS: shaders SPIR-V omitidos (usa Metal inline)")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
# Añadir ejecutable principal
|
# Añadir ejecutable principal
|
||||||
add_executable(${PROJECT_NAME} ${SOURCES})
|
add_executable(${PROJECT_NAME} ${SOURCES})
|
||||||
|
|
||||||
|
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"`)
|
||||||
|
target_include_directories(${PROJECT_NAME} PRIVATE ${DIR_SOURCES})
|
||||||
|
|
||||||
# Añadir definiciones de compilación dependiendo del tipo de build
|
# Añadir definiciones de compilación dependiendo del tipo de build
|
||||||
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
||||||
$<$<CONFIG:DEBUG>:DEBUG PAUSE>
|
$<$<CONFIG:DEBUG>:DEBUG>
|
||||||
$<$<CONFIG:RELEASE>:RELEASE_BUILD>
|
$<$<CONFIG:RELEASE>:RELEASE_BUILD>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,6 +150,19 @@ elseif(APPLE)
|
|||||||
-rpath @executable_path/../Frameworks/
|
-rpath @executable_path/../Frameworks/
|
||||||
)
|
)
|
||||||
endif()
|
endif()
|
||||||
|
elseif(EMSCRIPTEN)
|
||||||
|
target_compile_definitions(${PROJECT_NAME} PRIVATE EMSCRIPTEN_BUILD NO_SHADERS)
|
||||||
|
# En wasm NO empaquetamos un resources.pack: el propio --preload-file de
|
||||||
|
# emscripten ya hace el mismo trabajo (bundle del directorio en un .data),
|
||||||
|
# así que metemos directamente 'data' y dejamos que el Resource lea por
|
||||||
|
# filesystem (MEMFS). Evita doble empaquetado y el uso de memoria extra.
|
||||||
|
target_link_options(${PROJECT_NAME} PRIVATE
|
||||||
|
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/data@/data"
|
||||||
|
"SHELL:--preload-file ${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt@/gamecontrollerdb.txt"
|
||||||
|
-sALLOW_MEMORY_GROWTH=1
|
||||||
|
-sMAX_WEBGL_VERSION=2
|
||||||
|
)
|
||||||
|
set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".html")
|
||||||
elseif(UNIX AND NOT APPLE)
|
elseif(UNIX AND NOT APPLE)
|
||||||
target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD)
|
target_compile_definitions(${PROJECT_NAME} PRIVATE LINUX_BUILD)
|
||||||
target_link_options(${PROJECT_NAME} PRIVATE -Wl,--gc-sections)
|
target_link_options(${PROJECT_NAME} PRIVATE -Wl,--gc-sections)
|
||||||
@@ -77,6 +174,7 @@ endif()
|
|||||||
|
|
||||||
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
|
find_program(CLANG_TIDY_EXE NAMES clang-tidy)
|
||||||
find_program(CLANG_FORMAT_EXE NAMES clang-format)
|
find_program(CLANG_FORMAT_EXE NAMES clang-format)
|
||||||
|
find_program(CPPCHECK_EXE NAMES cppcheck)
|
||||||
|
|
||||||
# Recopilar todos los archivos fuente para analisis
|
# Recopilar todos los archivos fuente para analisis
|
||||||
file(GLOB_RECURSE ALL_SOURCE_FILES
|
file(GLOB_RECURSE ALL_SOURCE_FILES
|
||||||
@@ -84,17 +182,14 @@ file(GLOB_RECURSE ALL_SOURCE_FILES
|
|||||||
"${CMAKE_SOURCE_DIR}/source/*.h"
|
"${CMAKE_SOURCE_DIR}/source/*.h"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Excluir stb_image.h y stb_vorbis.c del analisis
|
|
||||||
set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES})
|
set(CLANG_TIDY_SOURCES ${ALL_SOURCE_FILES})
|
||||||
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*stb_image\\.h$")
|
|
||||||
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*stb_vorbis\\.c$")
|
|
||||||
list(FILTER CLANG_TIDY_SOURCES EXCLUDE REGEX ".*jail_audio\\.hpp$")
|
|
||||||
|
|
||||||
# Excluir stb y jail_audio del formateo tambien
|
|
||||||
set(FORMAT_SOURCES ${ALL_SOURCE_FILES})
|
set(FORMAT_SOURCES ${ALL_SOURCE_FILES})
|
||||||
list(FILTER FORMAT_SOURCES EXCLUDE REGEX ".*stb_image\\.h$")
|
|
||||||
list(FILTER FORMAT_SOURCES EXCLUDE REGEX ".*stb_vorbis\\.c$")
|
# Para cppcheck, pasar solo .cpp (los headers se procesan transitivamente).
|
||||||
list(FILTER FORMAT_SOURCES EXCLUDE REGEX ".*jail_audio\\.hpp$")
|
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
|
# Targets de clang-tidy
|
||||||
if(CLANG_TIDY_EXE)
|
if(CLANG_TIDY_EXE)
|
||||||
@@ -139,3 +234,77 @@ if(CLANG_FORMAT_EXE)
|
|||||||
else()
|
else()
|
||||||
message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles")
|
message(STATUS "clang-format no encontrado - targets 'format' y 'format-check' no disponibles")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# Target de cppcheck
|
||||||
|
if(CPPCHECK_EXE)
|
||||||
|
add_custom_target(cppcheck
|
||||||
|
COMMAND ${CPPCHECK_EXE}
|
||||||
|
--enable=warning,style,performance,portability
|
||||||
|
--std=c++20
|
||||||
|
--language=c++
|
||||||
|
--inline-suppr
|
||||||
|
--suppress=missingIncludeSystem
|
||||||
|
--suppress=toomanyconfigs
|
||||||
|
--suppress=*:*/source/external/*
|
||||||
|
--suppress=*:*/source/core/rendering/sdl3gpu/spv/*
|
||||||
|
--quiet
|
||||||
|
-I ${CMAKE_SOURCE_DIR}/source
|
||||||
|
${CPPCHECK_SOURCES}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
COMMENT "Running cppcheck..."
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(STATUS "cppcheck no encontrado - target 'cppcheck' no disponible")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# --- EINA STANDALONE: pack_resources ---
|
||||||
|
# Executable auxiliar que empaqueta `data/` a `resources.pack`.
|
||||||
|
# No es compila per defecte (EXCLUDE_FROM_ALL). Build explícit:
|
||||||
|
# cmake --build build --target pack_resources
|
||||||
|
# Després executar: ./build/pack_resources data resources.pack
|
||||||
|
if(NOT EMSCRIPTEN)
|
||||||
|
add_executable(pack_resources EXCLUDE_FROM_ALL
|
||||||
|
tools/pack_resources/pack_resources.cpp
|
||||||
|
source/core/resources/resource_pack.cpp
|
||||||
|
)
|
||||||
|
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
|
||||||
|
target_compile_options(pack_resources PRIVATE -Wall)
|
||||||
|
|
||||||
|
# Regeneració automàtica de resources.pack en cada build si canvia data/.
|
||||||
|
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
|
||||||
|
set(RESOURCE_PACK "${CMAKE_BINARY_DIR}/resources.pack")
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${RESOURCE_PACK}
|
||||||
|
COMMAND $<TARGET_FILE:pack_resources>
|
||||||
|
"${CMAKE_SOURCE_DIR}/data"
|
||||||
|
"${RESOURCE_PACK}"
|
||||||
|
DEPENDS pack_resources ${DATA_FILES}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
COMMENT "Empaquetant data/ → resources.pack"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
|
||||||
|
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
|
||||||
|
add_dependencies(${PROJECT_NAME} resource_pack)
|
||||||
|
|
||||||
|
# --- CÒPIA DE gamecontrollerdb.txt AL COSTAT DEL BINARI ---
|
||||||
|
# SDL_AddGamepadMappingsFromFile només llegeix del filesystem real (no del
|
||||||
|
# pack), així que el fitxer ha de viure al directori del binari. Es copia
|
||||||
|
# només si existeix per no fallar la build d'algú que encara no ha fet
|
||||||
|
# `make controllerdb`.
|
||||||
|
if(EXISTS "${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt")
|
||||||
|
set(CONTROLLER_DB "${CMAKE_BINARY_DIR}/gamecontrollerdb.txt")
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${CONTROLLER_DB}
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt"
|
||||||
|
"${CONTROLLER_DB}"
|
||||||
|
DEPENDS "${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt"
|
||||||
|
COMMENT "Copiant gamecontrollerdb.txt → build/"
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
add_custom_target(controller_db ALL DEPENDS ${CONTROLLER_DB})
|
||||||
|
add_dependencies(${PROJECT_NAME} controller_db)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|||||||
@@ -2,20 +2,42 @@
|
|||||||
# DIRECTORIES
|
# DIRECTORIES
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
|
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
|
||||||
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
|
BUILDDIR := build
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# TARGET NAMES
|
# TARGET NAMES
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
TARGET_NAME := coffee_crisis
|
TARGET_NAME := coffee_crisis
|
||||||
TARGET_FILE := $(DIR_BIN)$(TARGET_NAME)
|
TARGET_FILE := $(BUILDDIR)/$(TARGET_NAME)
|
||||||
APP_NAME := Coffee Crisis
|
APP_NAME := Coffee Crisis
|
||||||
VERSION := v2.3.3
|
|
||||||
DIST_DIR := dist
|
DIST_DIR := dist
|
||||||
RELEASE_FOLDER := dist/_tmp
|
RELEASE_FOLDER := dist/_tmp
|
||||||
RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
|
RELEASE_FILE := $(RELEASE_FOLDER)/$(TARGET_NAME)
|
||||||
RESOURCE_FILE := release/windows/coffee.res
|
RESOURCE_FILE := release/windows/coffee.res
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# VERSION (extracted from defines.hpp)
|
||||||
|
# ==============================================================================
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
VERSION := $(shell powershell -Command "(Select-String -Path 'source/utils/defines.hpp' -Pattern 'constexpr const char\* VERSION = \"(.+?)\"').Matches.Groups[1].Value")
|
||||||
|
else
|
||||||
|
VERSION := $(shell grep 'constexpr const char\* VERSION' source/utils/defines.hpp | sed -E 's/.*VERSION = "([^"]+)".*/\1/')
|
||||||
|
endif
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# GIT HASH (computat al host, passat a CMake via -DGIT_HASH)
|
||||||
|
# Evita que CMake haja de cridar git des de Docker/emscripten on falla per
|
||||||
|
# "dubious ownership" del volum muntat.
|
||||||
|
# ==============================================================================
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>NUL)
|
||||||
|
else
|
||||||
|
GIT_HASH := $(shell git rev-parse --short=7 HEAD 2>/dev/null)
|
||||||
|
endif
|
||||||
|
ifeq ($(GIT_HASH),)
|
||||||
|
GIT_HASH := unknown
|
||||||
|
endif
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# RELEASE NAMES
|
# RELEASE NAMES
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -50,47 +72,99 @@ endif
|
|||||||
# WINDOWS-SPECIFIC VARIABLES
|
# WINDOWS-SPECIFIC VARIABLES
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
WIN_TARGET_FILE := $(DIR_BIN)$(APP_NAME)
|
WIN_TARGET_FILE := $(BUILDDIR)/$(APP_NAME)
|
||||||
WIN_RELEASE_FILE := $(RELEASE_FOLDER)/$(APP_NAME)
|
WIN_RELEASE_FILE := $(RELEASE_FOLDER)/$(APP_NAME)
|
||||||
|
# Escapa apòstrofs per a PowerShell (duplica ' → ''). Sense això, APP_NAMEs
|
||||||
|
# com "JailDoctor's Dilemma" trencarien el parsing de -Destination '...'.
|
||||||
|
WIN_RELEASE_FILE_PS := $(subst ','',$(WIN_RELEASE_FILE))
|
||||||
else
|
else
|
||||||
WIN_TARGET_FILE := $(TARGET_FILE)
|
WIN_TARGET_FILE := $(TARGET_FILE)
|
||||||
WIN_RELEASE_FILE := $(RELEASE_FILE)
|
WIN_RELEASE_FILE := $(RELEASE_FILE)
|
||||||
|
WIN_RELEASE_FILE_PS := $(WIN_RELEASE_FILE)
|
||||||
|
endif
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# CMAKE GENERATOR (usa Ninja si está disponible; si no, MinGW Makefiles en
|
||||||
|
# Windows / generador por defecto en Linux/macOS). Ninja paraleliza mejor.
|
||||||
|
# ==============================================================================
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
# Dins MSYS2/Git Bash/MinGW, $(shell ...) usa sh.exe i "NUL" NO és
|
||||||
|
# dispositiu — un redirect "2>NUL" crearia un fitxer literal anomenat
|
||||||
|
# NUL al cwd. Detectem MSYSTEM per usar /dev/null en aquests entorns.
|
||||||
|
ifneq ($(MSYSTEM),)
|
||||||
|
NULDEV := /dev/null
|
||||||
|
else
|
||||||
|
NULDEV := NUL
|
||||||
|
endif
|
||||||
|
HAS_NINJA := $(shell ninja --version 2>$(NULDEV))
|
||||||
|
ifneq ($(HAS_NINJA),)
|
||||||
|
CMAKE_GEN := -G "Ninja"
|
||||||
|
else
|
||||||
|
CMAKE_GEN := -G "MinGW Makefiles"
|
||||||
|
endif
|
||||||
|
else
|
||||||
|
HAS_NINJA := $(shell ninja --version 2>/dev/null)
|
||||||
|
ifneq ($(HAS_NINJA),)
|
||||||
|
CMAKE_GEN := -G "Ninja"
|
||||||
|
else
|
||||||
|
CMAKE_GEN :=
|
||||||
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# COMPILACIÓN CON CMAKE
|
# COMPILACIÓN CON CMAKE
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
all:
|
all:
|
||||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
@cmake --build build
|
@cmake --build build
|
||||||
|
|
||||||
debug:
|
debug:
|
||||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH)
|
||||||
@cmake --build build
|
@cmake --build build
|
||||||
|
|
||||||
|
run: all
|
||||||
|
@./$(TARGET_FILE)
|
||||||
|
|
||||||
|
run-debug: debug
|
||||||
|
@./$(TARGET_FILE)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@rm -rf $(BUILDDIR)
|
||||||
|
|
||||||
|
rebuild: clean all
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# EMPAQUETADO DE RECURSOS (build previ de l'eina + execució)
|
||||||
|
# ==============================================================================
|
||||||
|
pack:
|
||||||
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
|
@cmake --build build --target pack_resources
|
||||||
|
@./build/pack_resources data build/resources.pack
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# RELEASE AUTOMÁTICO (detecta SO)
|
# RELEASE AUTOMÁTICO (detecta SO)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
release:
|
release:
|
||||||
ifeq ($(OS),Windows_NT)
|
ifeq ($(OS),Windows_NT)
|
||||||
@"$(MAKE)" windows_release
|
@"$(MAKE)" _windows-release
|
||||||
else
|
else
|
||||||
ifeq ($(UNAME_S),Darwin)
|
ifeq ($(UNAME_S),Darwin)
|
||||||
@$(MAKE) macos_release
|
@$(MAKE) _macos-release
|
||||||
else
|
else
|
||||||
@$(MAKE) linux_release
|
@$(MAKE) _linux-release
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# COMPILACIÓN PARA WINDOWS (RELEASE)
|
# COMPILACIÓN PARA WINDOWS (RELEASE)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
windows_release:
|
_windows-release:
|
||||||
|
@$(MAKE) pack
|
||||||
@echo off
|
@echo off
|
||||||
@echo Creando release para Windows - Version: $(VERSION)
|
@echo Creando release para Windows - Version: $(VERSION)
|
||||||
|
|
||||||
# Compila con cmake
|
# Compila con cmake
|
||||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
@cmake --build build
|
@cmake --build build
|
||||||
|
|
||||||
# Crea carpeta de distribución y carpeta temporal
|
# Crea carpeta de distribución y carpeta temporal
|
||||||
@@ -99,11 +173,12 @@ windows_release:
|
|||||||
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
|
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
|
||||||
|
|
||||||
# Copia ficheros
|
# Copia ficheros
|
||||||
@powershell -Command "Copy-Item -Path 'data' -Destination '$(RELEASE_FOLDER)' -Recurse -Force"
|
@powershell -Command "Copy-Item 'build/resources.pack' -Destination '$(RELEASE_FOLDER)'"
|
||||||
|
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
|
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
|
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
|
@powershell -Command "Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "Copy-Item -Path '$(TARGET_FILE)' -Destination '\"$(WIN_RELEASE_FILE).exe\"'"
|
@powershell -Command "Copy-Item -Path '$(TARGET_FILE).exe' -Destination '$(WIN_RELEASE_FILE_PS).exe'"
|
||||||
strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
|
strip -s -R .comment -R .gnu.version "$(WIN_RELEASE_FILE).exe" --strip-unneeded
|
||||||
|
|
||||||
# Crea el fichero .zip
|
# Crea el fichero .zip
|
||||||
@@ -117,15 +192,32 @@ windows_release:
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# COMPILACIÓN PARA MACOS (RELEASE)
|
# COMPILACIÓN PARA MACOS (RELEASE)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
macos_release:
|
_macos-release:
|
||||||
|
@$(MAKE) pack
|
||||||
@echo "Creando release para macOS - Version: $(VERSION)"
|
@echo "Creando release para macOS - Version: $(VERSION)"
|
||||||
|
|
||||||
# Verificar e instalar create-dmg si es necesario
|
# Verifica dependencias necesarias (create-dmg). Si falta, intenta instalarla
|
||||||
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg)
|
# con brew; si brew tampoco está, indica el comando exacto al usuario.
|
||||||
|
@command -v create-dmg >/dev/null 2>&1 || { \
|
||||||
# Compila la versión para procesadores Intel con cmake
|
echo ""; \
|
||||||
@cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON
|
echo "============================================"; \
|
||||||
@cmake --build build/intel
|
echo " Falta la dependencia: create-dmg"; \
|
||||||
|
echo "============================================"; \
|
||||||
|
if command -v brew >/dev/null 2>&1; then \
|
||||||
|
echo " Instalando con: brew install create-dmg"; \
|
||||||
|
brew install create-dmg || { \
|
||||||
|
echo ""; \
|
||||||
|
echo " ERROR: 'brew install create-dmg' ha fallado."; \
|
||||||
|
echo " Ejecuta el comando manualmente y vuelve a probar."; \
|
||||||
|
exit 1; \
|
||||||
|
}; \
|
||||||
|
else \
|
||||||
|
echo " Homebrew no está instalado."; \
|
||||||
|
echo " Instálalo desde https://brew.sh y luego ejecuta:"; \
|
||||||
|
echo " brew install create-dmg"; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
}
|
||||||
|
|
||||||
# Elimina datos de compilaciones anteriores
|
# Elimina datos de compilaciones anteriores
|
||||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||||
@@ -140,7 +232,8 @@ macos_release:
|
|||||||
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
|
|
||||||
# Copia carpetas y ficheros
|
# Copia carpetas y ficheros
|
||||||
cp -R data "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
cp build/resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
|
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
|
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
|
||||||
cp release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
cp release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
|
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
|
||||||
@@ -153,31 +246,50 @@ macos_release:
|
|||||||
sed -i '' '/<key>CFBundleShortVersionString<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"; \
|
sed -i '' '/<key>CFBundleShortVersionString<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"; \
|
||||||
sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
|
sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
|
||||||
|
|
||||||
# Copia el ejecutable Intel al bundle
|
# Compila y empaqueta la versión Intel (best-effort: si falla, se omite el
|
||||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
# DMG Intel y continúa con la build de Apple Silicon).
|
||||||
|
@echo ""
|
||||||
# Firma la aplicación
|
@echo "============================================"
|
||||||
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
|
@echo " Compilando version Intel (x86_64)"
|
||||||
|
@echo "============================================"
|
||||||
# Empaqueta el .dmg de la versión Intel con create-dmg
|
@if cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release \
|
||||||
@echo "Creando DMG Intel..."
|
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
|
||||||
create-dmg \
|
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \
|
||||||
--volname "$(APP_NAME)" \
|
-DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH) \
|
||||||
--window-pos 200 120 \
|
&& cmake --build build/intel; then \
|
||||||
--window-size 720 300 \
|
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"; \
|
||||||
--icon-size 96 \
|
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"; \
|
||||||
--text-size 12 \
|
echo "Creando DMG Intel..."; \
|
||||||
--icon "$(APP_NAME).app" 278 102 \
|
create-dmg \
|
||||||
--icon "LICENSE" 441 102 \
|
--volname "$(APP_NAME)" \
|
||||||
--icon "README.md" 604 102 \
|
--window-pos 200 120 \
|
||||||
--app-drop-link 115 102 \
|
--window-size 720 300 \
|
||||||
--hide-extension "$(APP_NAME).app" \
|
--icon-size 96 \
|
||||||
"$(MACOS_INTEL_RELEASE)" \
|
--text-size 12 \
|
||||||
"$(RELEASE_FOLDER)" || true
|
--icon "$(APP_NAME).app" 278 102 \
|
||||||
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"
|
--icon "LICENSE" 441 102 \
|
||||||
|
--icon "README.md" 604 102 \
|
||||||
|
--app-drop-link 115 102 \
|
||||||
|
--hide-extension "$(APP_NAME).app" \
|
||||||
|
"$(MACOS_INTEL_RELEASE)" \
|
||||||
|
"$(RELEASE_FOLDER)" || true; \
|
||||||
|
echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"; \
|
||||||
|
else \
|
||||||
|
echo ""; \
|
||||||
|
echo "============================================"; \
|
||||||
|
echo " WARNING: la build Intel ha fallado."; \
|
||||||
|
echo " Se omite el DMG Intel y se continúa con"; \
|
||||||
|
echo " la build de Apple Silicon."; \
|
||||||
|
echo "============================================"; \
|
||||||
|
echo ""; \
|
||||||
|
fi
|
||||||
|
|
||||||
# Compila la versión para procesadores Apple Silicon con cmake
|
# Compila la versión para procesadores Apple Silicon con cmake
|
||||||
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON
|
@echo ""
|
||||||
|
@echo "============================================"
|
||||||
|
@echo " Compilando version Apple Silicon (arm64)"
|
||||||
|
@echo "============================================"
|
||||||
|
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH)
|
||||||
@cmake --build build/arm
|
@cmake --build build/arm
|
||||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
||||||
|
|
||||||
@@ -210,11 +322,12 @@ macos_release:
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# COMPILACIÓN PARA LINUX (RELEASE)
|
# COMPILACIÓN PARA LINUX (RELEASE)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
linux_release:
|
_linux-release:
|
||||||
|
@$(MAKE) pack
|
||||||
@echo "Creando release para Linux - Version: $(VERSION)"
|
@echo "Creando release para Linux - Version: $(VERSION)"
|
||||||
|
|
||||||
# Compila con cmake
|
# Compila con cmake
|
||||||
@cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
@cmake --build build
|
@cmake --build build
|
||||||
|
|
||||||
# Elimina carpeta temporal previa y la recrea
|
# Elimina carpeta temporal previa y la recrea
|
||||||
@@ -222,7 +335,8 @@ linux_release:
|
|||||||
$(MKDIR) "$(RELEASE_FOLDER)"
|
$(MKDIR) "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
# Copia ficheros
|
# Copia ficheros
|
||||||
cp -R data "$(RELEASE_FOLDER)"
|
cp build/resources.pack "$(RELEASE_FOLDER)"
|
||||||
|
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
|
||||||
cp LICENSE "$(RELEASE_FOLDER)"
|
cp LICENSE "$(RELEASE_FOLDER)"
|
||||||
cp README.md "$(RELEASE_FOLDER)"
|
cp README.md "$(RELEASE_FOLDER)"
|
||||||
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
|
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
|
||||||
@@ -236,10 +350,94 @@ linux_release:
|
|||||||
# Elimina la carpeta temporal
|
# Elimina la carpeta temporal
|
||||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# COMPILACIÓN PARA WEBASSEMBLY (requiere Docker)
|
||||||
|
# ==============================================================================
|
||||||
|
wasm:
|
||||||
|
@$(MAKE) pack
|
||||||
|
@echo "Compilando para WebAssembly - Version: $(VERSION)"
|
||||||
|
docker run --rm \
|
||||||
|
--user $(shell id -u):$(shell id -g) \
|
||||||
|
-v $(DIR_ROOT):/src \
|
||||||
|
-w /src \
|
||||||
|
emscripten/emsdk:latest \
|
||||||
|
bash -c "emcmake cmake -S . -B build/wasm -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm"
|
||||||
|
$(MKDIR) "$(DIST_DIR)/wasm"
|
||||||
|
cp build/wasm/$(TARGET_NAME).html $(DIST_DIR)/wasm/
|
||||||
|
cp build/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/
|
||||||
|
cp build/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/
|
||||||
|
cp build/wasm/$(TARGET_NAME).data $(DIST_DIR)/wasm/
|
||||||
|
@echo "Output: $(DIST_DIR)/wasm/"
|
||||||
|
scp $(DIST_DIR)/wasm/$(TARGET_NAME).js $(DIST_DIR)/wasm/$(TARGET_NAME).wasm $(DIST_DIR)/wasm/$(TARGET_NAME).data \
|
||||||
|
maverick:/home/sergio/gitea/web_jailgames/static/games/coffee-crisis/wasm/
|
||||||
|
ssh maverick 'cd /home/sergio/gitea/web_jailgames && ./deploy.sh'
|
||||||
|
@echo "Deployed to maverick"
|
||||||
|
|
||||||
|
# Versió Debug del build wasm: build local sense deploy. Sortida a dist/wasm-debug/.
|
||||||
|
wasm-debug:
|
||||||
|
@$(MAKE) pack
|
||||||
|
@echo "Compilando WebAssembly Debug - Version: $(VERSION)"
|
||||||
|
docker run --rm \
|
||||||
|
--user $(shell id -u):$(shell id -g) \
|
||||||
|
-v $(DIR_ROOT):/src \
|
||||||
|
-w /src \
|
||||||
|
emscripten/emsdk:latest \
|
||||||
|
bash -c "emcmake cmake -S . -B build/wasm-debug -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH) && cmake --build build/wasm-debug"
|
||||||
|
$(MKDIR) "$(DIST_DIR)/wasm-debug"
|
||||||
|
cp build/wasm-debug/$(TARGET_NAME).html $(DIST_DIR)/wasm-debug/
|
||||||
|
cp build/wasm-debug/$(TARGET_NAME).js $(DIST_DIR)/wasm-debug/
|
||||||
|
cp build/wasm-debug/$(TARGET_NAME).wasm $(DIST_DIR)/wasm-debug/
|
||||||
|
cp build/wasm-debug/$(TARGET_NAME).data $(DIST_DIR)/wasm-debug/
|
||||||
|
@echo "Output: $(DIST_DIR)/wasm-debug/"
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# ==============================================================================
|
||||||
|
# CODE QUALITY (delegados a cmake)
|
||||||
|
# ==============================================================================
|
||||||
|
format:
|
||||||
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
|
@cmake --build build --target format
|
||||||
|
|
||||||
|
format-check:
|
||||||
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
|
@cmake --build build --target format-check
|
||||||
|
|
||||||
|
tidy:
|
||||||
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
|
@cmake --build build --target tidy
|
||||||
|
|
||||||
|
tidy-fix:
|
||||||
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
|
@cmake --build build --target tidy-fix
|
||||||
|
|
||||||
|
cppcheck:
|
||||||
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
|
@cmake --build build --target cppcheck
|
||||||
|
|
||||||
|
# SHADERS (SPIR-V) — sólo Linux/Windows. Requiere glslc en el PATH.
|
||||||
|
compile-shaders:
|
||||||
|
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||||
|
@cmake --build build --target shaders
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# GIT HOOKS
|
||||||
|
# ==============================================================================
|
||||||
|
hooks-install:
|
||||||
|
@git config core.hooksPath .githooks
|
||||||
|
@echo "Git hooks activats: $(shell pwd)/.githooks"
|
||||||
|
|
||||||
|
# DESCARGA DE GAMECONTROLLERDB
|
||||||
|
# ==============================================================================
|
||||||
|
controllerdb:
|
||||||
|
@echo "Descargando gamecontrollerdb.txt..."
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt \
|
||||||
|
-o gamecontrollerdb.txt
|
||||||
|
@echo "gamecontrollerdb.txt actualizado"
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# REGLAS ESPECIALES
|
# REGLAS ESPECIALES
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
show_version:
|
show-version:
|
||||||
@echo "Version actual: $(VERSION)"
|
@echo "Version actual: $(VERSION)"
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@@ -250,14 +448,32 @@ help:
|
|||||||
@echo " make - Compilar con cmake (Release)"
|
@echo " make - Compilar con cmake (Release)"
|
||||||
@echo " make debug - Compilar con cmake (Debug)"
|
@echo " make debug - Compilar con cmake (Debug)"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
@echo " Ejecucion:"
|
||||||
|
@echo " make run - Compilar (Release) y ejecutar"
|
||||||
|
@echo " make run-debug - Compilar (Debug) y ejecutar"
|
||||||
|
@echo ""
|
||||||
@echo " Release:"
|
@echo " Release:"
|
||||||
@echo " make release - Crear release (detecta SO automaticamente)"
|
@echo " make release - Crear release (detecta SO automaticamente)"
|
||||||
@echo " make windows_release - Crear release para Windows"
|
@echo " make wasm - Compilar para WebAssembly (requiere Docker) y deploy a maverick"
|
||||||
@echo " make linux_release - Crear release para Linux"
|
@echo " make wasm-debug - Compilar WebAssembly Debug local (sin deploy)"
|
||||||
@echo " make macos_release - Crear release para macOS"
|
@echo ""
|
||||||
|
@echo " Herramientas:"
|
||||||
|
@echo " make pack - Empaquetar recursos a $(BUILDDIR)/resources.pack"
|
||||||
|
@echo " make compile-shaders - Compilar shaders GLSL a headers SPIR-V (requiere glslc)"
|
||||||
|
@echo " make controllerdb - Descargar gamecontrollerdb.txt actualizado"
|
||||||
|
@echo ""
|
||||||
|
@echo " Calidad de codigo:"
|
||||||
|
@echo " make format - Formatear codigo con clang-format"
|
||||||
|
@echo " make format-check - Verificar formato sin modificar"
|
||||||
|
@echo " make tidy - Analisis estatico con clang-tidy"
|
||||||
|
@echo " make tidy-fix - Analisis estatico con auto-fix"
|
||||||
|
@echo " make cppcheck - Analisis estatico con cppcheck"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " Otros:"
|
@echo " Otros:"
|
||||||
@echo " make show_version - Mostrar version actual ($(VERSION))"
|
@echo " make clean - Borrar carpeta $(BUILDDIR)/"
|
||||||
|
@echo " make rebuild - clean + all"
|
||||||
|
@echo " make show-version - Mostrar version actual ($(VERSION))"
|
||||||
|
@echo " make hooks-install - Activar git hooks del proyecto"
|
||||||
@echo " make help - Mostrar esta ayuda"
|
@echo " make help - Mostrar esta ayuda"
|
||||||
|
|
||||||
.PHONY: all debug release windows_release macos_release linux_release show_version help
|
.PHONY: all debug run run-debug clean rebuild release _windows-release _macos-release _linux-release wasm wasm-debug controllerdb pack format format-check tidy tidy-fix cppcheck compile-shaders hooks-install show-version help
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+26
-2
@@ -140,7 +140,7 @@ CONTINUAR?
|
|||||||
CONTINUAR
|
CONTINUAR
|
||||||
|
|
||||||
## 47 - MENU DE PAUSA
|
## 47 - MENU DE PAUSA
|
||||||
EIXIR DEL JOC
|
TORNAR AL TITOL
|
||||||
|
|
||||||
## 48 - MENU GAME OVER
|
## 48 - MENU GAME OVER
|
||||||
SI
|
SI
|
||||||
@@ -278,4 +278,28 @@ DEIXA BUIT PER A
|
|||||||
MODE FORA DE LINEA
|
MODE FORA DE LINEA
|
||||||
|
|
||||||
## 93 - MENU OPCIONES
|
## 93 - MENU OPCIONES
|
||||||
TAULER DE PUNTS
|
TAULER DE PUNTS
|
||||||
|
|
||||||
|
## 94 - NOTIFICACIO COMANDAMENT
|
||||||
|
CONNECTAT
|
||||||
|
|
||||||
|
## 95 - NOTIFICACIO COMANDAMENT
|
||||||
|
DESCONNECTAT
|
||||||
|
|
||||||
|
## 96 - NOTIFICACIO HOTKEY
|
||||||
|
Zoom
|
||||||
|
|
||||||
|
## 97 - NOTIFICACIO HOTKEY
|
||||||
|
Pantalla completa
|
||||||
|
|
||||||
|
## 98 - NOTIFICACIO HOTKEY
|
||||||
|
Finestra
|
||||||
|
|
||||||
|
## 99 - NOTIFICACIO HOTKEY
|
||||||
|
Shader
|
||||||
|
|
||||||
|
## 100 - NOTIFICACIO HOTKEY
|
||||||
|
Preset
|
||||||
|
|
||||||
|
## 101 - NOTIFICACIO HOTKEY
|
||||||
|
Torna a premer ESC per a eixir
|
||||||
+26
-2
@@ -140,7 +140,7 @@ CONTINUE?
|
|||||||
CONTINUE
|
CONTINUE
|
||||||
|
|
||||||
## 47 - MENU DE PAUSA
|
## 47 - MENU DE PAUSA
|
||||||
LEAVE GAME
|
BACK TO TITLE
|
||||||
|
|
||||||
## 48 - MENU GAME OVER
|
## 48 - MENU GAME OVER
|
||||||
YES
|
YES
|
||||||
@@ -278,4 +278,28 @@ LEAVE BLANK FOR
|
|||||||
OFFLINE MODE
|
OFFLINE MODE
|
||||||
|
|
||||||
## 93 - MENU OPCIONES
|
## 93 - MENU OPCIONES
|
||||||
HISCORE TABLE
|
HISCORE TABLE
|
||||||
|
|
||||||
|
## 94 - GAMEPAD NOTIFICATION
|
||||||
|
CONNECTED
|
||||||
|
|
||||||
|
## 95 - GAMEPAD NOTIFICATION
|
||||||
|
DISCONNECTED
|
||||||
|
|
||||||
|
## 96 - HOTKEY NOTIFICATION
|
||||||
|
Zoom
|
||||||
|
|
||||||
|
## 97 - HOTKEY NOTIFICATION
|
||||||
|
Fullscreen
|
||||||
|
|
||||||
|
## 98 - HOTKEY NOTIFICATION
|
||||||
|
Window
|
||||||
|
|
||||||
|
## 99 - HOTKEY NOTIFICATION
|
||||||
|
Shader
|
||||||
|
|
||||||
|
## 100 - HOTKEY NOTIFICATION
|
||||||
|
Preset
|
||||||
|
|
||||||
|
## 101 - HOTKEY NOTIFICATION
|
||||||
|
Press ESC again to quit
|
||||||
+26
-2
@@ -140,7 +140,7 @@ CONTINUAR?
|
|||||||
CONTINUAR
|
CONTINUAR
|
||||||
|
|
||||||
## 47 - MENU DE PAUSA
|
## 47 - MENU DE PAUSA
|
||||||
SALIR DEL JUEGO
|
VOLVER AL TITULO
|
||||||
|
|
||||||
## 48 - MENU GAME OVER
|
## 48 - MENU GAME OVER
|
||||||
SI
|
SI
|
||||||
@@ -278,4 +278,28 @@ DEJA EN BLANCO PARA
|
|||||||
MODO SIN CONEXION
|
MODO SIN CONEXION
|
||||||
|
|
||||||
## 93 - MENU OPCIONES
|
## 93 - MENU OPCIONES
|
||||||
TABLA DE PUNTUACIONES
|
TABLA DE PUNTUACIONES
|
||||||
|
|
||||||
|
## 94 - NOTIFICACION MANDO
|
||||||
|
CONECTADO
|
||||||
|
|
||||||
|
## 95 - NOTIFICACION MANDO
|
||||||
|
DESCONECTADO
|
||||||
|
|
||||||
|
## 96 - NOTIFICACION HOTKEY
|
||||||
|
Zoom
|
||||||
|
|
||||||
|
## 97 - NOTIFICACION HOTKEY
|
||||||
|
Pantalla completa
|
||||||
|
|
||||||
|
## 98 - NOTIFICACION HOTKEY
|
||||||
|
Ventana
|
||||||
|
|
||||||
|
## 99 - NOTIFICACION HOTKEY
|
||||||
|
Shader
|
||||||
|
|
||||||
|
## 100 - NOTIFICACION HOTKEY
|
||||||
|
Preset
|
||||||
|
|
||||||
|
## 101 - NOTIFICACION HOTKEY
|
||||||
|
Vuelve a pulsar ESC para salir
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// Vulkan GLSL fragment shader — CRT-Pi PostFX
|
||||||
|
// Algoritmo de scanlines continuas con pesos gaussianos, bloom y máscara de fósforo.
|
||||||
|
// Basado en el shader CRT-Pi original (GLSL 3.3), portado a GLSL 4.50 con parámetros uniformes.
|
||||||
|
//
|
||||||
|
// Compile: glslc -fshader-stage=frag --target-env=vulkan1.0 crtpi_frag.glsl -o crtpi_frag.spv
|
||||||
|
// xxd -i crtpi_frag.spv > ../../source/core/rendering/sdl3gpu/crtpi_frag_spv.h
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 v_uv;
|
||||||
|
layout(location = 0) out vec4 out_color;
|
||||||
|
|
||||||
|
layout(set = 2, binding = 0) uniform sampler2D Texture;
|
||||||
|
|
||||||
|
layout(set = 3, binding = 0) uniform CrtPiBlock {
|
||||||
|
// vec4 #0
|
||||||
|
float scanline_weight; // Ajuste gaussiano de scanlines (default 6.0)
|
||||||
|
float scanline_gap_brightness; // Brillo mínimo entre scanlines (default 0.12)
|
||||||
|
float bloom_factor; // Factor de brillo en zonas iluminadas (default 3.5)
|
||||||
|
float input_gamma; // Gamma de entrada — linealización (default 2.4)
|
||||||
|
// vec4 #1
|
||||||
|
float output_gamma; // Gamma de salida — codificación (default 2.2)
|
||||||
|
float mask_brightness; // Brillo sub-píxeles de la máscara (default 0.80)
|
||||||
|
float curvature_x; // Distorsión barrel eje X (default 0.05)
|
||||||
|
float curvature_y; // Distorsión barrel eje Y (default 0.10)
|
||||||
|
// vec4 #2
|
||||||
|
int mask_type; // 0=ninguna, 1=verde/magenta, 2=RGB fósforo
|
||||||
|
int enable_scanlines; // 0 = off, 1 = on
|
||||||
|
int enable_multisample; // 0 = off, 1 = on (antialiasing analítico de scanlines)
|
||||||
|
int enable_gamma; // 0 = off, 1 = on
|
||||||
|
// vec4 #3
|
||||||
|
int enable_curvature; // 0 = off, 1 = on
|
||||||
|
int enable_sharper; // 0 = off, 1 = on
|
||||||
|
float texture_width; // Ancho del canvas lógico en píxeles
|
||||||
|
float texture_height; // Alto del canvas lógico en píxeles
|
||||||
|
} u;
|
||||||
|
|
||||||
|
// Distorsión barrel CRT
|
||||||
|
vec2 distort(vec2 coord, vec2 screen_scale) {
|
||||||
|
vec2 curvature = vec2(u.curvature_x, u.curvature_y);
|
||||||
|
vec2 barrel_scale = 1.0 - (0.23 * curvature);
|
||||||
|
coord *= screen_scale;
|
||||||
|
coord -= vec2(0.5);
|
||||||
|
float rsq = coord.x * coord.x + coord.y * coord.y;
|
||||||
|
coord += coord * (curvature * rsq);
|
||||||
|
coord *= barrel_scale;
|
||||||
|
if (abs(coord.x) >= 0.5 || abs(coord.y) >= 0.5) {
|
||||||
|
return vec2(-1.0); // fuera de pantalla
|
||||||
|
}
|
||||||
|
coord += vec2(0.5);
|
||||||
|
coord /= screen_scale;
|
||||||
|
return coord;
|
||||||
|
}
|
||||||
|
|
||||||
|
float calcScanLineWeight(float dist) {
|
||||||
|
return max(1.0 - dist * dist * u.scanline_weight, u.scanline_gap_brightness);
|
||||||
|
}
|
||||||
|
|
||||||
|
float calcScanLine(float dy, float filter_width) {
|
||||||
|
float weight = calcScanLineWeight(dy);
|
||||||
|
if (u.enable_multisample != 0) {
|
||||||
|
weight += calcScanLineWeight(dy - filter_width);
|
||||||
|
weight += calcScanLineWeight(dy + filter_width);
|
||||||
|
weight *= 0.3333333;
|
||||||
|
}
|
||||||
|
return weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 tex_size = vec2(u.texture_width, u.texture_height);
|
||||||
|
|
||||||
|
// filterWidth: equivalente al original (768.0 / TextureSize.y) / 3.0
|
||||||
|
float filter_width = (768.0 / u.texture_height) / 3.0;
|
||||||
|
|
||||||
|
vec2 texcoord = v_uv;
|
||||||
|
|
||||||
|
// Curvatura barrel opcional
|
||||||
|
if (u.enable_curvature != 0) {
|
||||||
|
texcoord = distort(texcoord, vec2(1.0, 1.0));
|
||||||
|
if (texcoord.x < 0.0) {
|
||||||
|
out_color = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 texcoord_in_pixels = texcoord * tex_size;
|
||||||
|
vec2 tc;
|
||||||
|
float scan_line_weight;
|
||||||
|
|
||||||
|
if (u.enable_sharper != 0) {
|
||||||
|
// Modo SHARPER: filtrado bicúbico-like con subpixel sharpen
|
||||||
|
vec2 temp_coord = floor(texcoord_in_pixels) + 0.5;
|
||||||
|
tc = temp_coord / tex_size;
|
||||||
|
vec2 deltas = texcoord_in_pixels - temp_coord;
|
||||||
|
scan_line_weight = calcScanLine(deltas.y, filter_width);
|
||||||
|
vec2 signs = sign(deltas);
|
||||||
|
deltas.x *= 2.0;
|
||||||
|
deltas = deltas * deltas;
|
||||||
|
deltas.y = deltas.y * deltas.y;
|
||||||
|
deltas.x *= 0.5;
|
||||||
|
deltas.y *= 8.0;
|
||||||
|
deltas /= tex_size;
|
||||||
|
deltas *= signs;
|
||||||
|
tc = tc + deltas;
|
||||||
|
} else {
|
||||||
|
// Modo estándar
|
||||||
|
float temp_y = floor(texcoord_in_pixels.y) + 0.5;
|
||||||
|
float y_coord = temp_y / tex_size.y;
|
||||||
|
float dy = texcoord_in_pixels.y - temp_y;
|
||||||
|
scan_line_weight = calcScanLine(dy, filter_width);
|
||||||
|
float sign_y = sign(dy);
|
||||||
|
dy = dy * dy;
|
||||||
|
dy = dy * dy;
|
||||||
|
dy *= 8.0;
|
||||||
|
dy /= tex_size.y;
|
||||||
|
dy *= sign_y;
|
||||||
|
tc = vec2(texcoord.x, y_coord + dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 colour = texture(Texture, tc).rgb;
|
||||||
|
|
||||||
|
if (u.enable_scanlines != 0) {
|
||||||
|
if (u.enable_gamma != 0) {
|
||||||
|
colour = pow(colour, vec3(u.input_gamma));
|
||||||
|
}
|
||||||
|
colour *= scan_line_weight * u.bloom_factor;
|
||||||
|
if (u.enable_gamma != 0) {
|
||||||
|
colour = pow(colour, vec3(1.0 / u.output_gamma));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Máscara de fósforo
|
||||||
|
if (u.mask_type == 1) {
|
||||||
|
float which_mask = fract(gl_FragCoord.x * 0.5);
|
||||||
|
vec3 mask = (which_mask < 0.5)
|
||||||
|
? vec3(u.mask_brightness, 1.0, u.mask_brightness)
|
||||||
|
: vec3(1.0, u.mask_brightness, 1.0);
|
||||||
|
colour *= mask;
|
||||||
|
} else if (u.mask_type == 2) {
|
||||||
|
float which_mask = fract(gl_FragCoord.x * 0.3333333);
|
||||||
|
vec3 mask = vec3(u.mask_brightness);
|
||||||
|
if (which_mask < 0.3333333)
|
||||||
|
mask.x = 1.0;
|
||||||
|
else if (which_mask < 0.6666666)
|
||||||
|
mask.y = 1.0;
|
||||||
|
else
|
||||||
|
mask.z = 1.0;
|
||||||
|
colour *= mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_color = vec4(colour, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// Vulkan GLSL fragment shader — PostFX effects
|
||||||
|
// Used for SDL3 GPU API (SPIR-V path, Win/Linux).
|
||||||
|
// Compile: glslc postfx.frag -o postfx.frag.spv
|
||||||
|
// xxd -i postfx.frag.spv > ../../source/core/rendering/sdl3gpu/postfx_frag_spv.h
|
||||||
|
//
|
||||||
|
// PostFXUniforms must match exactly the C++ struct in sdl3gpu_shader.hpp
|
||||||
|
// (16 floats = 4 × vec4 = 64 bytes, std140/scalar layout).
|
||||||
|
// IMPORTANT: Qualsevol canvi ací cal replicar-lo a mà a
|
||||||
|
// source/core/rendering/sdl3gpu/msl/postfx_frag.msl.h (no hi ha generador).
|
||||||
|
|
||||||
|
layout(location = 0) in vec2 v_uv;
|
||||||
|
layout(location = 0) out vec4 out_color;
|
||||||
|
|
||||||
|
layout(set = 2, binding = 0) uniform sampler2D scene;
|
||||||
|
|
||||||
|
layout(set = 3, binding = 0) uniform PostFXUniforms {
|
||||||
|
float vignette_strength;
|
||||||
|
float chroma_min; // intensitat mínima de l'aberració cromàtica
|
||||||
|
float scanline_strength;
|
||||||
|
float screen_height;
|
||||||
|
float mask_strength;
|
||||||
|
float gamma_strength;
|
||||||
|
float curvature;
|
||||||
|
float bleeding;
|
||||||
|
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||||||
|
float time; // seconds since SDL init
|
||||||
|
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
|
||||||
|
float chroma_max; // intensitat màxima; si == chroma_min → chroma estàtic
|
||||||
|
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
|
||||||
|
float scan_dark_ratio; // fracció de subfila fosca per fila lògica (1/3 ≈ 0.333)
|
||||||
|
float scan_dark_floor; // multiplicador de brillantor de la subfila fosca
|
||||||
|
float scan_edge_soft; // 0 = step dur; 1 = suavitzat d'1 píxel físic (estil crtpi)
|
||||||
|
float pad3; // padding per tancar a 64 bytes (4 × vec4)
|
||||||
|
} u;
|
||||||
|
|
||||||
|
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
|
||||||
|
// NEAREST quan l'offset de chroma és subpíxel: sense interpolar, l'offset
|
||||||
|
// arrodonia entre 1 i 2 píxels i el drift temporal feia un parpelleig discret.
|
||||||
|
float sampleBilinearX(vec2 uv_target, int channel) {
|
||||||
|
vec2 tex_size = vec2(textureSize(scene, 0));
|
||||||
|
float px = uv_target.x * tex_size.x - 0.5;
|
||||||
|
float p_floor = floor(px);
|
||||||
|
float f = px - p_floor;
|
||||||
|
vec4 c0 = texture(scene, vec2((p_floor + 0.5) / tex_size.x, uv_target.y));
|
||||||
|
vec4 c1 = texture(scene, vec2((p_floor + 1.5) / tex_size.x, uv_target.y));
|
||||||
|
return mix(c0[channel], c1[channel], f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// YCbCr helpers for NTSC bleeding
|
||||||
|
vec3 rgb_to_ycc(vec3 rgb) {
|
||||||
|
return vec3(
|
||||||
|
0.299*rgb.r + 0.587*rgb.g + 0.114*rgb.b,
|
||||||
|
-0.169*rgb.r - 0.331*rgb.g + 0.500*rgb.b + 0.5,
|
||||||
|
0.500*rgb.r - 0.419*rgb.g - 0.081*rgb.b + 0.5
|
||||||
|
);
|
||||||
|
}
|
||||||
|
vec3 ycc_to_rgb(vec3 ycc) {
|
||||||
|
float y = ycc.x;
|
||||||
|
float cb = ycc.y - 0.5;
|
||||||
|
float cr = ycc.z - 0.5;
|
||||||
|
return clamp(vec3(
|
||||||
|
y + 1.402*cr,
|
||||||
|
y - 0.344*cb - 0.714*cr,
|
||||||
|
y + 1.772*cb
|
||||||
|
), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 uv = v_uv;
|
||||||
|
|
||||||
|
// Curvatura barrel CRT
|
||||||
|
if (u.curvature > 0.0) {
|
||||||
|
vec2 c = uv - 0.5;
|
||||||
|
float rsq = dot(c, c);
|
||||||
|
vec2 dist = vec2(0.05, 0.1) * u.curvature;
|
||||||
|
vec2 barrelScale = vec2(1.0) - 0.23 * dist;
|
||||||
|
c += c * (dist * rsq);
|
||||||
|
c *= barrelScale;
|
||||||
|
if (abs(c.x) >= 0.5 || abs(c.y) >= 0.5) {
|
||||||
|
out_color = vec4(0.0, 0.0, 0.0, 1.0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uv = c + 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Muestra base
|
||||||
|
vec3 base = texture(scene, uv).rgb;
|
||||||
|
|
||||||
|
// Sangrado NTSC — difuminado horizontal de crominancia.
|
||||||
|
// step = 1 pixel lógico de juego en UV.
|
||||||
|
vec3 colour;
|
||||||
|
if (u.bleeding > 0.0) {
|
||||||
|
float tw = float(textureSize(scene, 0).x);
|
||||||
|
float step = 1.0 / tw; // 1 pixel lógico en UV
|
||||||
|
vec3 ycc = rgb_to_ycc(base);
|
||||||
|
vec3 ycc_l2 = rgb_to_ycc(texture(scene, uv - vec2(2.0*step, 0.0)).rgb);
|
||||||
|
vec3 ycc_l1 = rgb_to_ycc(texture(scene, uv - vec2(1.0*step, 0.0)).rgb);
|
||||||
|
vec3 ycc_r1 = rgb_to_ycc(texture(scene, uv + vec2(1.0*step, 0.0)).rgb);
|
||||||
|
vec3 ycc_r2 = rgb_to_ycc(texture(scene, uv + vec2(2.0*step, 0.0)).rgb);
|
||||||
|
ycc.yz = (ycc_l2.yz + ycc_l1.yz*2.0 + ycc.yz*2.0 + ycc_r1.yz*2.0 + ycc_r2.yz) / 8.0;
|
||||||
|
colour = mix(base, ycc_to_rgb(ycc), u.bleeding);
|
||||||
|
} else {
|
||||||
|
colour = base;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aberración cromática — intensitat varia entre chroma_min i chroma_max amb
|
||||||
|
// una sinusoidal (si min == max, queda estàtica). Mostreig bilinear horitzontal
|
||||||
|
// per evitar el "tic-tac" del NEAREST sampler quan l'offset és subpíxel.
|
||||||
|
if (u.chroma_min > 0.0 || u.chroma_max > 0.0) {
|
||||||
|
float ca = mix(u.chroma_min, u.chroma_max, 0.5 + 0.5 * sin(u.time * 7.3)) * 0.005;
|
||||||
|
colour.r = sampleBilinearX(uv + vec2(ca, 0.0), 0);
|
||||||
|
colour.b = sampleBilinearX(uv - vec2(ca, 0.0), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corrección gamma (linealizar antes de scanlines, codificar después)
|
||||||
|
if (u.gamma_strength > 0.0) {
|
||||||
|
vec3 lin = pow(colour, vec3(2.4));
|
||||||
|
colour = mix(colour, lin, u.gamma_strength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scanlines — tècnica dels 3 subpíxels verticals per píxel lògic (aee/projecte_2026):
|
||||||
|
// franja fosca ocupant `scan_dark_ratio` al final de cada fila lògica. La transició es
|
||||||
|
// suavitza amb smoothstep d'ample ≈ 1 píxel físic (estil crtpi: filtratge analític
|
||||||
|
// continu), controlat per `scan_edge_soft`. A 0 és equivalent al step dur antic.
|
||||||
|
if (u.scanline_strength > 0.0) {
|
||||||
|
float ps = max(u.pixel_scale, 1.0);
|
||||||
|
float sub = fract(uv.y * u.screen_height); // [0,1) dins la fila lògica
|
||||||
|
float dark_center = 1.0 - u.scan_dark_ratio * 0.5; // centre de la franja fosca
|
||||||
|
float d = abs(sub - dark_center);
|
||||||
|
d = min(d, 1.0 - d); // wrap a la fila següent
|
||||||
|
float half_width = u.scan_dark_ratio * 0.5;
|
||||||
|
float softness = u.scan_edge_soft * 0.5 / ps; // mig píxel físic a cada costat
|
||||||
|
float band = 1.0 - smoothstep(half_width - softness, half_width + softness, d);
|
||||||
|
float scan = mix(1.0, u.scan_dark_floor, band);
|
||||||
|
colour *= mix(1.0, scan, u.scanline_strength);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (u.gamma_strength > 0.0) {
|
||||||
|
vec3 enc = pow(colour, vec3(1.0 / 2.2));
|
||||||
|
colour = mix(colour, enc, u.gamma_strength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viñeta
|
||||||
|
vec2 d = uv - 0.5;
|
||||||
|
float vignette = 1.0 - dot(d, d) * u.vignette_strength;
|
||||||
|
colour *= clamp(vignette, 0.0, 1.0);
|
||||||
|
|
||||||
|
// Máscara de fósforo RGB — después de scanlines (orden original):
|
||||||
|
// filas brillantes saturadas → máscara invisible, filas oscuras → RGB visible.
|
||||||
|
if (u.mask_strength > 0.0) {
|
||||||
|
float whichMask = fract(gl_FragCoord.x * 0.3333333);
|
||||||
|
vec3 mask = vec3(0.80);
|
||||||
|
if (whichMask < 0.3333333)
|
||||||
|
mask.x = 1.0;
|
||||||
|
else if (whichMask < 0.6666666)
|
||||||
|
mask.y = 1.0;
|
||||||
|
else
|
||||||
|
mask.z = 1.0;
|
||||||
|
colour = mix(colour, colour * mask, u.mask_strength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parpadeo de fósforo CRT (~50 Hz)
|
||||||
|
if (u.flicker > 0.0) {
|
||||||
|
float flicker_wave = sin(u.time * 100.0) * 0.5 + 0.5;
|
||||||
|
colour *= 1.0 - u.flicker * 0.04 * flicker_wave;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_color = vec4(colour, 1.0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#version 450
|
||||||
|
|
||||||
|
// Vulkan GLSL vertex shader — postfx full-screen triangle
|
||||||
|
// Used for SDL3 GPU API (SPIR-V path, Win/Linux).
|
||||||
|
// Compile: glslc postfx.vert -o postfx.vert.spv
|
||||||
|
// xxd -i postfx.vert.spv > ../../source/core/rendering/sdl3gpu/postfx_vert_spv.h
|
||||||
|
|
||||||
|
layout(location = 0) out vec2 v_uv;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Full-screen triangle (no vertex buffer needed)
|
||||||
|
const vec2 positions[3] = vec2[3](
|
||||||
|
vec2(-1.0, -1.0),
|
||||||
|
vec2( 3.0, -1.0),
|
||||||
|
vec2(-1.0, 3.0)
|
||||||
|
);
|
||||||
|
const vec2 uvs[3] = vec2[3](
|
||||||
|
vec2(0.0, 1.0),
|
||||||
|
vec2(2.0, 1.0),
|
||||||
|
vec2(0.0,-1.0)
|
||||||
|
);
|
||||||
|
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
|
||||||
|
v_uv = uvs[gl_VertexIndex];
|
||||||
|
}
|
||||||
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();
|
|
||||||
};
|
|
||||||
@@ -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,118 @@
|
|||||||
|
#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"
|
||||||
|
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
// 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,390 @@
|
|||||||
|
#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 activo
|
||||||
|
auto Input::checkAnyInput(Device device, int index) -> bool {
|
||||||
|
if (device == Device::ANY) {
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device == Device::KEYBOARD || device == Device::ANY) {
|
||||||
|
const bool *key_states = SDL_GetKeyboardState(nullptr);
|
||||||
|
|
||||||
|
if (std::ranges::any_of(key_bindings_,
|
||||||
|
[key_states](const auto &key_binding) { return key_states[key_binding.scancode]; })) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameControllerFound() && index >= 0 && index < (int)connected_controllers_.size()) {
|
||||||
|
if (device == Device::GAMECONTROLLER || device == Device::ANY) {
|
||||||
|
for (auto &game_controller_binding : game_controller_bindings_) {
|
||||||
|
if (SDL_GetGamepadButton(connected_controllers_[index], game_controller_binding.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,129 @@
|
|||||||
|
#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,
|
||||||
|
|
||||||
|
// Centinela final (usar para sizing)
|
||||||
|
NUMBER_OF_INPUTS
|
||||||
|
};
|
||||||
|
|
||||||
|
// Singleton API
|
||||||
|
static void init(const std::string &game_controller_db_path); // Crea la instancia
|
||||||
|
static void destroy(); // Libera la instancia
|
||||||
|
static auto get() -> Input *; // Obtiene el puntero a la instancia
|
||||||
|
|
||||||
|
~Input(); // Destructor
|
||||||
|
|
||||||
|
void update(); // Actualiza el estado del objeto
|
||||||
|
void bindKey(Action input, SDL_Scancode code); // Asigna inputs a teclas
|
||||||
|
void bindGameControllerButton(Action input, SDL_GamepadButton button); // Asigna inputs a botones del mando
|
||||||
|
|
||||||
|
auto checkInput(Action input, Repeat repeat = Repeat::ON, Device device = Device::ANY, int index = 0) -> bool; // Comprueba si un input esta activo
|
||||||
|
auto checkAnyInput(Device device = Device::ANY, int index = 0) -> bool; // Comprueba si hay almenos un input activo
|
||||||
|
|
||||||
|
auto discoverGameController() -> bool; // Busca si hay un mando conectado
|
||||||
|
|
||||||
|
// Procesa un evento SDL_EVENT_GAMEPAD_ADDED. Devuelve true si el mando se ha añadido
|
||||||
|
// (no estaba ya registrado) y escribe el nombre visible en outName.
|
||||||
|
auto handleGamepadAdded(SDL_JoystickID jid, std::string &out_name) -> bool;
|
||||||
|
|
||||||
|
// Procesa un evento SDL_EVENT_GAMEPAD_REMOVED. Devuelve true si se ha encontrado y
|
||||||
|
// eliminado, y escribe el nombre visible en outName.
|
||||||
|
auto handleGamepadRemoved(SDL_JoystickID jid, std::string &out_name) -> bool;
|
||||||
|
|
||||||
|
[[nodiscard]] auto gameControllerFound() const -> bool; // Comprueba si hay algun mando conectado
|
||||||
|
[[nodiscard]] auto getNumControllers() const -> int; // Obten el numero de mandos conectados
|
||||||
|
auto getControllerName(int index) -> std::string; // Obten el nombre de un mando de juego
|
||||||
|
|
||||||
|
void setVerbose(bool value); // Establece si ha de mostrar mensajes
|
||||||
|
void disableUntil(Disable value); // Deshabilita las entradas durante un periodo de tiempo
|
||||||
|
void enable(); // Hablita las entradas
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct KeyBindings {
|
||||||
|
Uint8 scancode; // Scancode asociado
|
||||||
|
bool active; // Indica si está activo
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GameControllerBindings {
|
||||||
|
SDL_GamepadButton button; // GameControllerButton asociado
|
||||||
|
bool active; // Indica si está activo
|
||||||
|
};
|
||||||
|
|
||||||
|
// Objetos y punteros
|
||||||
|
std::vector<SDL_Gamepad *> connected_controllers_; // Vector con todos los mandos conectados
|
||||||
|
std::vector<SDL_JoystickID> connected_controller_ids_; // Instance IDs paralelos para mapear eventos
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
std::vector<KeyBindings> key_bindings_; // Vector con las teclas asociadas a los inputs predefinidos
|
||||||
|
std::vector<GameControllerBindings> game_controller_bindings_; // Vector con las teclas asociadas a los inputs predefinidos
|
||||||
|
std::vector<std::string> controller_names_; // Vector con los nombres de los mandos
|
||||||
|
int num_gamepads_{0}; // Numero de mandos conectados
|
||||||
|
std::string db_path_; // Ruta al archivo gamecontrollerdb.txt
|
||||||
|
bool verbose_{true}; // Indica si ha de mostrar mensajes
|
||||||
|
Disable disabled_until_{Disable::NOT_DISABLED}; // Tiempo que esta deshabilitado
|
||||||
|
bool enabled_{true}; // Indica si está habilitado
|
||||||
|
|
||||||
|
static Input *instance; // Instancia única
|
||||||
|
|
||||||
|
explicit Input(std::string file); // Constructor privado (usar Input::init)
|
||||||
|
|
||||||
|
// Construye el nombre visible de un mando (name truncado + sufijo #N)
|
||||||
|
static auto buildControllerName(SDL_Gamepad *pad, int pad_index) -> std::string;
|
||||||
|
|
||||||
|
// Helpers de checkInput
|
||||||
|
auto checkKeyboardInput(Action input, Repeat repeat) -> bool;
|
||||||
|
auto checkGameControllerInput(Action input, Repeat repeat, int index) -> bool;
|
||||||
|
|
||||||
|
// Helpers de discoverGameController
|
||||||
|
void resetGameControllerState();
|
||||||
|
void ensureGamepadSubsystem();
|
||||||
|
auto openGamepad(SDL_JoystickID joystick_id, int pad_index) -> bool;
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#include "core/input/mouse.hpp"
|
||||||
|
|
||||||
|
namespace Mouse {
|
||||||
|
Uint32 cursor_hide_time = 3000; // Tiempo en milisegundos para ocultar el cursor por inactividad
|
||||||
|
Uint32 last_mouse_move_time = 0; // Última vez que el ratón se movió
|
||||||
|
bool cursor_visible = true; // Estado del cursor
|
||||||
|
|
||||||
|
void handleEvent(const SDL_Event &event, bool fullscreen) {
|
||||||
|
if (event.type == SDL_EVENT_MOUSE_MOTION) {
|
||||||
|
last_mouse_move_time = SDL_GetTicks();
|
||||||
|
if (!cursor_visible && !fullscreen) {
|
||||||
|
SDL_ShowCursor();
|
||||||
|
cursor_visible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateCursorVisibility(bool fullscreen) {
|
||||||
|
// En pantalla completa el cursor siempre está oculto
|
||||||
|
if (fullscreen) {
|
||||||
|
if (cursor_visible) {
|
||||||
|
SDL_HideCursor();
|
||||||
|
cursor_visible = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// En modo ventana, lo oculta tras el periodo de inactividad
|
||||||
|
const Uint32 CURRENT_TIME = SDL_GetTicks();
|
||||||
|
if (cursor_visible && (CURRENT_TIME - last_mouse_move_time > cursor_hide_time)) {
|
||||||
|
SDL_HideCursor();
|
||||||
|
cursor_visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace Mouse
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
namespace Mouse {
|
||||||
|
extern Uint32 cursor_hide_time; // Tiempo en milisegundos para ocultar el cursor por inactividad
|
||||||
|
extern Uint32 last_mouse_move_time; // Última vez que el ratón se movió
|
||||||
|
extern bool cursor_visible; // Estado del cursor
|
||||||
|
|
||||||
|
// Procesa un evento de ratón. En pantalla completa ignora el movimiento
|
||||||
|
// para no volver a mostrar el cursor.
|
||||||
|
void handleEvent(const SDL_Event &event, bool fullscreen);
|
||||||
|
|
||||||
|
// Actualiza la visibilidad del cursor. En modo ventana lo oculta
|
||||||
|
// después de cursorHideTime ms sin movimiento. En pantalla completa
|
||||||
|
// lo mantiene siempre oculto.
|
||||||
|
void updateCursorVisibility(bool fullscreen);
|
||||||
|
} // namespace Mouse
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#include "core/locale/lang.h"
|
||||||
|
|
||||||
|
#include <fstream> // for basic_ifstream, basic_istream, ifstream
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include "core/resources/asset.h" // for Asset
|
||||||
|
#include "core/resources/resource_helper.h"
|
||||||
|
|
||||||
|
// Instancia única
|
||||||
|
Lang *Lang::instance = nullptr;
|
||||||
|
|
||||||
|
// Singleton API
|
||||||
|
void Lang::init() {
|
||||||
|
Lang::instance = new Lang();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lang::destroy() {
|
||||||
|
delete Lang::instance;
|
||||||
|
Lang::instance = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Lang::get() -> Lang * {
|
||||||
|
return Lang::instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Lang::Lang() = default;
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
Lang::~Lang() = default;
|
||||||
|
|
||||||
|
// Inicializa los textos del juego en el idioma seleccionado
|
||||||
|
auto Lang::setLang(Code lang) -> bool {
|
||||||
|
std::string file;
|
||||||
|
|
||||||
|
switch (lang) {
|
||||||
|
case Code::ES_ES:
|
||||||
|
file = Asset::get()->get("es_ES.txt");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Code::EN_UK:
|
||||||
|
file = Asset::get()->get("en_UK.txt");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Code::BA_BA:
|
||||||
|
file = Asset::get()->get("ba_BA.txt");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
file = Asset::get()->get("en_UK.txt");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto &text_string : text_strings_) {
|
||||||
|
text_string = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lee el fichero via ResourceHelper (pack o filesystem)
|
||||||
|
auto bytes = ResourceHelper::loadFile(file);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
|
||||||
|
std::stringstream ss(content);
|
||||||
|
std::string line;
|
||||||
|
int index = 0;
|
||||||
|
while (std::getline(ss, line)) {
|
||||||
|
// Normaliza CRLF: en Windows els fitxers es llegeixen en binari i
|
||||||
|
// getline només talla pel \n, deixant un \r residual que faria que les
|
||||||
|
// línies en blanc no semblen buides (i sobreescriguen més enllà de
|
||||||
|
// mTextStrings, corrompent el heap).
|
||||||
|
if (!line.empty() && line.back() == '\r') {
|
||||||
|
line.pop_back();
|
||||||
|
}
|
||||||
|
// Almacena solo las lineas que no empiezan por # o no esten vacias
|
||||||
|
const bool NOT_COMMENT = line.substr(0, 1) != "#";
|
||||||
|
const bool NOT_EMPTY = !line.empty();
|
||||||
|
if (NOT_COMMENT && NOT_EMPTY) {
|
||||||
|
if (index >= MAX_TEXT_STRINGS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
text_strings_[index] = line;
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene la cadena de texto del indice
|
||||||
|
auto Lang::getText(int index) -> std::string {
|
||||||
|
return text_strings_[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devuelve el siguiente idioma del ciclo (wrap-around)
|
||||||
|
auto Lang::nextLanguage(Code c) -> Code {
|
||||||
|
const int NEXT = (static_cast<int>(c) + 1) % MAX_LANGUAGES;
|
||||||
|
return static_cast<Code>(NEXT);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint> // for uint8_t
|
||||||
|
#include <string> // for string, basic_string
|
||||||
|
|
||||||
|
// Clase Lang
|
||||||
|
class Lang {
|
||||||
|
public:
|
||||||
|
// Códigos de idioma (basados en la convención IETF de los ficheros de locale)
|
||||||
|
enum class Code : std::uint8_t {
|
||||||
|
ES_ES = 0,
|
||||||
|
BA_BA = 1,
|
||||||
|
EN_UK = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr int MAX_LANGUAGES = 3; // Número total de idiomas disponibles
|
||||||
|
|
||||||
|
// Singleton API
|
||||||
|
static void init(); // Crea la instancia
|
||||||
|
static void destroy(); // Libera la instancia
|
||||||
|
static auto get() -> Lang *; // Obtiene el puntero a la instancia
|
||||||
|
|
||||||
|
~Lang(); // Destructor
|
||||||
|
|
||||||
|
auto setLang(Code lang) -> bool; // Inicializa los textos del juego en el idioma seleccionado
|
||||||
|
auto getText(int index) -> std::string; // Obtiene la cadena de texto del indice
|
||||||
|
|
||||||
|
static auto nextLanguage(Code c) -> Code; // Devuelve el siguiente idioma del ciclo
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr int MAX_TEXT_STRINGS = 110;
|
||||||
|
|
||||||
|
std::string text_strings_[MAX_TEXT_STRINGS]; // Vector con los textos
|
||||||
|
|
||||||
|
static Lang *instance; // Instancia única
|
||||||
|
|
||||||
|
Lang(); // Constructor privado (usar Lang::init)
|
||||||
|
};
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
#include "core/rendering/animatedsprite.h"
|
||||||
|
|
||||||
|
#include <iostream> // for cout
|
||||||
|
#include <sstream> // for basic_stringstream
|
||||||
|
|
||||||
|
#include "core/rendering/texture.h" // for Texture
|
||||||
|
#include "core/resources/resource_helper.h" // for loadFile (pack + filesystem fallback)
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Normalitza CRLF: fitxers .ani amb terminadors de Windows fan que
|
||||||
|
// line == "[animation]" no faci match i el parser entri en bucle
|
||||||
|
// infinit / no carregui cap animació.
|
||||||
|
void stripCr(std::string &s) {
|
||||||
|
if (!s.empty() && s.back() == '\r') {
|
||||||
|
s.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseFramesList(const std::string &value, Animation &buffer, int frame_width, int frame_height, int frames_per_row, int max_tiles) {
|
||||||
|
std::stringstream ss(value);
|
||||||
|
std::string tmp;
|
||||||
|
SDL_Rect rect = {0, 0, frame_width, frame_height};
|
||||||
|
while (getline(ss, tmp, ',')) {
|
||||||
|
const int NUM_TILE = std::stoi(tmp) > max_tiles ? 0 : std::stoi(tmp);
|
||||||
|
rect.x = (NUM_TILE % frames_per_row) * frame_width;
|
||||||
|
rect.y = (NUM_TILE / frames_per_row) * frame_height;
|
||||||
|
buffer.frames.push_back(rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseAnimationField(const std::string &line, int pos, Animation &buffer, int frame_width, int frame_height, int frames_per_row, int max_tiles, const std::string &filename) {
|
||||||
|
const std::string KEY = line.substr(0, pos);
|
||||||
|
const std::string VALUE = line.substr(pos + 1, line.length());
|
||||||
|
if (KEY == "name") {
|
||||||
|
buffer.name = VALUE;
|
||||||
|
} else if (KEY == "speed") {
|
||||||
|
buffer.speed = std::stoi(VALUE);
|
||||||
|
// Time-based: el valor del .ani s'expressa en "ticks per frame
|
||||||
|
// d'animació" (assumint 60 Hz). El camp `speed` (int) es manté per al
|
||||||
|
// fallback frame-based; el nou `step_duration_s` (float) és el que
|
||||||
|
// gasta animate(dt).
|
||||||
|
buffer.step_duration_s = static_cast<float>(buffer.speed) / 60.0F;
|
||||||
|
} else if (KEY == "loop") {
|
||||||
|
buffer.loop = std::stoi(VALUE);
|
||||||
|
} else if (KEY == "frames") {
|
||||||
|
parseFramesList(VALUE, buffer, frame_width, frame_height, frames_per_row, max_tiles);
|
||||||
|
} else {
|
||||||
|
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << KEY.c_str() << "\"" << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parseAnimationBlock(std::istream &file, int frame_width, int frame_height, int frames_per_row, int max_tiles, const std::string &filename) -> Animation {
|
||||||
|
Animation buffer;
|
||||||
|
buffer.speed = 0;
|
||||||
|
buffer.step_duration_s = 0.0F;
|
||||||
|
buffer.loop = -1;
|
||||||
|
buffer.counter = 0;
|
||||||
|
buffer.current_frame = 0;
|
||||||
|
buffer.completed = false;
|
||||||
|
buffer.time_accumulator_s = 0.0F;
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
do {
|
||||||
|
if (!std::getline(file, line)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
stripCr(line);
|
||||||
|
int pos = line.find('=');
|
||||||
|
if (pos != (int)std::string::npos) {
|
||||||
|
parseAnimationField(line, pos, buffer, frame_width, frame_height, frames_per_row, max_tiles, filename);
|
||||||
|
}
|
||||||
|
} while (line != "[/animation]");
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
void parseGlobalField(const std::string &line, int pos, int &frames_per_row, int &frame_width, int &frame_height, int &max_tiles, const Texture *texture, const std::string &filename) {
|
||||||
|
const std::string KEY = line.substr(0, pos);
|
||||||
|
const std::string VALUE = line.substr(pos + 1, line.length());
|
||||||
|
if (KEY == "framesPerRow") {
|
||||||
|
frames_per_row = std::stoi(VALUE);
|
||||||
|
} else if (KEY == "frameWidth") {
|
||||||
|
frame_width = std::stoi(VALUE);
|
||||||
|
} else if (KEY == "frameHeight") {
|
||||||
|
frame_height = std::stoi(VALUE);
|
||||||
|
} else {
|
||||||
|
std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << KEY.c_str() << "\"" << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frames_per_row == 0 && frame_width > 0) {
|
||||||
|
frames_per_row = texture->getWidth() / frame_width;
|
||||||
|
}
|
||||||
|
if (max_tiles == 0 && frame_width > 0 && frame_height > 0) {
|
||||||
|
const int W = texture->getWidth() / frame_width;
|
||||||
|
const int H = texture->getHeight() / frame_height;
|
||||||
|
max_tiles = W * H;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Parser compartido: lee un istream con el formato .ani
|
||||||
|
static auto parseAnimationStream(std::istream &file, Texture *texture, const std::string &filename, bool verbose) -> AnimatedSpriteData {
|
||||||
|
AnimatedSpriteData as;
|
||||||
|
as.texture = texture;
|
||||||
|
int frames_per_row = 0;
|
||||||
|
int frame_width = 0;
|
||||||
|
int frame_height = 0;
|
||||||
|
int max_tiles = 0;
|
||||||
|
std::string line;
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
std::cout << "Animation loaded: " << filename << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
while (std::getline(file, line)) {
|
||||||
|
stripCr(line);
|
||||||
|
if (line == "[animation]") {
|
||||||
|
as.animations.push_back(parseAnimationBlock(file, frame_width, frame_height, frames_per_row, max_tiles, filename));
|
||||||
|
} else {
|
||||||
|
int pos = line.find('=');
|
||||||
|
if (pos != (int)std::string::npos) {
|
||||||
|
parseGlobalField(line, pos, frames_per_row, frame_width, frame_height, max_tiles, texture, filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return as;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carga la animación desde un fichero (vía ResourceHelper: pack si està inicialitzat, filesystem si no)
|
||||||
|
auto loadAnimationFromFile(Texture *texture, const std::string &file_path, bool verbose) -> AnimatedSpriteData {
|
||||||
|
const std::string FILE_NAME = file_path.substr(file_path.find_last_of("\\/") + 1);
|
||||||
|
auto bytes = ResourceHelper::loadFile(file_path);
|
||||||
|
if (bytes.empty()) {
|
||||||
|
if (verbose) {
|
||||||
|
std::cout << "Warning: Unable to open " << FILE_NAME.c_str() << " file" << '\n';
|
||||||
|
}
|
||||||
|
AnimatedSpriteData as;
|
||||||
|
as.texture = texture;
|
||||||
|
return as;
|
||||||
|
}
|
||||||
|
return loadAnimationFromMemory(texture, bytes, FILE_NAME, verbose);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carga la animación desde bytes en memoria
|
||||||
|
auto loadAnimationFromMemory(Texture *texture, const std::vector<uint8_t> &bytes, const std::string &name_for_logs, bool verbose) -> AnimatedSpriteData {
|
||||||
|
if (bytes.empty()) {
|
||||||
|
AnimatedSpriteData as;
|
||||||
|
as.texture = texture;
|
||||||
|
return as;
|
||||||
|
}
|
||||||
|
std::string content(reinterpret_cast<const char *>(bytes.data()), bytes.size());
|
||||||
|
std::stringstream ss(content);
|
||||||
|
return parseAnimationStream(ss, texture, name_for_logs, verbose);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
AnimatedSprite::AnimatedSprite(Texture *texture, SDL_Renderer *renderer, const std::string &file, const std::vector<std::string> *buffer)
|
||||||
|
: current_animation_(0) {
|
||||||
|
// Copia los punteros
|
||||||
|
setTexture(texture);
|
||||||
|
setRenderer(renderer);
|
||||||
|
|
||||||
|
// Carga las animaciones
|
||||||
|
if (!file.empty()) {
|
||||||
|
AnimatedSpriteData as = loadAnimationFromFile(texture, file);
|
||||||
|
|
||||||
|
// Copia los datos de las animaciones
|
||||||
|
animation_.insert(animation_.end(), as.animations.begin(), as.animations.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (buffer != nullptr) {
|
||||||
|
loadFromVector(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
AnimatedSprite::AnimatedSprite(SDL_Renderer *renderer, AnimatedSpriteData *data)
|
||||||
|
: current_animation_(0) {
|
||||||
|
// Copia los punteros
|
||||||
|
setTexture(data->texture);
|
||||||
|
setRenderer(renderer);
|
||||||
|
|
||||||
|
// Copia los datos de las animaciones
|
||||||
|
this->animation_.insert(this->animation_.end(), data->animations.begin(), data->animations.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
AnimatedSprite::~AnimatedSprite() {
|
||||||
|
for (auto &a : animation_) {
|
||||||
|
a.frames.clear();
|
||||||
|
}
|
||||||
|
animation_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el indice de la animación a partir del nombre
|
||||||
|
auto AnimatedSprite::getIndex(const std::string &name) -> int {
|
||||||
|
int index = -1;
|
||||||
|
|
||||||
|
for (const auto &a : animation_) {
|
||||||
|
index++;
|
||||||
|
if (a.name == name) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "** Warning: could not find \"" << name.c_str() << "\" animation" << '\n';
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avança l'acumulador i calcula el frame actual a partir de `step_duration_s`.
|
||||||
|
void AnimatedSprite::animate(float dt_s) {
|
||||||
|
Animation &anim = animation_[current_animation_];
|
||||||
|
if (!enabled_ || anim.step_duration_s <= 0.0F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
anim.time_accumulator_s += dt_s;
|
||||||
|
anim.current_frame = static_cast<int>(anim.time_accumulator_s / anim.step_duration_s);
|
||||||
|
|
||||||
|
if (anim.current_frame >= (int)anim.frames.size()) {
|
||||||
|
if (anim.loop == -1) {
|
||||||
|
anim.current_frame = anim.frames.size();
|
||||||
|
anim.completed = true;
|
||||||
|
} else {
|
||||||
|
anim.time_accumulator_s = 0.0F;
|
||||||
|
anim.current_frame = anim.loop;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSpriteClip(anim.frames[anim.current_frame]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el numero de frames de la animación actual
|
||||||
|
auto AnimatedSprite::getNumFrames() -> int {
|
||||||
|
return (int)animation_[current_animation_].frames.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el frame actual de la animación
|
||||||
|
void AnimatedSprite::setCurrentFrame(int num) {
|
||||||
|
// Descarta valores fuera de rango
|
||||||
|
if (num >= (int)animation_[current_animation_].frames.size()) {
|
||||||
|
num = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cambia el valor de la variable
|
||||||
|
animation_[current_animation_].current_frame = num;
|
||||||
|
animation_[current_animation_].counter = 0;
|
||||||
|
|
||||||
|
// Escoge el frame correspondiente de la animación
|
||||||
|
setSpriteClip(animation_[current_animation_].frames[animation_[current_animation_].current_frame]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor del contador
|
||||||
|
void AnimatedSprite::setAnimationCounter(const std::string &name, int num) {
|
||||||
|
animation_[getIndex(name)].counter = num;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la velocidad de una animación
|
||||||
|
void AnimatedSprite::setAnimationSpeed(const std::string &name, int speed) {
|
||||||
|
animation_[getIndex(name)].counter = speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la velocidad de una animación
|
||||||
|
void AnimatedSprite::setAnimationSpeed(int index, int speed) {
|
||||||
|
animation_[index].counter = speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece si la animación se reproduce en bucle
|
||||||
|
void AnimatedSprite::setAnimationLoop(const std::string &name, int loop) {
|
||||||
|
animation_[getIndex(name)].loop = loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece si la animación se reproduce en bucle
|
||||||
|
void AnimatedSprite::setAnimationLoop(int index, int loop) {
|
||||||
|
animation_[index].loop = loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void AnimatedSprite::setAnimationCompleted(const std::string &name, bool value) {
|
||||||
|
animation_[getIndex(name)].completed = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OLD - Establece el valor de la variable
|
||||||
|
void AnimatedSprite::setAnimationCompleted(int index, bool value) {
|
||||||
|
animation_[index].completed = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si ha terminado la animación
|
||||||
|
auto AnimatedSprite::animationIsCompleted() -> bool {
|
||||||
|
return animation_[current_animation_].completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devuelve el rectangulo de una animación y frame concreto
|
||||||
|
auto AnimatedSprite::getAnimationClip(const std::string &name, Uint8 index) -> SDL_Rect {
|
||||||
|
return animation_[getIndex(name)].frames[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devuelve el rectangulo de una animación y frame concreto
|
||||||
|
auto AnimatedSprite::getAnimationClip(int index_a, Uint8 index_f) -> SDL_Rect {
|
||||||
|
return animation_[index_a].frames[index_f];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carga la animación desde un vector (reutiliza parseAnimationStream via stringstream)
|
||||||
|
auto AnimatedSprite::loadFromVector(const std::vector<std::string> *source) -> bool {
|
||||||
|
std::stringstream ss;
|
||||||
|
for (const auto &line : *source) {
|
||||||
|
ss << line << '\n';
|
||||||
|
}
|
||||||
|
AnimatedSpriteData as = parseAnimationStream(ss, texture_, "", false);
|
||||||
|
animation_.insert(animation_.end(), as.animations.begin(), as.animations.end());
|
||||||
|
|
||||||
|
// El primer frame lleva frame_width/frame_height en .w/.h — los usamos como rect por defecto
|
||||||
|
if (!as.animations.empty() && !as.animations.front().frames.empty()) {
|
||||||
|
const auto &first = as.animations.front().frames.front();
|
||||||
|
setRect({0, 0, first.w, first.h});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la animacion actual
|
||||||
|
void AnimatedSprite::setCurrentAnimation(const std::string &name) {
|
||||||
|
const int NEW_ANIMATION = getIndex(name);
|
||||||
|
if (current_animation_ != NEW_ANIMATION) {
|
||||||
|
current_animation_ = NEW_ANIMATION;
|
||||||
|
animation_[current_animation_].current_frame = 0;
|
||||||
|
animation_[current_animation_].counter = 0;
|
||||||
|
animation_[current_animation_].completed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la animacion actual
|
||||||
|
void AnimatedSprite::setCurrentAnimation(int index) {
|
||||||
|
const int NEW_ANIMATION = index;
|
||||||
|
if (current_animation_ != NEW_ANIMATION) {
|
||||||
|
current_animation_ = NEW_ANIMATION;
|
||||||
|
animation_[current_animation_].current_frame = 0;
|
||||||
|
animation_[current_animation_].counter = 0;
|
||||||
|
animation_[current_animation_].completed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// animate(dt) + MovingSprite::update(dt) (move + rotació)
|
||||||
|
void AnimatedSprite::update(float dt_s) {
|
||||||
|
animate(dt_s);
|
||||||
|
MovingSprite::update(dt_s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el rectangulo para un frame de una animación
|
||||||
|
void AnimatedSprite::setAnimationFrames(Uint8 index_animation, Uint8 index_frame, int x, int y, int w, int h) {
|
||||||
|
animation_[index_animation].frames.push_back({x, y, w, h});
|
||||||
|
}
|
||||||
|
|
||||||
|
// OLD - Establece el contador para todas las animaciones
|
||||||
|
void AnimatedSprite::setAnimationCounter(int value) {
|
||||||
|
for (auto &a : animation_) {
|
||||||
|
a.counter = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reinicia la animación
|
||||||
|
void AnimatedSprite::resetAnimation() {
|
||||||
|
animation_[current_animation_].current_frame = 0;
|
||||||
|
animation_[current_animation_].counter = 0;
|
||||||
|
animation_[current_animation_].completed = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string> // for string, basic_string
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include "core/rendering/movingsprite.h" // for MovingSprite
|
||||||
|
class Texture;
|
||||||
|
|
||||||
|
struct Animation {
|
||||||
|
std::string name; // Nombre de la animacion
|
||||||
|
std::vector<SDL_Rect> frames; // Cada uno de los frames que componen la animación
|
||||||
|
int speed; // Velocidad de la animación (frame-based: ticks per frame)
|
||||||
|
float step_duration_s; // Time-based: segons per frame d'animació (derivat de speed al parse: speed/60)
|
||||||
|
int loop; // Indica a que frame vuelve la animación al terminar. -1 para que no vuelva
|
||||||
|
bool completed; // Indica si ha finalizado la animación
|
||||||
|
int current_frame; // Frame actual
|
||||||
|
int counter; // Contador per a les animacions (frame-based)
|
||||||
|
float time_accumulator_s; // Acumulador de temps (time-based)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AnimatedSpriteData {
|
||||||
|
std::vector<Animation> animations; // Vector con las diferentes animaciones
|
||||||
|
Texture *texture; // Textura con los graficos para el sprite
|
||||||
|
};
|
||||||
|
|
||||||
|
// Carga la animación desde un fichero
|
||||||
|
auto loadAnimationFromFile(Texture *texture, const std::string &file_path, bool verbose = false) -> AnimatedSpriteData;
|
||||||
|
|
||||||
|
// Carga la animación desde bytes en memoria
|
||||||
|
auto loadAnimationFromMemory(Texture *texture, const std::vector<uint8_t> &bytes, const std::string &name_for_logs = "", bool verbose = false) -> AnimatedSpriteData;
|
||||||
|
|
||||||
|
class AnimatedSprite : public MovingSprite {
|
||||||
|
public:
|
||||||
|
explicit AnimatedSprite(Texture *texture = nullptr, SDL_Renderer *renderer = nullptr, const std::string &file = "", const std::vector<std::string> *buffer = nullptr); // Constructor
|
||||||
|
AnimatedSprite(SDL_Renderer *renderer, AnimatedSpriteData *data);
|
||||||
|
|
||||||
|
~AnimatedSprite() override; // Destructor
|
||||||
|
|
||||||
|
void animate(float dt_s); // Calcula el frame correspondiente a la animación actual
|
||||||
|
auto getNumFrames() -> int; // Obtiene el numero de frames de la animación actual
|
||||||
|
void setCurrentFrame(int num); // Establece el frame actual de la animación
|
||||||
|
void setAnimationCounter(const std::string &name, int num); // Establece el valor del contador
|
||||||
|
|
||||||
|
void setAnimationSpeed(const std::string &name, int speed); // Establece la velocidad de una animación
|
||||||
|
void setAnimationSpeed(int index, int speed);
|
||||||
|
|
||||||
|
void setAnimationLoop(const std::string &name, int loop); // Establece el frame al que vuelve la animación al finalizar
|
||||||
|
void setAnimationLoop(int index, int loop);
|
||||||
|
|
||||||
|
void setAnimationCompleted(const std::string &name, bool value); // Establece el valor de la variable
|
||||||
|
void setAnimationCompleted(int index, bool value);
|
||||||
|
|
||||||
|
auto animationIsCompleted() -> bool; // Comprueba si ha terminado la animación
|
||||||
|
|
||||||
|
auto getAnimationClip(const std::string &name = "default", Uint8 index = 0) -> SDL_Rect; // Devuelve el rectangulo de una animación y frame concreto
|
||||||
|
auto getAnimationClip(int index_a = 0, Uint8 index_f = 0) -> SDL_Rect;
|
||||||
|
|
||||||
|
auto getIndex(const std::string &name) -> int; // Obtiene el indice de la animación a partir del nombre
|
||||||
|
auto loadFromVector(const std::vector<std::string> *source) -> bool; // Carga la animación desde un vector
|
||||||
|
|
||||||
|
void setCurrentAnimation(const std::string &name = "default"); // Establece la animacion actual
|
||||||
|
void setCurrentAnimation(int index = 0);
|
||||||
|
|
||||||
|
void update(float dt_s) override; // Actualiza las variables del objeto
|
||||||
|
|
||||||
|
void setAnimationFrames(Uint8 index_animation, Uint8 index_frame, int x, int y, int w, int h); // OLD - Establece el rectangulo para un frame de una animación
|
||||||
|
void setAnimationCounter(int value); // OLD - Establece el contador para todas las animaciones
|
||||||
|
|
||||||
|
void resetAnimation(); // Reinicia la animación
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Variables
|
||||||
|
std::vector<Animation> animation_; // Vector con las diferentes animaciones
|
||||||
|
int current_animation_; // Animacion activa
|
||||||
|
};
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
#include "core/rendering/fade.h"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdlib> // for rand
|
||||||
|
#include <iostream> // for char_traits, basic_ostream, operator<<
|
||||||
|
|
||||||
|
#include "game/defaults.hpp" // for GAMECANVAS_HEIGHT, GAMECANVAS_WIDTH
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Fade::Fade(SDL_Renderer *renderer)
|
||||||
|
: renderer_(renderer) {
|
||||||
|
backbuffer_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT);
|
||||||
|
if (backbuffer_ != nullptr) {
|
||||||
|
SDL_SetTextureScaleMode(backbuffer_, SDL_SCALEMODE_NEAREST);
|
||||||
|
}
|
||||||
|
if (backbuffer_ == nullptr) {
|
||||||
|
std::cout << "Error: textTexture could not be created!\nSDL Error: " << SDL_GetError() << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
Fade::~Fade() {
|
||||||
|
SDL_DestroyTexture(backbuffer_);
|
||||||
|
backbuffer_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inicializa las variables
|
||||||
|
void Fade::init(Uint8 r, Uint8 g, Uint8 b) {
|
||||||
|
fade_type_ = Type::CENTER;
|
||||||
|
enabled_ = false;
|
||||||
|
finished_ = false;
|
||||||
|
counter_ = 0;
|
||||||
|
elapsed_s_ = 0.0F;
|
||||||
|
r_ = r;
|
||||||
|
g_ = g;
|
||||||
|
b_ = b;
|
||||||
|
r_original_ = r;
|
||||||
|
g_original_ = g;
|
||||||
|
b_original_ = b;
|
||||||
|
last_square_ticks_ = 0;
|
||||||
|
squares_drawn_ = 0;
|
||||||
|
fullscreen_done_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pinta una transición en pantalla
|
||||||
|
void Fade::render() {
|
||||||
|
if (enabled_ && !finished_) {
|
||||||
|
switch (fade_type_) {
|
||||||
|
case Type::FULLSCREEN:
|
||||||
|
renderFadeFullscreen();
|
||||||
|
break;
|
||||||
|
case Type::CENTER:
|
||||||
|
renderFadeCenter();
|
||||||
|
break;
|
||||||
|
case Type::RANDOM_SQUARE:
|
||||||
|
renderFadeRandomSquare();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finished_) {
|
||||||
|
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, 255);
|
||||||
|
SDL_RenderClear(renderer_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper de render: tipo FULLSCREEN
|
||||||
|
void Fade::renderFadeFullscreen() {
|
||||||
|
if (fullscreen_done_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int ALPHA = counter_ * 4;
|
||||||
|
if (ALPHA >= 255) {
|
||||||
|
fullscreen_done_ = true;
|
||||||
|
|
||||||
|
// Deja todos los buffers del mismo color
|
||||||
|
SDL_SetRenderTarget(renderer_, backbuffer_);
|
||||||
|
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, 255);
|
||||||
|
SDL_RenderClear(renderer_);
|
||||||
|
|
||||||
|
SDL_SetRenderTarget(renderer_, nullptr);
|
||||||
|
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, 255);
|
||||||
|
SDL_RenderClear(renderer_);
|
||||||
|
|
||||||
|
finished_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dibujamos sobre el renderizador
|
||||||
|
SDL_SetRenderTarget(renderer_, nullptr);
|
||||||
|
|
||||||
|
// Copia el backbuffer con la imagen que había al renderizador
|
||||||
|
SDL_RenderTexture(renderer_, backbuffer_, nullptr, nullptr);
|
||||||
|
|
||||||
|
SDL_FRect f_rect1 = {0, 0, (float)GAMECANVAS_WIDTH, (float)GAMECANVAS_HEIGHT};
|
||||||
|
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, ALPHA);
|
||||||
|
SDL_RenderFillRect(renderer_, &f_rect1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper de render: tipo CENTER
|
||||||
|
void Fade::renderFadeCenter() {
|
||||||
|
SDL_FRect f_r1 = {0, 0, (float)GAMECANVAS_WIDTH, 0};
|
||||||
|
SDL_FRect f_r2 = {0, 0, (float)GAMECANVAS_WIDTH, 0};
|
||||||
|
|
||||||
|
SDL_SetRenderDrawColor(renderer_, r_, g_, b_, 64);
|
||||||
|
|
||||||
|
for (int i = 0; i < counter_; i++) {
|
||||||
|
f_r1.h = f_r2.h = (float)(i * 4);
|
||||||
|
f_r2.y = (float)(GAMECANVAS_HEIGHT - (i * 4));
|
||||||
|
|
||||||
|
SDL_RenderFillRect(renderer_, &f_r1);
|
||||||
|
SDL_RenderFillRect(renderer_, &f_r2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((counter_ * 4) > GAMECANVAS_HEIGHT) {
|
||||||
|
finished_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper de render: tipo RANDOM_SQUARE
|
||||||
|
void Fade::renderFadeRandomSquare() {
|
||||||
|
const Uint32 NOW = SDL_GetTicks();
|
||||||
|
if (squares_drawn_ < 50 && NOW - last_square_ticks_ >= 100) {
|
||||||
|
last_square_ticks_ = NOW;
|
||||||
|
|
||||||
|
SDL_FRect f_rs = {0, 0, 32, 32};
|
||||||
|
|
||||||
|
// Crea un color al azar
|
||||||
|
const Uint8 R = 255 * (rand() % 2);
|
||||||
|
const Uint8 G = 255 * (rand() % 2);
|
||||||
|
const Uint8 B = 255 * (rand() % 2);
|
||||||
|
SDL_SetRenderDrawColor(renderer_, R, G, B, 64);
|
||||||
|
|
||||||
|
// Dibujamos sobre el backbuffer
|
||||||
|
SDL_SetRenderTarget(renderer_, backbuffer_);
|
||||||
|
|
||||||
|
f_rs.x = (float)(rand() % (GAMECANVAS_WIDTH - 32));
|
||||||
|
f_rs.y = (float)(rand() % (GAMECANVAS_HEIGHT - 32));
|
||||||
|
SDL_RenderFillRect(renderer_, &f_rs);
|
||||||
|
|
||||||
|
// Volvemos a usar el renderizador de forma normal
|
||||||
|
SDL_SetRenderTarget(renderer_, nullptr);
|
||||||
|
|
||||||
|
squares_drawn_++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copiamos el backbuffer al renderizador
|
||||||
|
SDL_RenderTexture(renderer_, backbuffer_, nullptr, nullptr);
|
||||||
|
|
||||||
|
if (squares_drawn_ >= 50) {
|
||||||
|
finished_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualiza les variables internes. `counter_` (Uint16, frames a la cadència
|
||||||
|
// de referència 60Hz) es deriva de `elapsed_s_` perquè els helpers de
|
||||||
|
// `render()` (renderFadeFullscreen / Center / RandomSquare) segueixin
|
||||||
|
// llegint-lo igual que abans.
|
||||||
|
void Fade::update(float dt_s) {
|
||||||
|
if (!enabled_) { return; }
|
||||||
|
elapsed_s_ += dt_s;
|
||||||
|
constexpr float FADE_STEPS_PER_S = 60.0F;
|
||||||
|
counter_ = static_cast<Uint16>(elapsed_s_ * FADE_STEPS_PER_S);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activa el fade
|
||||||
|
void Fade::activateFade() {
|
||||||
|
enabled_ = true;
|
||||||
|
finished_ = false;
|
||||||
|
counter_ = 0;
|
||||||
|
elapsed_s_ = 0.0F;
|
||||||
|
squares_drawn_ = 0;
|
||||||
|
last_square_ticks_ = 0;
|
||||||
|
fullscreen_done_ = false;
|
||||||
|
r_ = r_original_;
|
||||||
|
g_ = g_original_;
|
||||||
|
b_ = b_original_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si está activo
|
||||||
|
auto Fade::isEnabled() const -> bool {
|
||||||
|
return enabled_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si ha terminado la transicion
|
||||||
|
auto Fade::hasEnded() const -> bool {
|
||||||
|
return finished_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el tipo de fade
|
||||||
|
void Fade::setFadeType(Type fade_type) {
|
||||||
|
fade_type_ = fade_type;
|
||||||
|
}
|
||||||
@@ -0,0 +1,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,54 @@
|
|||||||
|
#include "core/rendering/notifications.hpp"
|
||||||
|
|
||||||
|
#include "core/rendering/screen.h"
|
||||||
|
#include "utils/utils.h"
|
||||||
|
|
||||||
|
namespace Notifications {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Paleta pastel. Per a tunejar l'aparença només cal tocar aquí.
|
||||||
|
// (Color no és literal type ⇒ const, no constexpr.)
|
||||||
|
const Color INFO_COLOR{0xF0, 0xE0, 0x90}; // groc trigo
|
||||||
|
const Color TOGGLE_COLOR{0xA0, 0xE0, 0xF0}; // cian gel
|
||||||
|
const Color CHOICE_COLOR{0xE0, 0xA0, 0xE0}; // rosa orquídia
|
||||||
|
const Color SUCCESS_COLOR{0xB0, 0xE6, 0xB0}; // verd menta
|
||||||
|
const Color DANGER_COLOR{0xF0, 0xA0, 0xA0}; // rosa salmó
|
||||||
|
|
||||||
|
// 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,759 @@
|
|||||||
|
#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() {
|
||||||
|
// Dibuja la notificación activa sobre el gameCanvas antes de presentar
|
||||||
|
SDL_SetRenderTarget(renderer_, game_canvas_);
|
||||||
|
renderNotification();
|
||||||
|
|
||||||
|
#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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el escalado entero
|
||||||
|
void Screen::setIntegerScale(bool enabled) {
|
||||||
|
if (Options::video.integer_scale == enabled) { return; }
|
||||||
|
Options::video.integer_scale = enabled;
|
||||||
|
setVideoMode(Options::video.fullscreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alterna el escalado entero
|
||||||
|
void Screen::toggleIntegerScale() {
|
||||||
|
setIntegerScale(!Options::video.integer_scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 rectángulo dest para fullscreen: integer_scale / aspect ratio
|
||||||
|
void Screen::computeFullscreenGameRect() {
|
||||||
|
if (Options::video.integer_scale) {
|
||||||
|
// Calcula el tamaño de la escala máxima
|
||||||
|
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;
|
||||||
|
dest_.x = (window_width_ - dest_.w) / 2;
|
||||||
|
dest_.y = (window_height_ - dest_.h) / 2;
|
||||||
|
} else {
|
||||||
|
// Manté la relació d'aspecte sense escalat enter (letterbox/pillarbox).
|
||||||
|
float ratio = (float)game_canvas_width_ / (float)game_canvas_height_;
|
||||||
|
if ((window_width_ - game_canvas_width_) >= (window_height_ - game_canvas_height_)) {
|
||||||
|
dest_.h = window_height_;
|
||||||
|
dest_.w = static_cast<int>(std::lround(window_height_ * ratio));
|
||||||
|
dest_.x = (window_width_ - dest_.w) / 2;
|
||||||
|
dest_.y = (window_height_ - dest_.h) / 2;
|
||||||
|
} else {
|
||||||
|
dest_.w = window_width_;
|
||||||
|
dest_.h = static_cast<int>(std::lround(window_width_ / ratio));
|
||||||
|
dest_.x = (window_width_ - dest_.w) / 2;
|
||||||
|
dest_.y = (window_height_ - dest_.h) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplica la logical presentation y persiste el estado en options
|
||||||
|
void Screen::applyLogicalPresentation(bool fullscreen) {
|
||||||
|
SDL_SetRenderLogicalPresentation(renderer_, window_width_, window_height_, SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
||||||
|
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
|
||||||
|
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_,
|
||||||
|
notification_message_,
|
||||||
|
1,
|
||||||
|
notification_text_color_,
|
||||||
|
1,
|
||||||
|
notification_outline_color_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 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_->setScaleMode(Options::video.integer_scale);
|
||||||
|
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,147 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <memory> // for unique_ptr
|
||||||
|
#include <string> // for string
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#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 toggleIntegerScale(); // Alterna el escalado entero
|
||||||
|
void setIntegerScale(bool enabled); // Establece el escalado entero
|
||||||
|
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
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
|
#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,624 @@
|
|||||||
|
#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 {
|
||||||
|
float vw = 0.0F;
|
||||||
|
float vh = 0.0F;
|
||||||
|
if (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);
|
||||||
|
} else {
|
||||||
|
const float SCALE = std::min(
|
||||||
|
static_cast<float>(sw) / static_cast<float>(game_width_),
|
||||||
|
static_cast<float>(sh) / static_cast<float>(game_height_));
|
||||||
|
vw = static_cast<float>(game_width_) * SCALE;
|
||||||
|
vh = static_cast<float>(game_height_) * SCALE;
|
||||||
|
}
|
||||||
|
const float VX = std::floor((static_cast<float>(sw) - vw) * 0.5F);
|
||||||
|
const float VY = std::floor((static_cast<float>(sh) - 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::setScaleMode(bool integer_scale) {
|
||||||
|
integer_scale_ = integer_scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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,169 @@
|
|||||||
|
#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;
|
||||||
|
|
||||||
|
// Activa/desactiva escalado entero (integer scale)
|
||||||
|
void setScaleMode(bool integer_scale) 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, .pixel_scale = 1.0F, .chroma_max = 0.15F, .scan_dark_ratio = 0.333F, .scan_dark_floor = 0.42F, .scan_edge_soft = 1.0F};
|
||||||
|
CrtPiUniforms crtpi_uniforms_{.scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = 1, .enable_multisample = 1, .enable_gamma = 1};
|
||||||
|
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;
|
||||||
|
bool integer_scale_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // 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,154 @@
|
|||||||
|
#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 Activa o desactiva el escalado entero (integer scale)
|
||||||
|
*/
|
||||||
|
virtual void setScaleMode(bool /*integer_scale*/) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#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
|
||||||
|
int 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,265 @@
|
|||||||
|
#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 == "demo.bin") {
|
||||||
|
demo_bytes_ = 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,57 @@
|
|||||||
|
#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", ...
|
||||||
|
auto getDemoBytes() const -> const std::vector<uint8_t> & { return demo_bytes_; }
|
||||||
|
|
||||||
|
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<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,681 @@
|
|||||||
|
#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);
|
||||||
|
|
||||||
|
// 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/demo.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 = {.st_dev = 0};
|
||||||
|
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,696 +0,0 @@
|
|||||||
#include "director.h"
|
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
#include <errno.h> // for errno, EEXIST, EACCES, ENAMETOO...
|
|
||||||
#include <stdio.h> // for printf, perror
|
|
||||||
#include <string.h> // for strcmp
|
|
||||||
#include <sys/stat.h> // for mkdir, stat, S_IRWXU
|
|
||||||
#include <unistd.h> // for getuid
|
|
||||||
|
|
||||||
#include <cstdlib> // for exit, EXIT_FAILURE, srand
|
|
||||||
#include <fstream> // for basic_ostream, operator<<, basi...
|
|
||||||
#include <iostream> // for cout
|
|
||||||
#include <memory>
|
|
||||||
#include <string> // for basic_string, operator+, char_t...
|
|
||||||
#include <vector> // for vector
|
|
||||||
|
|
||||||
#include "asset.h" // for Asset, assetType
|
|
||||||
#include "const.h" // for SECTION_PROG_LOGO, GAMECANVAS_H...
|
|
||||||
#include "game.h" // for Game
|
|
||||||
#include "input.h" // for Input, inputs_e, INPUT_USE_GAME...
|
|
||||||
#include "intro.h" // for Intro
|
|
||||||
#include "jail_audio.hpp" // for JA_Init
|
|
||||||
#include "lang.h" // for Lang, MAX_LANGUAGES, ba_BA, en_UK
|
|
||||||
#include "logo.h" // for Logo
|
|
||||||
#include "screen.h" // for FILTER_NEAREST, Screen, FILTER_...
|
|
||||||
#include "texture.h" // for Texture
|
|
||||||
#include "title.h" // for Title
|
|
||||||
#include "utils.h" // for options_t, input_t, boolToString
|
|
||||||
|
|
||||||
#ifndef _WIN32
|
|
||||||
#include <pwd.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Constructor
|
|
||||||
Director::Director(int argc, const char *argv[]) {
|
|
||||||
std::cout << "Game start" << std::endl;
|
|
||||||
// Inicializa variables
|
|
||||||
section = new section_t();
|
|
||||||
section->name = SECTION_PROG_LOGO;
|
|
||||||
|
|
||||||
// Inicializa las opciones del programa
|
|
||||||
initOptions();
|
|
||||||
|
|
||||||
// Comprueba los parametros del programa
|
|
||||||
checkProgramArguments(argc, argv);
|
|
||||||
|
|
||||||
// Crea la carpeta del sistema donde guardar datos
|
|
||||||
createSystemFolder("jailgames");
|
|
||||||
#ifndef DEBUG
|
|
||||||
createSystemFolder("jailgames/coffee_crisis");
|
|
||||||
#else
|
|
||||||
createSystemFolder("jailgames/coffee_crisis_debug");
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Crea el objeto que controla los ficheros de recursos
|
|
||||||
asset = new Asset(executablePath);
|
|
||||||
asset->setVerbose(options->console);
|
|
||||||
|
|
||||||
// Si falta algún fichero no inicia el programa
|
|
||||||
if (!setFileList()) {
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carga el fichero de configuración
|
|
||||||
loadConfigFile();
|
|
||||||
|
|
||||||
// Inicializa SDL
|
|
||||||
initSDL();
|
|
||||||
|
|
||||||
// Inicializa JailAudio
|
|
||||||
initJailAudio();
|
|
||||||
|
|
||||||
// Establece el modo de escalado de texturas
|
|
||||||
Texture::setGlobalScaleMode(options->filter == FILTER_NEAREST ? SDL_SCALEMODE_NEAREST : SDL_SCALEMODE_LINEAR);
|
|
||||||
|
|
||||||
// Crea los objetos
|
|
||||||
lang = new Lang(asset);
|
|
||||||
lang->setLang(options->language);
|
|
||||||
|
|
||||||
input = new Input(asset->get("gamecontrollerdb.txt"));
|
|
||||||
initInput();
|
|
||||||
|
|
||||||
screen = new Screen(window, renderer, asset, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
Director::~Director() {
|
|
||||||
saveConfigFile();
|
|
||||||
|
|
||||||
delete asset;
|
|
||||||
delete input;
|
|
||||||
delete screen;
|
|
||||||
delete lang;
|
|
||||||
delete options;
|
|
||||||
delete section;
|
|
||||||
|
|
||||||
SDL_DestroyRenderer(renderer);
|
|
||||||
SDL_DestroyWindow(window);
|
|
||||||
|
|
||||||
SDL_Quit();
|
|
||||||
|
|
||||||
std::cout << "\nBye!" << std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inicializa el objeto input
|
|
||||||
void Director::initInput() {
|
|
||||||
// Establece si ha de mostrar mensajes
|
|
||||||
input->setVerbose(options->console);
|
|
||||||
|
|
||||||
// Busca si hay un mando conectado
|
|
||||||
input->discoverGameController();
|
|
||||||
|
|
||||||
// Teclado - Movimiento del jugador
|
|
||||||
input->bindKey(input_up, SDL_SCANCODE_UP);
|
|
||||||
input->bindKey(input_down, SDL_SCANCODE_DOWN);
|
|
||||||
input->bindKey(input_left, SDL_SCANCODE_LEFT);
|
|
||||||
input->bindKey(input_right, SDL_SCANCODE_RIGHT);
|
|
||||||
input->bindKey(input_fire_left, SDL_SCANCODE_Q);
|
|
||||||
input->bindKey(input_fire_center, SDL_SCANCODE_W);
|
|
||||||
input->bindKey(input_fire_right, SDL_SCANCODE_E);
|
|
||||||
|
|
||||||
// Teclado - Otros
|
|
||||||
input->bindKey(input_accept, SDL_SCANCODE_RETURN);
|
|
||||||
input->bindKey(input_cancel, SDL_SCANCODE_ESCAPE);
|
|
||||||
input->bindKey(input_pause, SDL_SCANCODE_ESCAPE);
|
|
||||||
input->bindKey(input_exit, SDL_SCANCODE_ESCAPE);
|
|
||||||
input->bindKey(input_window_dec_size, SDL_SCANCODE_F1);
|
|
||||||
input->bindKey(input_window_inc_size, SDL_SCANCODE_F2);
|
|
||||||
input->bindKey(input_window_fullscreen, SDL_SCANCODE_F3);
|
|
||||||
|
|
||||||
// Mando - Movimiento del jugador
|
|
||||||
input->bindGameControllerButton(input_up, SDL_GAMEPAD_BUTTON_DPAD_UP);
|
|
||||||
input->bindGameControllerButton(input_down, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
|
|
||||||
input->bindGameControllerButton(input_left, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
|
||||||
input->bindGameControllerButton(input_right, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
|
|
||||||
input->bindGameControllerButton(input_fire_left, SDL_GAMEPAD_BUTTON_WEST);
|
|
||||||
input->bindGameControllerButton(input_fire_center, SDL_GAMEPAD_BUTTON_NORTH);
|
|
||||||
input->bindGameControllerButton(input_fire_right, SDL_GAMEPAD_BUTTON_EAST);
|
|
||||||
|
|
||||||
// Mando - Otros
|
|
||||||
input->bindGameControllerButton(input_accept, SDL_GAMEPAD_BUTTON_EAST);
|
|
||||||
input->bindGameControllerButton(input_cancel, SDL_GAMEPAD_BUTTON_SOUTH);
|
|
||||||
#ifdef GAME_CONSOLE
|
|
||||||
input->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_BACK);
|
|
||||||
input->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_START);
|
|
||||||
#else
|
|
||||||
input->bindGameControllerButton(input_pause, SDL_GAMEPAD_BUTTON_START);
|
|
||||||
input->bindGameControllerButton(input_exit, SDL_GAMEPAD_BUTTON_BACK);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inicializa JailAudio
|
|
||||||
void Director::initJailAudio() {
|
|
||||||
JA_Init(48000, SDL_AUDIO_S16, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arranca SDL y crea la ventana
|
|
||||||
bool Director::initSDL() {
|
|
||||||
// Indicador de éxito
|
|
||||||
bool success = true;
|
|
||||||
|
|
||||||
// Inicializa SDL
|
|
||||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMEPAD)) {
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << "SDL could not initialize!\nSDL Error: " << SDL_GetError() << std::endl;
|
|
||||||
}
|
|
||||||
success = false;
|
|
||||||
} else {
|
|
||||||
// Inicia el generador de numeros aleatorios
|
|
||||||
std::srand(static_cast<unsigned int>(SDL_GetTicks()));
|
|
||||||
|
|
||||||
// Crea la ventana
|
|
||||||
int incW = 0;
|
|
||||||
int incH = 0;
|
|
||||||
if (options->borderEnabled) {
|
|
||||||
incW = options->borderWidth * 2;
|
|
||||||
incH = options->borderHeight * 2;
|
|
||||||
}
|
|
||||||
window = SDL_CreateWindow(WINDOW_CAPTION, (options->gameWidth + incW) * options->windowSize, (options->gameHeight + incH) * options->windowSize, 0);
|
|
||||||
if (window == nullptr) {
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << "Window could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
|
|
||||||
}
|
|
||||||
success = false;
|
|
||||||
} else {
|
|
||||||
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
|
||||||
|
|
||||||
// Crea un renderizador para la ventana
|
|
||||||
renderer = SDL_CreateRenderer(window, NULL);
|
|
||||||
|
|
||||||
if (renderer == nullptr) {
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << "Renderer could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
|
|
||||||
}
|
|
||||||
success = false;
|
|
||||||
} else {
|
|
||||||
// Activa vsync si es necesario
|
|
||||||
if (options->vSync) {
|
|
||||||
SDL_SetRenderVSync(renderer, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inicializa el color de renderizado
|
|
||||||
SDL_SetRenderDrawColor(renderer, 0x00, 0x00, 0x00, 0xFF);
|
|
||||||
|
|
||||||
// Establece el tamaño del buffer de renderizado
|
|
||||||
SDL_SetRenderLogicalPresentation(renderer, options->gameWidth, options->gameHeight, SDL_LOGICAL_PRESENTATION_LETTERBOX);
|
|
||||||
|
|
||||||
// Establece el modo de mezcla
|
|
||||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << std::endl;
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crea el indice de ficheros
|
|
||||||
bool Director::setFileList() {
|
|
||||||
#ifdef MACOS_BUNDLE
|
|
||||||
const std::string prefix = "/../Resources";
|
|
||||||
#else
|
|
||||||
const std::string prefix = "";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Ficheros de configuración
|
|
||||||
asset->add(systemFolder + "/config.txt", t_data, false, true);
|
|
||||||
asset->add(systemFolder + "/score.bin", t_data, false, true);
|
|
||||||
asset->add(prefix + "/data/config/demo.bin", t_data);
|
|
||||||
asset->add(prefix + "/data/config/gamecontrollerdb.txt", t_data);
|
|
||||||
|
|
||||||
// Musicas
|
|
||||||
asset->add(prefix + "/data/music/intro.ogg", t_music);
|
|
||||||
asset->add(prefix + "/data/music/playing.ogg", t_music);
|
|
||||||
asset->add(prefix + "/data/music/title.ogg", t_music);
|
|
||||||
|
|
||||||
// Sonidos
|
|
||||||
asset->add(prefix + "/data/sound/balloon.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/bubble1.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/bubble2.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/bubble3.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/bubble4.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/bullet.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/coffeeout.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/hiscore.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/itemdrop.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/itempickup.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/menu_move.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/menu_select.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/player_collision.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/stage_change.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/title.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/clock.wav", t_sound);
|
|
||||||
asset->add(prefix + "/data/sound/powerball.wav", t_sound);
|
|
||||||
|
|
||||||
// Texturas
|
|
||||||
asset->add(prefix + "/data/gfx/balloon1.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/balloon1.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/balloon2.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/balloon2.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/balloon3.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/balloon3.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/balloon4.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/balloon4.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/bullet.png", t_bitmap);
|
|
||||||
|
|
||||||
asset->add(prefix + "/data/gfx/game_buildings.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/game_clouds.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/game_grass.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/game_power_meter.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/game_sky_colors.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/game_text.png", t_bitmap);
|
|
||||||
|
|
||||||
asset->add(prefix + "/data/gfx/intro.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/logo.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/menu_game_over.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/menu_game_over_end.png", t_bitmap);
|
|
||||||
|
|
||||||
asset->add(prefix + "/data/gfx/item_points1_disk.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/item_points1_disk.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/item_points2_gavina.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/item_points2_gavina.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/item_points3_pacmar.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/item_points3_pacmar.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/item_clock.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/item_clock.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/item_coffee.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/item_coffee.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/item_coffee_machine.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/item_coffee_machine.ani", t_data);
|
|
||||||
|
|
||||||
asset->add(prefix + "/data/gfx/title_bg_tile.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/title_coffee.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/title_crisis.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/title_dust.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/title_dust.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/title_gradient.png", t_bitmap);
|
|
||||||
|
|
||||||
asset->add(prefix + "/data/gfx/player_head.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/player_body.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/player_legs.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/player_death.ani", t_data);
|
|
||||||
asset->add(prefix + "/data/gfx/player_fire.ani", t_data);
|
|
||||||
|
|
||||||
asset->add(prefix + "/data/gfx/player_bal1_head.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/player_bal1_body.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/player_bal1_legs.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/player_bal1_death.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/player_bal1_fire.png", t_bitmap);
|
|
||||||
|
|
||||||
asset->add(prefix + "/data/gfx/player_arounder_head.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/player_arounder_body.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/player_arounder_legs.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/player_arounder_death.png", t_bitmap);
|
|
||||||
asset->add(prefix + "/data/gfx/player_arounder_fire.png", t_bitmap);
|
|
||||||
|
|
||||||
// Fuentes
|
|
||||||
asset->add(prefix + "/data/font/8bithud.png", t_font);
|
|
||||||
asset->add(prefix + "/data/font/8bithud.txt", t_font);
|
|
||||||
asset->add(prefix + "/data/font/nokia.png", t_font);
|
|
||||||
asset->add(prefix + "/data/font/nokia_big2.png", t_font);
|
|
||||||
asset->add(prefix + "/data/font/nokia.txt", t_font);
|
|
||||||
asset->add(prefix + "/data/font/nokia2.png", t_font);
|
|
||||||
asset->add(prefix + "/data/font/nokia2.txt", t_font);
|
|
||||||
asset->add(prefix + "/data/font/nokia_big2.txt", t_font);
|
|
||||||
asset->add(prefix + "/data/font/smb2_big.png", t_font);
|
|
||||||
asset->add(prefix + "/data/font/smb2_big.txt", t_font);
|
|
||||||
asset->add(prefix + "/data/font/smb2.png", t_font);
|
|
||||||
asset->add(prefix + "/data/font/smb2.txt", t_font);
|
|
||||||
|
|
||||||
// Textos
|
|
||||||
asset->add(prefix + "/data/lang/es_ES.txt", t_lang);
|
|
||||||
asset->add(prefix + "/data/lang/en_UK.txt", t_lang);
|
|
||||||
asset->add(prefix + "/data/lang/ba_BA.txt", t_lang);
|
|
||||||
|
|
||||||
// Menus
|
|
||||||
asset->add(prefix + "/data/menu/title.men", t_data);
|
|
||||||
asset->add(prefix + "/data/menu/title_gc.men", t_data);
|
|
||||||
asset->add(prefix + "/data/menu/options.men", t_data);
|
|
||||||
asset->add(prefix + "/data/menu/options_gc.men", t_data);
|
|
||||||
asset->add(prefix + "/data/menu/pause.men", t_data);
|
|
||||||
asset->add(prefix + "/data/menu/gameover.men", t_data);
|
|
||||||
asset->add(prefix + "/data/menu/player_select.men", t_data);
|
|
||||||
|
|
||||||
return asset->check();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inicializa las opciones del programa
|
|
||||||
void Director::initOptions() {
|
|
||||||
// Crea el puntero a la estructura de opciones
|
|
||||||
options = new options_t;
|
|
||||||
|
|
||||||
// Pone unos valores por defecto para las opciones de control
|
|
||||||
options->input.clear();
|
|
||||||
|
|
||||||
input_t inp;
|
|
||||||
inp.id = 0;
|
|
||||||
inp.name = "KEYBOARD";
|
|
||||||
inp.deviceType = INPUT_USE_KEYBOARD;
|
|
||||||
options->input.push_back(inp);
|
|
||||||
|
|
||||||
inp.id = 0;
|
|
||||||
inp.name = "GAME CONTROLLER";
|
|
||||||
inp.deviceType = INPUT_USE_GAMECONTROLLER;
|
|
||||||
options->input.push_back(inp);
|
|
||||||
|
|
||||||
// Opciones de video
|
|
||||||
options->gameWidth = GAMECANVAS_WIDTH;
|
|
||||||
options->gameHeight = GAMECANVAS_HEIGHT;
|
|
||||||
options->videoMode = 0;
|
|
||||||
options->windowSize = 3;
|
|
||||||
options->filter = FILTER_NEAREST;
|
|
||||||
options->vSync = true;
|
|
||||||
options->integerScale = true;
|
|
||||||
options->keepAspect = true;
|
|
||||||
options->borderWidth = 0;
|
|
||||||
options->borderHeight = 0;
|
|
||||||
options->borderEnabled = false;
|
|
||||||
|
|
||||||
// Opciones varios
|
|
||||||
options->playerSelected = 0;
|
|
||||||
options->difficulty = DIFFICULTY_NORMAL;
|
|
||||||
options->language = ba_BA;
|
|
||||||
options->console = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprueba los parametros del programa
|
|
||||||
void Director::checkProgramArguments(int argc, const char *argv[]) {
|
|
||||||
// Establece la ruta del programa
|
|
||||||
executablePath = argv[0];
|
|
||||||
|
|
||||||
// Comprueba el resto de parametros
|
|
||||||
for (int i = 1; i < argc; ++i) {
|
|
||||||
if (strcmp(argv[i], "--console") == 0) {
|
|
||||||
options->console = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crea la carpeta del sistema donde guardar datos
|
|
||||||
void Director::createSystemFolder(const std::string &folder) {
|
|
||||||
#ifdef _WIN32
|
|
||||||
systemFolder = std::string(getenv("APPDATA")) + "/" + folder;
|
|
||||||
#elif __APPLE__
|
|
||||||
struct passwd *pw = getpwuid(getuid());
|
|
||||||
const char *homedir = pw->pw_dir;
|
|
||||||
systemFolder = std::string(homedir) + "/Library/Application Support" + "/" + folder;
|
|
||||||
#elif __linux__
|
|
||||||
struct passwd *pw = getpwuid(getuid());
|
|
||||||
const char *homedir = pw->pw_dir;
|
|
||||||
systemFolder = std::string(homedir) + "/.config/" + folder;
|
|
||||||
|
|
||||||
{
|
|
||||||
// Intenta crear ".config", per si no existeix
|
|
||||||
std::string config_base_folder = std::string(homedir) + "/.config";
|
|
||||||
int ret = mkdir(config_base_folder.c_str(), S_IRWXU);
|
|
||||||
if (ret == -1 && errno != EEXIST) {
|
|
||||||
printf("ERROR CREATING CONFIG BASE FOLDER.");
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct stat st = {0};
|
|
||||||
if (stat(systemFolder.c_str(), &st) == -1) {
|
|
||||||
errno = 0;
|
|
||||||
#ifdef _WIN32
|
|
||||||
int ret = mkdir(systemFolder.c_str());
|
|
||||||
#else
|
|
||||||
int ret = mkdir(systemFolder.c_str(), S_IRWXU);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (ret == -1) {
|
|
||||||
switch (errno) {
|
|
||||||
case EACCES:
|
|
||||||
printf("the parent directory does not allow write");
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
|
|
||||||
case EEXIST:
|
|
||||||
printf("pathname already exists");
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
|
|
||||||
case ENAMETOOLONG:
|
|
||||||
printf("pathname is too long");
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
|
|
||||||
default:
|
|
||||||
perror("mkdir");
|
|
||||||
exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carga el fichero de configuración
|
|
||||||
bool Director::loadConfigFile() {
|
|
||||||
// Indicador de éxito en la carga
|
|
||||||
bool success = true;
|
|
||||||
|
|
||||||
// Variables para manejar el fichero
|
|
||||||
const std::string filePath = "config.txt";
|
|
||||||
std::string line;
|
|
||||||
std::ifstream file(asset->get(filePath));
|
|
||||||
|
|
||||||
// Si el fichero se puede abrir
|
|
||||||
if (file.good()) {
|
|
||||||
// Procesa el fichero linea a linea
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << "Reading file " << filePath << std::endl;
|
|
||||||
}
|
|
||||||
while (std::getline(file, line)) {
|
|
||||||
// Comprueba que la linea no sea un comentario
|
|
||||||
if (line.substr(0, 1) != "#") {
|
|
||||||
// Encuentra la posición del caracter '='
|
|
||||||
int pos = line.find("=");
|
|
||||||
// Procesa las dos subcadenas
|
|
||||||
if (!setOptions(options, line.substr(0, pos), line.substr(pos + 1, line.length()))) {
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << "Warning: file " << filePath << std::endl;
|
|
||||||
std::cout << "Unknown parameter " << line.substr(0, pos).c_str() << std::endl;
|
|
||||||
}
|
|
||||||
success = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cierra el fichero
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << "Closing file " << filePath << std::endl;
|
|
||||||
}
|
|
||||||
file.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// El fichero no existe
|
|
||||||
else { // Crea el fichero con los valores por defecto
|
|
||||||
saveConfigFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normaliza los valores
|
|
||||||
if (options->videoMode != 0 && options->videoMode != SDL_WINDOW_FULLSCREEN) {
|
|
||||||
options->videoMode = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options->windowSize < 1 || options->windowSize > 4) {
|
|
||||||
options->windowSize = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options->language < 0 || options->language > MAX_LANGUAGES) {
|
|
||||||
options->language = en_UK;
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Guarda el fichero de configuración
|
|
||||||
bool Director::saveConfigFile() {
|
|
||||||
bool success = true;
|
|
||||||
|
|
||||||
// Crea y abre el fichero de texto
|
|
||||||
std::ofstream file(asset->get("config.txt"));
|
|
||||||
|
|
||||||
if (file.good()) {
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << asset->get("config.txt") << " open for writing" << std::endl;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (options->console) {
|
|
||||||
std::cout << asset->get("config.txt") << " can't be opened" << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opciones g´raficas
|
|
||||||
file << "## VISUAL OPTIONS\n";
|
|
||||||
if (options->videoMode == 0) {
|
|
||||||
file << "videoMode=0\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (options->videoMode == SDL_WINDOW_FULLSCREEN) {
|
|
||||||
file << "videoMode=SDL_WINDOW_FULLSCREEN\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
file << "windowSize=" + std::to_string(options->windowSize) + "\n";
|
|
||||||
|
|
||||||
if (options->filter == FILTER_NEAREST) {
|
|
||||||
file << "filter=FILTER_NEAREST\n";
|
|
||||||
} else {
|
|
||||||
file << "filter=FILTER_LINEAL\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
file << "vSync=" + boolToString(options->vSync) + "\n";
|
|
||||||
file << "integerScale=" + boolToString(options->integerScale) + "\n";
|
|
||||||
file << "keepAspect=" + boolToString(options->keepAspect) + "\n";
|
|
||||||
file << "borderEnabled=" + boolToString(options->borderEnabled) + "\n";
|
|
||||||
file << "borderWidth=" + std::to_string(options->borderWidth) + "\n";
|
|
||||||
file << "borderHeight=" + std::to_string(options->borderHeight) + "\n";
|
|
||||||
|
|
||||||
// Otras opciones del programa
|
|
||||||
file << "\n## OTHER OPTIONS\n";
|
|
||||||
file << "language=" + std::to_string(options->language) + "\n";
|
|
||||||
file << "difficulty=" + std::to_string(options->difficulty) + "\n";
|
|
||||||
file << "input0=" + std::to_string(options->input[0].deviceType) + "\n";
|
|
||||||
file << "input1=" + std::to_string(options->input[1].deviceType) + "\n";
|
|
||||||
|
|
||||||
// Cierra el fichero
|
|
||||||
file.close();
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Director::runLogo() {
|
|
||||||
auto logo = std::make_unique<Logo>(renderer, screen, asset, input, section);
|
|
||||||
logo->run();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Director::runIntro() {
|
|
||||||
auto intro = std::make_unique<Intro>(renderer, screen, asset, input, lang, section);
|
|
||||||
intro->run();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Director::runTitle() {
|
|
||||||
auto title = std::make_unique<Title>(renderer, screen, input, asset, options, lang, section);
|
|
||||||
title->run();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Director::runGame() {
|
|
||||||
const int numPlayers = section->subsection == SUBSECTION_GAME_PLAY_1P ? 1 : 2;
|
|
||||||
auto game = std::make_unique<Game>(numPlayers, 0, renderer, screen, asset, lang, input, false, options, section);
|
|
||||||
game->run();
|
|
||||||
}
|
|
||||||
|
|
||||||
int Director::run() {
|
|
||||||
// Bucle principal
|
|
||||||
while (section->name != SECTION_PROG_QUIT) {
|
|
||||||
switch (section->name) {
|
|
||||||
case SECTION_PROG_LOGO:
|
|
||||||
runLogo();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SECTION_PROG_INTRO:
|
|
||||||
runIntro();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SECTION_PROG_TITLE:
|
|
||||||
runTitle();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SECTION_PROG_GAME:
|
|
||||||
runGame();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Asigna variables a partir de dos cadenas
|
|
||||||
bool Director::setOptions(options_t *options, std::string var, std::string value) {
|
|
||||||
// Indicador de éxito en la asignación
|
|
||||||
bool success = true;
|
|
||||||
|
|
||||||
// Opciones de video
|
|
||||||
if (var == "videoMode") {
|
|
||||||
if (value == "SDL_WINDOW_FULLSCREEN" || value == "SDL_WINDOW_FULLSCREEN_DESKTOP") {
|
|
||||||
options->videoMode = SDL_WINDOW_FULLSCREEN;
|
|
||||||
} else {
|
|
||||||
options->videoMode = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "windowSize") {
|
|
||||||
options->windowSize = std::stoi(value);
|
|
||||||
if ((options->windowSize < 1) || (options->windowSize > 4)) {
|
|
||||||
options->windowSize = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "filter") {
|
|
||||||
if (value == "FILTER_LINEAL") {
|
|
||||||
options->filter = FILTER_LINEAL;
|
|
||||||
} else {
|
|
||||||
options->filter = FILTER_NEAREST;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "vSync") {
|
|
||||||
options->vSync = stringToBool(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "integerScale") {
|
|
||||||
options->integerScale = stringToBool(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "keepAspect") {
|
|
||||||
options->keepAspect = stringToBool(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "borderEnabled") {
|
|
||||||
options->borderEnabled = stringToBool(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "borderWidth") {
|
|
||||||
options->borderWidth = std::stoi(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "borderHeight") {
|
|
||||||
options->borderHeight = std::stoi(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opciones varias
|
|
||||||
else if (var == "language") {
|
|
||||||
options->language = std::stoi(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "difficulty") {
|
|
||||||
options->difficulty = std::stoi(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "input0") {
|
|
||||||
options->input[0].deviceType = std::stoi(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (var == "input1") {
|
|
||||||
options->input[1].deviceType = std::stoi(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lineas vacias o que empiezan por comentario
|
|
||||||
else if (var == "" || var.substr(0, 1) == "#") {
|
|
||||||
}
|
|
||||||
|
|
||||||
else {
|
|
||||||
success = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
|
|
||||||
#include <string> // for string, basic_string
|
|
||||||
class Asset;
|
|
||||||
class Game;
|
|
||||||
class Input;
|
|
||||||
class Intro;
|
|
||||||
class Lang;
|
|
||||||
class Logo;
|
|
||||||
class Screen;
|
|
||||||
class Title;
|
|
||||||
struct options_t;
|
|
||||||
struct section_t;
|
|
||||||
|
|
||||||
// Textos
|
|
||||||
constexpr const char *WINDOW_CAPTION = "© 2020 Coffee Crisis — JailDesigner";
|
|
||||||
|
|
||||||
class Director {
|
|
||||||
private:
|
|
||||||
// Objetos y punteros
|
|
||||||
SDL_Window *window; // La ventana donde dibujamos
|
|
||||||
SDL_Renderer *renderer; // El renderizador de la ventana
|
|
||||||
Screen *screen; // Objeto encargado de dibujar en pantalla
|
|
||||||
Input *input; // Objeto Input para gestionar las entradas
|
|
||||||
Lang *lang; // Objeto para gestionar los textos en diferentes idiomas
|
|
||||||
Asset *asset; // Objeto que gestiona todos los ficheros de recursos
|
|
||||||
section_t *section; // Sección y subsección actual del programa;
|
|
||||||
|
|
||||||
// Variables
|
|
||||||
struct options_t *options; // Variable con todas las opciones del programa
|
|
||||||
std::string executablePath; // Path del ejecutable
|
|
||||||
std::string systemFolder; // Carpeta del sistema donde guardar datos
|
|
||||||
|
|
||||||
// Inicializa jail_audio
|
|
||||||
void initJailAudio();
|
|
||||||
|
|
||||||
// Arranca SDL y crea la ventana
|
|
||||||
bool initSDL();
|
|
||||||
|
|
||||||
// Inicializa el objeto input
|
|
||||||
void initInput();
|
|
||||||
|
|
||||||
// Inicializa las opciones del programa
|
|
||||||
void initOptions();
|
|
||||||
|
|
||||||
// Asigna variables a partir de dos cadenas
|
|
||||||
bool setOptions(options_t *options, std::string var, std::string value);
|
|
||||||
|
|
||||||
// Crea el indice de ficheros
|
|
||||||
bool setFileList();
|
|
||||||
|
|
||||||
// Carga el fichero de configuración
|
|
||||||
bool loadConfigFile();
|
|
||||||
|
|
||||||
// Guarda el fichero de configuración
|
|
||||||
bool saveConfigFile();
|
|
||||||
|
|
||||||
// Comprueba los parametros del programa
|
|
||||||
void checkProgramArguments(int argc, const char *argv[]);
|
|
||||||
|
|
||||||
// Crea la carpeta del sistema donde guardar datos
|
|
||||||
void createSystemFolder(const std::string &folder);
|
|
||||||
|
|
||||||
// Ejecuta la seccion de juego con el logo
|
|
||||||
void runLogo();
|
|
||||||
|
|
||||||
// Ejecuta la seccion de juego de la introducción
|
|
||||||
void runIntro();
|
|
||||||
|
|
||||||
// Ejecuta la seccion de juego con el titulo y los menus
|
|
||||||
void runTitle();
|
|
||||||
|
|
||||||
// Ejecuta la seccion de juego donde se juega
|
|
||||||
void runGame();
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Constructor
|
|
||||||
Director(int argc, const char *argv[]);
|
|
||||||
|
|
||||||
// Destructor
|
|
||||||
~Director();
|
|
||||||
|
|
||||||
// Bucle principal
|
|
||||||
int run();
|
|
||||||
};
|
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
DisableFormat: true
|
||||||
|
SortIncludes: Never
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
# source/external/.clang-tidy
|
||||||
|
Checks: '-*'
|
||||||
|
WarningsAsErrors: ''
|
||||||
|
HeaderFilterRegex: ''
|
||||||
Vendored
+14726
File diff suppressed because it is too large
Load Diff
+68
-49
@@ -1,4 +1,4 @@
|
|||||||
// Ogg Vorbis audio decoder - v1.20 - public domain
|
// Ogg Vorbis audio decoder - v1.22 - public domain
|
||||||
// http://nothings.org/stb_vorbis/
|
// http://nothings.org/stb_vorbis/
|
||||||
//
|
//
|
||||||
// Original version written by Sean Barrett in 2007.
|
// Original version written by Sean Barrett in 2007.
|
||||||
@@ -29,12 +29,15 @@
|
|||||||
// Bernhard Wodo Evan Balster github:alxprd
|
// Bernhard Wodo Evan Balster github:alxprd
|
||||||
// Tom Beaumont Ingo Leitgeb Nicolas Guillemot
|
// Tom Beaumont Ingo Leitgeb Nicolas Guillemot
|
||||||
// Phillip Bennefall Rohit Thiago Goulart
|
// Phillip Bennefall Rohit Thiago Goulart
|
||||||
// github:manxorist saga musix github:infatum
|
// github:manxorist Saga Musix github:infatum
|
||||||
// Timur Gagiev Maxwell Koo Peter Waller
|
// Timur Gagiev Maxwell Koo Peter Waller
|
||||||
// github:audinowho Dougall Johnson David Reid
|
// github:audinowho Dougall Johnson David Reid
|
||||||
// github:Clownacy Pedro J. Estebanez Remi Verschelde
|
// github:Clownacy Pedro J. Estebanez Remi Verschelde
|
||||||
|
// AnthoFoxo github:morlat Gabriel Ravier
|
||||||
//
|
//
|
||||||
// Partial history:
|
// Partial history:
|
||||||
|
// 1.22 - 2021-07-11 - various small fixes
|
||||||
|
// 1.21 - 2021-07-02 - fix bug for files with no comments
|
||||||
// 1.20 - 2020-07-11 - several small fixes
|
// 1.20 - 2020-07-11 - several small fixes
|
||||||
// 1.19 - 2020-02-05 - warnings
|
// 1.19 - 2020-02-05 - warnings
|
||||||
// 1.18 - 2020-02-02 - fix seek bugs; parse header comments; misc warnings etc.
|
// 1.18 - 2020-02-02 - fix seek bugs; parse header comments; misc warnings etc.
|
||||||
@@ -220,6 +223,12 @@ extern int stb_vorbis_decode_frame_pushdata(
|
|||||||
// channel. In other words, (*output)[0][0] contains the first sample from
|
// channel. In other words, (*output)[0][0] contains the first sample from
|
||||||
// the first channel, and (*output)[1][0] contains the first sample from
|
// the first channel, and (*output)[1][0] contains the first sample from
|
||||||
// the second channel.
|
// the second channel.
|
||||||
|
//
|
||||||
|
// *output points into stb_vorbis's internal output buffer storage; these
|
||||||
|
// buffers are owned by stb_vorbis and application code should not free
|
||||||
|
// them or modify their contents. They are transient and will be overwritten
|
||||||
|
// once you ask for more data to get decoded, so be sure to grab any data
|
||||||
|
// you need before then.
|
||||||
|
|
||||||
extern void stb_vorbis_flush_pushdata(stb_vorbis *f);
|
extern void stb_vorbis_flush_pushdata(stb_vorbis *f);
|
||||||
// inform stb_vorbis that your next datablock will not be contiguous with
|
// inform stb_vorbis that your next datablock will not be contiguous with
|
||||||
@@ -579,7 +588,7 @@ enum STBVorbisError
|
|||||||
#if defined(_MSC_VER) || defined(__MINGW32__)
|
#if defined(_MSC_VER) || defined(__MINGW32__)
|
||||||
#include <malloc.h>
|
#include <malloc.h>
|
||||||
#endif
|
#endif
|
||||||
#if defined(__linux__) || defined(__linux) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__)
|
#if defined(__linux__) || defined(__linux) || defined(__sun__) || defined(__EMSCRIPTEN__) || defined(__NEWLIB__)
|
||||||
#include <alloca.h>
|
#include <alloca.h>
|
||||||
#endif
|
#endif
|
||||||
#else // STB_VORBIS_NO_CRT
|
#else // STB_VORBIS_NO_CRT
|
||||||
@@ -646,6 +655,12 @@ typedef signed int int32;
|
|||||||
|
|
||||||
typedef float codetype;
|
typedef float codetype;
|
||||||
|
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
#define STBV_NOTUSED(v) (void)(v)
|
||||||
|
#else
|
||||||
|
#define STBV_NOTUSED(v) (void)sizeof(v)
|
||||||
|
#endif
|
||||||
|
|
||||||
// @NOTE
|
// @NOTE
|
||||||
//
|
//
|
||||||
// Some arrays below are tagged "//varies", which means it's actually
|
// Some arrays below are tagged "//varies", which means it's actually
|
||||||
@@ -1046,7 +1061,7 @@ static float float32_unpack(uint32 x)
|
|||||||
uint32 sign = x & 0x80000000;
|
uint32 sign = x & 0x80000000;
|
||||||
uint32 exp = (x & 0x7fe00000) >> 21;
|
uint32 exp = (x & 0x7fe00000) >> 21;
|
||||||
double res = sign ? -(double)mantissa : (double)mantissa;
|
double res = sign ? -(double)mantissa : (double)mantissa;
|
||||||
return (float) ldexp((float)res, exp-788);
|
return (float) ldexp((float)res, (int)exp-788);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1077,6 +1092,7 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
|
|||||||
// find the first entry
|
// find the first entry
|
||||||
for (k=0; k < n; ++k) if (len[k] < NO_CODE) break;
|
for (k=0; k < n; ++k) if (len[k] < NO_CODE) break;
|
||||||
if (k == n) { assert(c->sorted_entries == 0); return TRUE; }
|
if (k == n) { assert(c->sorted_entries == 0); return TRUE; }
|
||||||
|
assert(len[k] < 32); // no error return required, code reading lens checks this
|
||||||
// add to the list
|
// add to the list
|
||||||
add_entry(c, 0, k, m++, len[k], values);
|
add_entry(c, 0, k, m++, len[k], values);
|
||||||
// add all available leaves
|
// add all available leaves
|
||||||
@@ -1090,6 +1106,7 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
|
|||||||
uint32 res;
|
uint32 res;
|
||||||
int z = len[i], y;
|
int z = len[i], y;
|
||||||
if (z == NO_CODE) continue;
|
if (z == NO_CODE) continue;
|
||||||
|
assert(z < 32); // no error return required, code reading lens checks this
|
||||||
// find lowest available leaf (should always be earliest,
|
// find lowest available leaf (should always be earliest,
|
||||||
// which is what the specification calls for)
|
// which is what the specification calls for)
|
||||||
// note that this property, and the fact we can never have
|
// note that this property, and the fact we can never have
|
||||||
@@ -1099,12 +1116,10 @@ static int compute_codewords(Codebook *c, uint8 *len, int n, uint32 *values)
|
|||||||
while (z > 0 && !available[z]) --z;
|
while (z > 0 && !available[z]) --z;
|
||||||
if (z == 0) { return FALSE; }
|
if (z == 0) { return FALSE; }
|
||||||
res = available[z];
|
res = available[z];
|
||||||
assert(z >= 0 && z < 32);
|
|
||||||
available[z] = 0;
|
available[z] = 0;
|
||||||
add_entry(c, bit_reverse(res), i, m++, len[i], values);
|
add_entry(c, bit_reverse(res), i, m++, len[i], values);
|
||||||
// propagate availability up the tree
|
// propagate availability up the tree
|
||||||
if (z != len[i]) {
|
if (z != len[i]) {
|
||||||
assert(len[i] >= 0 && len[i] < 32);
|
|
||||||
for (y=len[i]; y > z; --y) {
|
for (y=len[i]; y > z; --y) {
|
||||||
assert(available[y] == 0);
|
assert(available[y] == 0);
|
||||||
available[y] = res + (1 << (32-y));
|
available[y] = res + (1 << (32-y));
|
||||||
@@ -2577,34 +2592,33 @@ static void imdct_step3_inner_s_loop_ld654(int n, float *e, int i_off, float *A,
|
|||||||
|
|
||||||
while (z > base) {
|
while (z > base) {
|
||||||
float k00,k11;
|
float k00,k11;
|
||||||
|
float l00,l11;
|
||||||
|
|
||||||
k00 = z[-0] - z[-8];
|
k00 = z[-0] - z[ -8];
|
||||||
k11 = z[-1] - z[-9];
|
k11 = z[-1] - z[ -9];
|
||||||
z[-0] = z[-0] + z[-8];
|
l00 = z[-2] - z[-10];
|
||||||
z[-1] = z[-1] + z[-9];
|
l11 = z[-3] - z[-11];
|
||||||
z[-8] = k00;
|
z[ -0] = z[-0] + z[ -8];
|
||||||
z[-9] = k11 ;
|
z[ -1] = z[-1] + z[ -9];
|
||||||
|
z[ -2] = z[-2] + z[-10];
|
||||||
|
z[ -3] = z[-3] + z[-11];
|
||||||
|
z[ -8] = k00;
|
||||||
|
z[ -9] = k11;
|
||||||
|
z[-10] = (l00+l11) * A2;
|
||||||
|
z[-11] = (l11-l00) * A2;
|
||||||
|
|
||||||
k00 = z[ -2] - z[-10];
|
k00 = z[ -4] - z[-12];
|
||||||
k11 = z[ -3] - z[-11];
|
|
||||||
z[ -2] = z[ -2] + z[-10];
|
|
||||||
z[ -3] = z[ -3] + z[-11];
|
|
||||||
z[-10] = (k00+k11) * A2;
|
|
||||||
z[-11] = (k11-k00) * A2;
|
|
||||||
|
|
||||||
k00 = z[-12] - z[ -4]; // reverse to avoid a unary negation
|
|
||||||
k11 = z[ -5] - z[-13];
|
k11 = z[ -5] - z[-13];
|
||||||
|
l00 = z[ -6] - z[-14];
|
||||||
|
l11 = z[ -7] - z[-15];
|
||||||
z[ -4] = z[ -4] + z[-12];
|
z[ -4] = z[ -4] + z[-12];
|
||||||
z[ -5] = z[ -5] + z[-13];
|
z[ -5] = z[ -5] + z[-13];
|
||||||
z[-12] = k11;
|
|
||||||
z[-13] = k00;
|
|
||||||
|
|
||||||
k00 = z[-14] - z[ -6]; // reverse to avoid a unary negation
|
|
||||||
k11 = z[ -7] - z[-15];
|
|
||||||
z[ -6] = z[ -6] + z[-14];
|
z[ -6] = z[ -6] + z[-14];
|
||||||
z[ -7] = z[ -7] + z[-15];
|
z[ -7] = z[ -7] + z[-15];
|
||||||
z[-14] = (k00+k11) * A2;
|
z[-12] = k11;
|
||||||
z[-15] = (k00-k11) * A2;
|
z[-13] = -k00;
|
||||||
|
z[-14] = (l11-l00) * A2;
|
||||||
|
z[-15] = (l00+l11) * -A2;
|
||||||
|
|
||||||
iter_54(z);
|
iter_54(z);
|
||||||
iter_54(z-8);
|
iter_54(z-8);
|
||||||
@@ -3069,6 +3083,7 @@ static int do_floor(vorb *f, Mapping *map, int i, int n, float *target, YTYPE *f
|
|||||||
for (q=1; q < g->values; ++q) {
|
for (q=1; q < g->values; ++q) {
|
||||||
j = g->sorted_order[q];
|
j = g->sorted_order[q];
|
||||||
#ifndef STB_VORBIS_NO_DEFER_FLOOR
|
#ifndef STB_VORBIS_NO_DEFER_FLOOR
|
||||||
|
STBV_NOTUSED(step2_flag);
|
||||||
if (finalY[j] >= 0)
|
if (finalY[j] >= 0)
|
||||||
#else
|
#else
|
||||||
if (step2_flag[j])
|
if (step2_flag[j])
|
||||||
@@ -3171,6 +3186,7 @@ static int vorbis_decode_packet_rest(vorb *f, int *len, Mode *m, int left_start,
|
|||||||
|
|
||||||
// WINDOWING
|
// WINDOWING
|
||||||
|
|
||||||
|
STBV_NOTUSED(left_end);
|
||||||
n = f->blocksize[m->blockflag];
|
n = f->blocksize[m->blockflag];
|
||||||
map = &f->mapping[m->mapping];
|
map = &f->mapping[m->mapping];
|
||||||
|
|
||||||
@@ -3368,7 +3384,7 @@ static int vorbis_decode_packet_rest(vorb *f, int *len, Mode *m, int left_start,
|
|||||||
// this isn't to spec, but spec would require us to read ahead
|
// this isn't to spec, but spec would require us to read ahead
|
||||||
// and decode the size of all current frames--could be done,
|
// and decode the size of all current frames--could be done,
|
||||||
// but presumably it's not a commonly used feature
|
// but presumably it's not a commonly used feature
|
||||||
f->current_loc = -n2; // start of first frame is positioned for discard
|
f->current_loc = 0u - n2; // start of first frame is positioned for discard (NB this is an intentional unsigned overflow/wrap-around)
|
||||||
// we might have to discard samples "from" the next frame too,
|
// we might have to discard samples "from" the next frame too,
|
||||||
// if we're lapping a large block then a small at the start?
|
// if we're lapping a large block then a small at the start?
|
||||||
f->discard_samples_deferred = n - right_end;
|
f->discard_samples_deferred = n - right_end;
|
||||||
@@ -3642,9 +3658,11 @@ static int start_decoder(vorb *f)
|
|||||||
f->vendor[len] = (char)'\0';
|
f->vendor[len] = (char)'\0';
|
||||||
//user comments
|
//user comments
|
||||||
f->comment_list_length = get32_packet(f);
|
f->comment_list_length = get32_packet(f);
|
||||||
if (f->comment_list_length > 0) {
|
f->comment_list = NULL;
|
||||||
f->comment_list = (char**)setup_malloc(f, sizeof(char*) * (f->comment_list_length));
|
if (f->comment_list_length > 0)
|
||||||
if (f->comment_list == NULL) return error(f, VORBIS_outofmem);
|
{
|
||||||
|
f->comment_list = (char**) setup_malloc(f, sizeof(char*) * (f->comment_list_length));
|
||||||
|
if (f->comment_list == NULL) return error(f, VORBIS_outofmem);
|
||||||
}
|
}
|
||||||
|
|
||||||
for(i=0; i < f->comment_list_length; ++i) {
|
for(i=0; i < f->comment_list_length; ++i) {
|
||||||
@@ -3867,8 +3885,7 @@ static int start_decoder(vorb *f)
|
|||||||
unsigned int div=1;
|
unsigned int div=1;
|
||||||
for (k=0; k < c->dimensions; ++k) {
|
for (k=0; k < c->dimensions; ++k) {
|
||||||
int off = (z / div) % c->lookup_values;
|
int off = (z / div) % c->lookup_values;
|
||||||
float val = mults[off];
|
float val = mults[off]*c->delta_value + c->minimum_value + last;
|
||||||
val = mults[off]*c->delta_value + c->minimum_value + last;
|
|
||||||
c->multiplicands[j*c->dimensions + k] = val;
|
c->multiplicands[j*c->dimensions + k] = val;
|
||||||
if (c->sequence_p)
|
if (c->sequence_p)
|
||||||
last = val;
|
last = val;
|
||||||
@@ -3951,7 +3968,7 @@ static int start_decoder(vorb *f)
|
|||||||
if (g->class_masterbooks[j] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
|
if (g->class_masterbooks[j] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
|
||||||
}
|
}
|
||||||
for (k=0; k < 1 << g->class_subclasses[j]; ++k) {
|
for (k=0; k < 1 << g->class_subclasses[j]; ++k) {
|
||||||
g->subclass_books[j][k] = get_bits(f,8)-1;
|
g->subclass_books[j][k] = (int16)get_bits(f,8)-1;
|
||||||
if (g->subclass_books[j][k] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
|
if (g->subclass_books[j][k] >= f->codebook_count) return error(f, VORBIS_invalid_setup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4509,6 +4526,7 @@ stb_vorbis *stb_vorbis_open_pushdata(
|
|||||||
*error = VORBIS_need_more_data;
|
*error = VORBIS_need_more_data;
|
||||||
else
|
else
|
||||||
*error = p.error;
|
*error = p.error;
|
||||||
|
vorbis_deinit(&p);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
f = vorbis_alloc(&p);
|
f = vorbis_alloc(&p);
|
||||||
@@ -4566,7 +4584,7 @@ static uint32 vorbis_find_page(stb_vorbis *f, uint32 *end, uint32 *last)
|
|||||||
header[i] = get8(f);
|
header[i] = get8(f);
|
||||||
if (f->eof) return 0;
|
if (f->eof) return 0;
|
||||||
if (header[4] != 0) goto invalid;
|
if (header[4] != 0) goto invalid;
|
||||||
goal = header[22] + (header[23] << 8) + (header[24]<<16) + (header[25]<<24);
|
goal = header[22] + (header[23] << 8) + (header[24]<<16) + ((uint32)header[25]<<24);
|
||||||
for (i=22; i < 26; ++i)
|
for (i=22; i < 26; ++i)
|
||||||
header[i] = 0;
|
header[i] = 0;
|
||||||
crc = 0;
|
crc = 0;
|
||||||
@@ -4970,7 +4988,7 @@ unsigned int stb_vorbis_stream_length_in_samples(stb_vorbis *f)
|
|||||||
// set. whoops!
|
// set. whoops!
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
previous_safe = last_page_loc+1;
|
//previous_safe = last_page_loc+1; // NOTE: not used after this point, but note for debugging
|
||||||
last_page_loc = stb_vorbis_get_file_offset(f);
|
last_page_loc = stb_vorbis_get_file_offset(f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5081,7 +5099,10 @@ stb_vorbis * stb_vorbis_open_filename(const char *filename, int *error, const st
|
|||||||
stb_vorbis * stb_vorbis_open_memory(const unsigned char *data, int len, int *error, const stb_vorbis_alloc *alloc)
|
stb_vorbis * stb_vorbis_open_memory(const unsigned char *data, int len, int *error, const stb_vorbis_alloc *alloc)
|
||||||
{
|
{
|
||||||
stb_vorbis *f, p;
|
stb_vorbis *f, p;
|
||||||
if (data == NULL) return NULL;
|
if (!data) {
|
||||||
|
if (error) *error = VORBIS_unexpected_eof;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
vorbis_init(&p, alloc);
|
vorbis_init(&p, alloc);
|
||||||
p.stream = (uint8 *) data;
|
p.stream = (uint8 *) data;
|
||||||
p.stream_end = (uint8 *) data + len;
|
p.stream_end = (uint8 *) data + len;
|
||||||
@@ -5156,11 +5177,11 @@ static void copy_samples(short *dest, float *src, int len)
|
|||||||
|
|
||||||
static void compute_samples(int mask, short *output, int num_c, float **data, int d_offset, int len)
|
static void compute_samples(int mask, short *output, int num_c, float **data, int d_offset, int len)
|
||||||
{
|
{
|
||||||
#define BUFFER_SIZE 32
|
#define STB_BUFFER_SIZE 32
|
||||||
float buffer[BUFFER_SIZE];
|
float buffer[STB_BUFFER_SIZE];
|
||||||
int i,j,o,n = BUFFER_SIZE;
|
int i,j,o,n = STB_BUFFER_SIZE;
|
||||||
check_endianness();
|
check_endianness();
|
||||||
for (o = 0; o < len; o += BUFFER_SIZE) {
|
for (o = 0; o < len; o += STB_BUFFER_SIZE) {
|
||||||
memset(buffer, 0, sizeof(buffer));
|
memset(buffer, 0, sizeof(buffer));
|
||||||
if (o + n > len) n = len - o;
|
if (o + n > len) n = len - o;
|
||||||
for (j=0; j < num_c; ++j) {
|
for (j=0; j < num_c; ++j) {
|
||||||
@@ -5177,16 +5198,17 @@ static void compute_samples(int mask, short *output, int num_c, float **data, in
|
|||||||
output[o+i] = v;
|
output[o+i] = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#undef STB_BUFFER_SIZE
|
||||||
}
|
}
|
||||||
|
|
||||||
static void compute_stereo_samples(short *output, int num_c, float **data, int d_offset, int len)
|
static void compute_stereo_samples(short *output, int num_c, float **data, int d_offset, int len)
|
||||||
{
|
{
|
||||||
#define BUFFER_SIZE 32
|
#define STB_BUFFER_SIZE 32
|
||||||
float buffer[BUFFER_SIZE];
|
float buffer[STB_BUFFER_SIZE];
|
||||||
int i,j,o,n = BUFFER_SIZE >> 1;
|
int i,j,o,n = STB_BUFFER_SIZE >> 1;
|
||||||
// o is the offset in the source data
|
// o is the offset in the source data
|
||||||
check_endianness();
|
check_endianness();
|
||||||
for (o = 0; o < len; o += BUFFER_SIZE >> 1) {
|
for (o = 0; o < len; o += STB_BUFFER_SIZE >> 1) {
|
||||||
// o2 is the offset in the output data
|
// o2 is the offset in the output data
|
||||||
int o2 = o << 1;
|
int o2 = o << 1;
|
||||||
memset(buffer, 0, sizeof(buffer));
|
memset(buffer, 0, sizeof(buffer));
|
||||||
@@ -5216,6 +5238,7 @@ static void compute_stereo_samples(short *output, int num_c, float **data, int d
|
|||||||
output[o2+i] = v;
|
output[o2+i] = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#undef STB_BUFFER_SIZE
|
||||||
}
|
}
|
||||||
|
|
||||||
static void convert_samples_short(int buf_c, short **buffer, int b_offset, int data_c, float **data, int d_offset, int samples)
|
static void convert_samples_short(int buf_c, short **buffer, int b_offset, int data_c, float **data, int d_offset, int samples)
|
||||||
@@ -5288,8 +5311,6 @@ int stb_vorbis_get_samples_short_interleaved(stb_vorbis *f, int channels, short
|
|||||||
float **outputs;
|
float **outputs;
|
||||||
int len = num_shorts / channels;
|
int len = num_shorts / channels;
|
||||||
int n=0;
|
int n=0;
|
||||||
int z = f->channels;
|
|
||||||
if (z > channels) z = channels;
|
|
||||||
while (n < len) {
|
while (n < len) {
|
||||||
int k = f->channel_buffer_end - f->channel_buffer_start;
|
int k = f->channel_buffer_end - f->channel_buffer_start;
|
||||||
if (n+k >= len) k = len - n;
|
if (n+k >= len) k = len - n;
|
||||||
@@ -5308,8 +5329,6 @@ int stb_vorbis_get_samples_short(stb_vorbis *f, int channels, short **buffer, in
|
|||||||
{
|
{
|
||||||
float **outputs;
|
float **outputs;
|
||||||
int n=0;
|
int n=0;
|
||||||
int z = f->channels;
|
|
||||||
if (z > channels) z = channels;
|
|
||||||
while (n < len) {
|
while (n < len) {
|
||||||
int k = f->channel_buffer_end - f->channel_buffer_start;
|
int k = f->channel_buffer_end - f->channel_buffer_start;
|
||||||
if (n+k >= len) k = len - n;
|
if (n+k >= len) k = len - n;
|
||||||
-158
@@ -1,158 +0,0 @@
|
|||||||
#include "fade.h"
|
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
#include <stdlib.h> // for rand
|
|
||||||
|
|
||||||
#include <iostream> // for char_traits, basic_ostream, operator<<
|
|
||||||
|
|
||||||
#include "const.h" // for GAMECANVAS_HEIGHT, GAMECANVAS_WIDTH
|
|
||||||
|
|
||||||
// Constructor
|
|
||||||
Fade::Fade(SDL_Renderer *renderer) {
|
|
||||||
mRenderer = renderer;
|
|
||||||
|
|
||||||
mBackbuffer = SDL_CreateTexture(mRenderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, GAMECANVAS_WIDTH, GAMECANVAS_HEIGHT);
|
|
||||||
if (mBackbuffer != nullptr) {
|
|
||||||
SDL_SetTextureScaleMode(mBackbuffer, SDL_SCALEMODE_NEAREST);
|
|
||||||
}
|
|
||||||
if (mBackbuffer == nullptr) {
|
|
||||||
std::cout << "Error: textTexture could not be created!\nSDL Error: " << SDL_GetError() << std::endl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destructor
|
|
||||||
Fade::~Fade() {
|
|
||||||
SDL_DestroyTexture(mBackbuffer);
|
|
||||||
mBackbuffer = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inicializa las variables
|
|
||||||
void Fade::init(Uint8 r, Uint8 g, Uint8 b) {
|
|
||||||
mFadeType = FADE_CENTER;
|
|
||||||
mEnabled = false;
|
|
||||||
mFinished = false;
|
|
||||||
mCounter = 0;
|
|
||||||
mR = r;
|
|
||||||
mG = g;
|
|
||||||
mB = b;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pinta una transición en pantalla
|
|
||||||
void Fade::render() {
|
|
||||||
if (mEnabled && !mFinished) {
|
|
||||||
switch (mFadeType) {
|
|
||||||
case FADE_FULLSCREEN: {
|
|
||||||
SDL_FRect fRect1 = {0, 0, (float)GAMECANVAS_WIDTH, (float)GAMECANVAS_HEIGHT};
|
|
||||||
|
|
||||||
for (int i = 0; i < 256; i += 4) {
|
|
||||||
// Dibujamos sobre el renderizador
|
|
||||||
SDL_SetRenderTarget(mRenderer, nullptr);
|
|
||||||
|
|
||||||
// Copia el backbuffer con la imagen que había al renderizador
|
|
||||||
SDL_RenderTexture(mRenderer, mBackbuffer, nullptr, nullptr);
|
|
||||||
|
|
||||||
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, i);
|
|
||||||
SDL_RenderFillRect(mRenderer, &fRect1);
|
|
||||||
|
|
||||||
// Vuelca el renderizador en pantalla
|
|
||||||
SDL_RenderPresent(mRenderer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deja todos los buffers del mismo color
|
|
||||||
SDL_SetRenderTarget(mRenderer, mBackbuffer);
|
|
||||||
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 255);
|
|
||||||
SDL_RenderClear(mRenderer);
|
|
||||||
|
|
||||||
SDL_SetRenderTarget(mRenderer, nullptr);
|
|
||||||
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 255);
|
|
||||||
SDL_RenderClear(mRenderer);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case FADE_CENTER: {
|
|
||||||
SDL_FRect fR1 = {0, 0, (float)GAMECANVAS_WIDTH, 0};
|
|
||||||
SDL_FRect fR2 = {0, 0, (float)GAMECANVAS_WIDTH, 0};
|
|
||||||
|
|
||||||
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 64);
|
|
||||||
|
|
||||||
for (int i = 0; i < mCounter; i++) {
|
|
||||||
fR1.h = fR2.h = (float)(i * 4);
|
|
||||||
fR2.y = (float)(GAMECANVAS_HEIGHT - (i * 4));
|
|
||||||
|
|
||||||
SDL_RenderFillRect(mRenderer, &fR1);
|
|
||||||
SDL_RenderFillRect(mRenderer, &fR2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((mCounter * 4) > GAMECANVAS_HEIGHT)
|
|
||||||
mFinished = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case FADE_RANDOM_SQUARE: {
|
|
||||||
SDL_FRect fRs = {0, 0, 32, 32};
|
|
||||||
|
|
||||||
for (Uint16 i = 0; i < 50; i++) {
|
|
||||||
// Crea un color al azar
|
|
||||||
mR = 255 * (rand() % 2);
|
|
||||||
mG = 255 * (rand() % 2);
|
|
||||||
mB = 255 * (rand() % 2);
|
|
||||||
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 64);
|
|
||||||
|
|
||||||
// Dibujamos sobre el backbuffer
|
|
||||||
SDL_SetRenderTarget(mRenderer, mBackbuffer);
|
|
||||||
|
|
||||||
fRs.x = (float)(rand() % (GAMECANVAS_WIDTH - 32));
|
|
||||||
fRs.y = (float)(rand() % (GAMECANVAS_HEIGHT - 32));
|
|
||||||
SDL_RenderFillRect(mRenderer, &fRs);
|
|
||||||
|
|
||||||
// Volvemos a usar el renderizador de forma normal
|
|
||||||
SDL_SetRenderTarget(mRenderer, nullptr);
|
|
||||||
|
|
||||||
// Copiamos el backbuffer al renderizador
|
|
||||||
SDL_RenderTexture(mRenderer, mBackbuffer, nullptr, nullptr);
|
|
||||||
|
|
||||||
// Volcamos el renderizador en pantalla
|
|
||||||
SDL_RenderPresent(mRenderer);
|
|
||||||
SDL_Delay(100);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mFinished) {
|
|
||||||
SDL_SetRenderDrawColor(mRenderer, mR, mG, mB, 255);
|
|
||||||
SDL_RenderClear(mRenderer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actualiza las variables internas
|
|
||||||
void Fade::update() {
|
|
||||||
if (mEnabled)
|
|
||||||
mCounter++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Activa el fade
|
|
||||||
void Fade::activateFade() {
|
|
||||||
mEnabled = true;
|
|
||||||
mFinished = false;
|
|
||||||
mCounter = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprueba si está activo
|
|
||||||
bool Fade::isEnabled() {
|
|
||||||
return mEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprueba si ha terminado la transicion
|
|
||||||
bool Fade::hasEnded() {
|
|
||||||
return mFinished;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establece el tipo de fade
|
|
||||||
void Fade::setFadeType(Uint8 fadeType) {
|
|
||||||
mFadeType = fadeType;
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
|
||||||
|
|
||||||
// Tipos de fundido
|
|
||||||
constexpr int FADE_FULLSCREEN = 0;
|
|
||||||
constexpr int FADE_CENTER = 1;
|
|
||||||
constexpr int FADE_RANDOM_SQUARE = 2;
|
|
||||||
|
|
||||||
// Clase Fade
|
|
||||||
class Fade {
|
|
||||||
private:
|
|
||||||
SDL_Renderer *mRenderer; // El renderizador de la ventana
|
|
||||||
SDL_Texture *mBackbuffer; // Textura para usar como backbuffer
|
|
||||||
Uint8 mFadeType; // Tipo de fade a realizar
|
|
||||||
Uint16 mCounter; // Contador interno
|
|
||||||
bool mEnabled; // Indica si el fade está activo
|
|
||||||
bool mFinished; // Indica si ha terminado la transición
|
|
||||||
Uint8 mR, mG, mB; // Colores para el fade
|
|
||||||
SDL_Rect mRect1; // Rectangulo usado para crear los efectos de transición
|
|
||||||
SDL_Rect mRect2; // Rectangulo usado para crear los efectos de transición
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Constructor
|
|
||||||
Fade(SDL_Renderer *renderer);
|
|
||||||
|
|
||||||
// Destructor
|
|
||||||
~Fade();
|
|
||||||
|
|
||||||
// Inicializa las variables
|
|
||||||
void init(Uint8 r, Uint8 g, Uint8 b);
|
|
||||||
|
|
||||||
// Pinta una transición en pantalla
|
|
||||||
void render();
|
|
||||||
|
|
||||||
// Actualiza las variables internas
|
|
||||||
void update();
|
|
||||||
|
|
||||||
// Activa el fade
|
|
||||||
void activateFade();
|
|
||||||
|
|
||||||
// Comprueba si ha terminado la transicion
|
|
||||||
bool hasEnded();
|
|
||||||
|
|
||||||
// Comprueba si está activo
|
|
||||||
bool isEnabled();
|
|
||||||
|
|
||||||
// Establece el tipo de fade
|
|
||||||
void setFadeType(Uint8 fadeType);
|
|
||||||
};
|
|
||||||
-3436
File diff suppressed because it is too large
Load Diff
-522
@@ -1,522 +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
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
|
||||||
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
|
|
||||||
#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();
|
|
||||||
|
|
||||||
// Agita la pantalla
|
|
||||||
void shakeScreen();
|
|
||||||
|
|
||||||
// Actualiza las variables del menu de pausa del juego
|
|
||||||
void updatePausedGame();
|
|
||||||
|
|
||||||
// Dibuja el menu de pausa del juego
|
|
||||||
void renderPausedGame();
|
|
||||||
|
|
||||||
// Bucle para el menu de pausa del juego
|
|
||||||
void runPausedGame();
|
|
||||||
|
|
||||||
// Actualiza los elementos de la pantalla de game over
|
|
||||||
void updateGameOverScreen();
|
|
||||||
|
|
||||||
// Dibuja los elementos de la pantalla de game over
|
|
||||||
void renderGameOverScreen();
|
|
||||||
|
|
||||||
// Bucle para la pantalla de game over
|
|
||||||
void runGameOverScreen();
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
};
|
|
||||||
@@ -2,8 +2,58 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include "lang.h"
|
#include "core/locale/lang.h"
|
||||||
#include "utils.h"
|
#include "utils/utils.h"
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Defaults per a Options (alineats amb coffee_crisis_arcade_edition).
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
namespace Defaults::Window {
|
||||||
|
constexpr const char *CAPTION = "© 2020 Coffee Crisis — JailDesigner";
|
||||||
|
constexpr int ZOOM = 3;
|
||||||
|
constexpr int MAX_ZOOM = 4;
|
||||||
|
} // namespace Defaults::Window
|
||||||
|
|
||||||
|
namespace Defaults::Video {
|
||||||
|
constexpr SDL_ScaleMode SCALE_MODE = SDL_ScaleMode::SDL_SCALEMODE_NEAREST;
|
||||||
|
constexpr bool FULLSCREEN = false;
|
||||||
|
constexpr bool VSYNC = true;
|
||||||
|
constexpr bool INTEGER_SCALE = true;
|
||||||
|
constexpr bool GPU_ACCELERATION = true;
|
||||||
|
constexpr const char *GPU_PREFERRED_DRIVER = "";
|
||||||
|
constexpr bool SHADER_ENABLED = false;
|
||||||
|
} // namespace Defaults::Video
|
||||||
|
|
||||||
|
namespace Defaults::Audio {
|
||||||
|
constexpr bool ENABLED = true;
|
||||||
|
constexpr float VOLUME = 1.0F;
|
||||||
|
} // namespace Defaults::Audio
|
||||||
|
|
||||||
|
namespace Defaults::Music {
|
||||||
|
constexpr bool ENABLED = true;
|
||||||
|
constexpr float VOLUME = 0.8F;
|
||||||
|
} // namespace Defaults::Music
|
||||||
|
|
||||||
|
namespace Defaults::Sound {
|
||||||
|
constexpr bool ENABLED = true;
|
||||||
|
constexpr float VOLUME = 1.0F;
|
||||||
|
} // namespace Defaults::Sound
|
||||||
|
|
||||||
|
namespace Defaults::Loading {
|
||||||
|
constexpr bool SHOW = false;
|
||||||
|
constexpr bool SHOW_RESOURCE_NAME = true;
|
||||||
|
constexpr bool WAIT_FOR_INPUT = false;
|
||||||
|
} // namespace Defaults::Loading
|
||||||
|
|
||||||
|
namespace Defaults::Settings {
|
||||||
|
constexpr int DIFFICULTY = DIFFICULTY_NORMAL;
|
||||||
|
constexpr Lang::Code LANGUAGE = Lang::Code::BA_BA;
|
||||||
|
} // namespace Defaults::Settings
|
||||||
|
|
||||||
|
namespace Defaults::Gameplay {
|
||||||
|
constexpr bool PAUSE_COUNTDOWN = true;
|
||||||
|
} // namespace Defaults::Gameplay
|
||||||
|
|
||||||
// Tamaño de bloque
|
// Tamaño de bloque
|
||||||
constexpr int BLOCK = 8;
|
constexpr int BLOCK = 8;
|
||||||
@@ -56,6 +106,6 @@ constexpr int SUBSECTION_TITLE_INSTRUCTIONS = 6;
|
|||||||
constexpr int NO_KIND = 0;
|
constexpr int NO_KIND = 0;
|
||||||
|
|
||||||
// Colores
|
// Colores
|
||||||
const color_t bgColor = {0x27, 0x27, 0x36};
|
const Color BG_COLOR = {0x27, 0x27, 0x36};
|
||||||
const color_t noColor = {0xFF, 0xFF, 0xFF};
|
const Color NO_COLOR = {0xFF, 0xFF, 0xFF};
|
||||||
const color_t shdwTxtColor = {0x43, 0x43, 0x4F};
|
const Color SHADOW_COLOR = {0x43, 0x43, 0x4F};
|
||||||
@@ -0,0 +1,690 @@
|
|||||||
|
#include "game/entities/balloon.h"
|
||||||
|
|
||||||
|
#include <algorithm> // for std::max
|
||||||
|
#include <cmath> // for std::fabs
|
||||||
|
|
||||||
|
#include "core/rendering/animatedsprite.h" // for AnimatedSprite
|
||||||
|
#include "core/rendering/movingsprite.h" // for MovingSprite
|
||||||
|
#include "core/rendering/sprite.h" // for Sprite
|
||||||
|
#include "core/rendering/texture.h" // for Texture
|
||||||
|
#include "game/defaults.hpp" // for PLAY_AREA_LEFT, PLAY_AREA_RIGHT, PLAY_AR...
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Balloon::Balloon(float x, float y, Uint8 kind, float velx, float speed, Uint16 creationtimer, Texture *texture, const std::vector<std::string> *animation, SDL_Renderer *renderer) {
|
||||||
|
sprite_ = new AnimatedSprite(texture, renderer, "", animation);
|
||||||
|
disable();
|
||||||
|
|
||||||
|
enabled_ = true;
|
||||||
|
|
||||||
|
switch (kind) {
|
||||||
|
case BALLOON_1:
|
||||||
|
width_ = WIDTH_1;
|
||||||
|
height_ = WIDTH_1;
|
||||||
|
size_ = SIZE_1;
|
||||||
|
power_ = 1;
|
||||||
|
|
||||||
|
vel_x_s_ = velx;
|
||||||
|
vel_y_s_ = 0.0F;
|
||||||
|
max_vel_y_s_ = 180.0F;
|
||||||
|
gravity_s_ = 324.0F;
|
||||||
|
default_vel_y_s_ = 156.0F;
|
||||||
|
|
||||||
|
score_ = SCORE_1;
|
||||||
|
menace_ = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BALLOON_2:
|
||||||
|
width_ = WIDTH_2;
|
||||||
|
height_ = WIDTH_2;
|
||||||
|
size_ = SIZE_2;
|
||||||
|
power_ = 3;
|
||||||
|
|
||||||
|
vel_x_s_ = velx;
|
||||||
|
vel_y_s_ = 0.0F;
|
||||||
|
max_vel_y_s_ = 180.0F;
|
||||||
|
gravity_s_ = 360.0F;
|
||||||
|
default_vel_y_s_ = 210.0F;
|
||||||
|
|
||||||
|
score_ = SCORE_2;
|
||||||
|
menace_ = 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BALLOON_3:
|
||||||
|
width_ = WIDTH_3;
|
||||||
|
height_ = WIDTH_3;
|
||||||
|
size_ = SIZE_3;
|
||||||
|
power_ = 7;
|
||||||
|
|
||||||
|
vel_x_s_ = velx;
|
||||||
|
vel_y_s_ = 0.0F;
|
||||||
|
max_vel_y_s_ = 180.0F;
|
||||||
|
gravity_s_ = 360.0F;
|
||||||
|
default_vel_y_s_ = 270.0F;
|
||||||
|
|
||||||
|
score_ = SCORE_3;
|
||||||
|
menace_ = 4;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BALLOON_4:
|
||||||
|
width_ = WIDTH_4;
|
||||||
|
height_ = WIDTH_4;
|
||||||
|
size_ = SIZE_4;
|
||||||
|
power_ = 15;
|
||||||
|
|
||||||
|
vel_x_s_ = velx;
|
||||||
|
vel_y_s_ = 0.0F;
|
||||||
|
max_vel_y_s_ = 180.0F;
|
||||||
|
gravity_s_ = 360.0F;
|
||||||
|
default_vel_y_s_ = 297.0F;
|
||||||
|
|
||||||
|
score_ = SCORE_4;
|
||||||
|
menace_ = 8;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HEXAGON_1:
|
||||||
|
width_ = WIDTH_1;
|
||||||
|
height_ = WIDTH_1;
|
||||||
|
size_ = SIZE_1;
|
||||||
|
power_ = 1;
|
||||||
|
|
||||||
|
vel_x_s_ = velx;
|
||||||
|
vel_y_s_ = std::fabs(velx) * 2.0F;
|
||||||
|
max_vel_y_s_ = std::fabs(velx) * 2.0F;
|
||||||
|
gravity_s_ = 0.0F;
|
||||||
|
default_vel_y_s_ = std::fabs(velx) * 2.0F;
|
||||||
|
|
||||||
|
score_ = SCORE_1;
|
||||||
|
menace_ = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HEXAGON_2:
|
||||||
|
width_ = WIDTH_2;
|
||||||
|
height_ = WIDTH_2;
|
||||||
|
size_ = SIZE_2;
|
||||||
|
power_ = 3;
|
||||||
|
|
||||||
|
vel_x_s_ = velx;
|
||||||
|
vel_y_s_ = std::fabs(velx) * 2.0F;
|
||||||
|
max_vel_y_s_ = std::fabs(velx) * 2.0F;
|
||||||
|
gravity_s_ = 0.0F;
|
||||||
|
default_vel_y_s_ = std::fabs(velx) * 2.0F;
|
||||||
|
|
||||||
|
score_ = SCORE_2;
|
||||||
|
menace_ = 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HEXAGON_3:
|
||||||
|
width_ = WIDTH_3;
|
||||||
|
height_ = WIDTH_3;
|
||||||
|
size_ = SIZE_3;
|
||||||
|
power_ = 7;
|
||||||
|
|
||||||
|
vel_x_s_ = velx;
|
||||||
|
vel_y_s_ = std::fabs(velx) * 2.0F;
|
||||||
|
max_vel_y_s_ = std::fabs(velx) * 2.0F;
|
||||||
|
gravity_s_ = 0.0F;
|
||||||
|
default_vel_y_s_ = std::fabs(velx) * 2.0F;
|
||||||
|
|
||||||
|
score_ = SCORE_3;
|
||||||
|
menace_ = 4;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HEXAGON_4:
|
||||||
|
width_ = WIDTH_4;
|
||||||
|
height_ = WIDTH_4;
|
||||||
|
size_ = SIZE_4;
|
||||||
|
power_ = 15;
|
||||||
|
|
||||||
|
vel_x_s_ = velx;
|
||||||
|
vel_y_s_ = std::fabs(velx) * 2.0F;
|
||||||
|
max_vel_y_s_ = std::fabs(velx) * 2.0F;
|
||||||
|
gravity_s_ = 0.0F;
|
||||||
|
default_vel_y_s_ = std::fabs(velx) * 2.0F;
|
||||||
|
|
||||||
|
score_ = SCORE_4;
|
||||||
|
menace_ = 8;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case POWER_BALL:
|
||||||
|
width_ = WIDTH_4;
|
||||||
|
height_ = WIDTH_4;
|
||||||
|
size_ = 4;
|
||||||
|
power_ = 0;
|
||||||
|
|
||||||
|
vel_x_s_ = velx;
|
||||||
|
vel_y_s_ = 0.0F;
|
||||||
|
max_vel_y_s_ = 180.0F;
|
||||||
|
gravity_s_ = 360.0F;
|
||||||
|
default_vel_y_s_ = 297.0F;
|
||||||
|
|
||||||
|
// Puntos que da el globo al ser destruido
|
||||||
|
score_ = 0;
|
||||||
|
|
||||||
|
// Amenaza que genera el globo
|
||||||
|
menace_ = 0;
|
||||||
|
|
||||||
|
// Configura la rotació del sprite. L'activació/desactivació es
|
||||||
|
// delega a `setStop()`, que la sincronitza amb l'estat stopped_
|
||||||
|
// sempre que canvia. setRotateSpeed(1) evita la UB del `counter_
|
||||||
|
// % 0` dins MovingSprite::rotate().
|
||||||
|
sprite_->setRotateSpeed(1);
|
||||||
|
sprite_->setRotateAmount(vel_x_s_ > 0.0F ? 2.0 : -2.0);
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Posición inicial
|
||||||
|
pos_x_ = x;
|
||||||
|
pos_y_ = y;
|
||||||
|
|
||||||
|
// Valores para el efecto de rebote
|
||||||
|
bouncing_.enabled = false;
|
||||||
|
bouncing_.counter = 0;
|
||||||
|
bouncing_.speed = 2;
|
||||||
|
bouncing_.zoom_width = 1.0F;
|
||||||
|
bouncing_.zoom_height = 1.0F;
|
||||||
|
bouncing_.desp_x = 0.0F;
|
||||||
|
bouncing_.desp_y = 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)pos_x_);
|
||||||
|
sprite_->setPosY((int)pos_y_);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
stopped_counter_ = 0;
|
||||||
|
stopped_counter_s_ = 0.0F;
|
||||||
|
blinking_ = false;
|
||||||
|
visible_ = true;
|
||||||
|
creation_counter_ = creationtimer;
|
||||||
|
creation_counter_ini_ = creationtimer;
|
||||||
|
creation_counter_s_ = static_cast<float>(creationtimer) / 60.0F;
|
||||||
|
creation_counter_ini_s_ = creation_counter_s_;
|
||||||
|
creation_phase_s_ = 0.0F;
|
||||||
|
bounce_phase_s_ = 0.0F;
|
||||||
|
popping_ = false;
|
||||||
|
|
||||||
|
// Valores iniciales dependentes del timer
|
||||||
|
being_created_ = creation_counter_ != 0;
|
||||||
|
invulnerable_ = being_created_;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
pos_x_ = float(x - (width_ / 2));
|
||||||
|
|
||||||
|
if (pos_x_ < PLAY_AREA_LEFT) {
|
||||||
|
pos_x_ = PLAY_AREA_LEFT + 1;
|
||||||
|
} else if ((pos_x_ + width_) > PLAY_AREA_RIGHT) {
|
||||||
|
pos_x_ = 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_.desp_x);
|
||||||
|
sprite_->setPosY(getPosY() + bouncing_.desp_y);
|
||||||
|
sprite_->render();
|
||||||
|
sprite_->setPosX(getPosX() - bouncing_.desp_x);
|
||||||
|
sprite_->setPosY(getPosY() - bouncing_.desp_y);
|
||||||
|
}
|
||||||
|
} else if (isBeingCreated()) {
|
||||||
|
// Aplica alpha blending
|
||||||
|
sprite_->getTexture()->setAlpha(255 - (int)((float)creation_counter_ * (255.0F / (float)creation_counter_ini_)));
|
||||||
|
sprite_->render();
|
||||||
|
if (kind_ == POWER_BALL) {
|
||||||
|
auto *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_) {
|
||||||
|
auto *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. Integració contínua: gravetat i
|
||||||
|
// posició s'apliquen escalades per `speed_` (tempo del joc) cada tick.
|
||||||
|
void Balloon::move(float dt_s) {
|
||||||
|
if (!isStopped()) {
|
||||||
|
// Eix X
|
||||||
|
pos_x_ += vel_x_s_ * speed_ * dt_s;
|
||||||
|
|
||||||
|
if ((pos_x_ < PLAY_AREA_LEFT) || (pos_x_ + width_ > PLAY_AREA_RIGHT)) {
|
||||||
|
pos_x_ -= vel_x_s_ * speed_ * dt_s;
|
||||||
|
vel_x_s_ = -vel_x_s_;
|
||||||
|
sprite_->switchRotate();
|
||||||
|
if (kind_ != POWER_BALL) { bounceStart(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eix Y
|
||||||
|
pos_y_ += vel_y_s_ * speed_ * dt_s;
|
||||||
|
|
||||||
|
if (pos_y_ < PLAY_AREA_TOP) {
|
||||||
|
pos_y_ = PLAY_AREA_TOP;
|
||||||
|
vel_y_s_ = -vel_y_s_;
|
||||||
|
if (kind_ != POWER_BALL) { bounceStart(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos_y_ + height_ > PLAY_AREA_BOTTOM) {
|
||||||
|
pos_y_ = PLAY_AREA_BOTTOM - height_;
|
||||||
|
vel_y_s_ = -default_vel_y_s_;
|
||||||
|
if (kind_ != POWER_BALL) { bounceStart(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gravetat contínua (el tempo `speed_` escala també la gravetat).
|
||||||
|
vel_y_s_ += gravity_s_ * speed_ * dt_s;
|
||||||
|
|
||||||
|
sprite_->setPosX(getPosX());
|
||||||
|
sprite_->setPosY(getPosY());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deshabilita el globo y pone a cero todos los valores
|
||||||
|
void Balloon::disable() {
|
||||||
|
being_created_ = false;
|
||||||
|
blinking_ = false;
|
||||||
|
collider_.r = 0;
|
||||||
|
collider_.x = 0;
|
||||||
|
collider_.y = 0;
|
||||||
|
creation_counter_ = 0;
|
||||||
|
creation_counter_ini_ = 0;
|
||||||
|
creation_counter_s_ = 0.0F;
|
||||||
|
creation_counter_ini_s_ = 0.0F;
|
||||||
|
creation_phase_s_ = 0.0F;
|
||||||
|
bounce_phase_s_ = 0.0F;
|
||||||
|
default_vel_y_s_ = 0.0F;
|
||||||
|
enabled_ = false;
|
||||||
|
gravity_s_ = 0.0F;
|
||||||
|
height_ = 0;
|
||||||
|
invulnerable_ = false;
|
||||||
|
kind_ = 0;
|
||||||
|
max_vel_y_s_ = 0.0F;
|
||||||
|
menace_ = 0;
|
||||||
|
popping_ = false;
|
||||||
|
pos_x_ = 0.0F;
|
||||||
|
pos_y_ = 0.0F;
|
||||||
|
power_ = 0;
|
||||||
|
score_ = 0;
|
||||||
|
size_ = 0;
|
||||||
|
speed_ = 0;
|
||||||
|
stopped_ = false;
|
||||||
|
stopped_counter_ = 0;
|
||||||
|
stopped_counter_s_ = 0.0F;
|
||||||
|
vel_x_s_ = 0.0F;
|
||||||
|
vel_y_s_ = 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(float dt_s) {
|
||||||
|
if (enabled_) {
|
||||||
|
// MovingSprite::update(dt_s) avança la rotació (entre altres). La posició
|
||||||
|
// del sprite la posa move(dt_s) directament des de pos_x_/pos_y_.
|
||||||
|
sprite_->MovingSprite::update(dt_s);
|
||||||
|
move(dt_s);
|
||||||
|
updateAnimation(dt_s);
|
||||||
|
updateColliders();
|
||||||
|
updateState(dt_s);
|
||||||
|
updateBounce(dt_s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualiza los estados del globo
|
||||||
|
void Balloon::updateState(float dt_s) {
|
||||||
|
if (isPopping()) {
|
||||||
|
updateStatePopping();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBeingCreated()) {
|
||||||
|
updateStateBeingCreated(dt_s);
|
||||||
|
} else if (isStopped()) {
|
||||||
|
updateStateStopped(dt_s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rama de updateState: globo explotando
|
||||||
|
void Balloon::updateStatePopping() {
|
||||||
|
setInvulnerable(true);
|
||||||
|
setStop(true);
|
||||||
|
if (sprite_->animationIsCompleted()) {
|
||||||
|
disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rama de updateState: globo creándose. Manté el chunk pattern original:
|
||||||
|
// cada CREATION_STEP_S s'aplica un step de drift (equivalent a "cada 10
|
||||||
|
// frames" del codi original). El drift en X usa vel_x_s_/60 per a obtenir
|
||||||
|
// el mateix delta px-per-step.
|
||||||
|
void Balloon::updateStateBeingCreated(float dt_s) {
|
||||||
|
setStop(true);
|
||||||
|
setInvulnerable(true);
|
||||||
|
|
||||||
|
if (creation_counter_s_ > 0.0F) {
|
||||||
|
creation_phase_s_ += dt_s;
|
||||||
|
while (creation_phase_s_ >= CREATION_STEP_S) {
|
||||||
|
creation_phase_s_ -= CREATION_STEP_S;
|
||||||
|
pos_y_ += 1.0F;
|
||||||
|
const float DRIFT_X = vel_x_s_ / 60.0F;
|
||||||
|
pos_x_ += DRIFT_X;
|
||||||
|
|
||||||
|
if ((pos_x_ < PLAY_AREA_LEFT) || (pos_x_ > (PLAY_AREA_RIGHT - width_))) {
|
||||||
|
pos_x_ -= DRIFT_X;
|
||||||
|
vel_x_s_ = -vel_x_s_;
|
||||||
|
}
|
||||||
|
|
||||||
|
sprite_->setPosX(getPosX());
|
||||||
|
sprite_->setPosY(getPosY());
|
||||||
|
updateColliders();
|
||||||
|
}
|
||||||
|
|
||||||
|
creation_counter_s_ = std::max(0.0F, creation_counter_s_ - dt_s);
|
||||||
|
creation_counter_ = static_cast<Uint16>(creation_counter_s_ * 60.0F);
|
||||||
|
} else {
|
||||||
|
setBeingCreated(false);
|
||||||
|
setStop(false);
|
||||||
|
setVisible(true);
|
||||||
|
setInvulnerable(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rama de updateState: globo detenido (no creándose)
|
||||||
|
void Balloon::updateStateStopped(float dt_s) {
|
||||||
|
if (stopped_counter_s_ > 0.0F) {
|
||||||
|
stopped_counter_s_ = std::max(0.0F, stopped_counter_s_ - dt_s);
|
||||||
|
stopped_counter_ = static_cast<Uint16>(stopped_counter_s_ * 60.0F);
|
||||||
|
} else if (!isPopping()) {
|
||||||
|
setStop(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece la animación correspondiente al estado
|
||||||
|
void Balloon::updateAnimation(float dt_s) {
|
||||||
|
std::string creating_animation = "blue";
|
||||||
|
std::string normal_animation = "orange";
|
||||||
|
|
||||||
|
if (kind_ == POWER_BALL) {
|
||||||
|
creating_animation = "powerball";
|
||||||
|
normal_animation = "powerball";
|
||||||
|
} else if (getClass() == HEXAGON_CLASS) {
|
||||||
|
creating_animation = "red";
|
||||||
|
normal_animation = "green";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPopping()) {
|
||||||
|
sprite_->setCurrentAnimation("pop");
|
||||||
|
} else if (isBeingCreated()) {
|
||||||
|
sprite_->setCurrentAnimation(creating_animation);
|
||||||
|
} else {
|
||||||
|
sprite_->setCurrentAnimation(normal_animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
sprite_->animate(dt_s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si el globo está habilitado
|
||||||
|
auto Balloon::isEnabled() const -> bool {
|
||||||
|
return enabled_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Balloon::getPosX() const -> float {
|
||||||
|
return pos_x_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Balloon::getPosY() const -> float {
|
||||||
|
return pos_y_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Balloon::getWidth() const -> int {
|
||||||
|
return width_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Balloon::getHeight() const -> int {
|
||||||
|
return height_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable (px/s)
|
||||||
|
void Balloon::setVelY(float vel_y) {
|
||||||
|
this->vel_y_s_ = vel_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Balloon::setSpeed(float speed) {
|
||||||
|
this->speed_ = speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Balloon::getKind() const -> int {
|
||||||
|
return kind_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Balloon::getSize() const -> Uint8 {
|
||||||
|
return size_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene la clase a la que pertenece el globo
|
||||||
|
auto Balloon::getClass() const -> Uint8 {
|
||||||
|
if ((kind_ >= BALLOON_1) && (kind_ <= BALLOON_4)) {
|
||||||
|
return BALLOON_CLASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((kind_ >= HEXAGON_1) && (kind_ <= HEXAGON_4)) {
|
||||||
|
return HEXAGON_CLASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BALLOON_CLASS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable. Per a la PowerBall sincronitza la
|
||||||
|
// rotació del sprite: stopped=true ⇒ no roda, stopped=false ⇒ roda. Així,
|
||||||
|
// vingui d'on vingui la crida (FSM intern, Game::stopAll/startAllBalloons,
|
||||||
|
// fi de creació, etc.), la rotació sempre queda coherent amb l'estat.
|
||||||
|
void Balloon::setStop(bool state) {
|
||||||
|
stopped_ = state;
|
||||||
|
if (kind_ == POWER_BALL) {
|
||||||
|
sprite_->setRotate(!stopped_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Balloon::isStopped() const -> bool {
|
||||||
|
return stopped_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Balloon::setBlink(bool value) {
|
||||||
|
blinking_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Balloon::isBlinking() const -> bool {
|
||||||
|
return blinking_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Balloon::setVisible(bool value) {
|
||||||
|
visible_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Balloon::isVisible() const -> bool {
|
||||||
|
return visible_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Balloon::setInvulnerable(bool value) {
|
||||||
|
invulnerable_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Balloon::isInvulnerable() const -> bool {
|
||||||
|
return invulnerable_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Balloon::setBeingCreated(bool value) {
|
||||||
|
being_created_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Balloon::isBeingCreated() const -> bool {
|
||||||
|
return being_created_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Balloon::setPopping(bool value) {
|
||||||
|
popping_ = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Balloon::isPopping() const -> bool {
|
||||||
|
return popping_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Balloon::setStoppedTimer(Uint16 time) {
|
||||||
|
stopped_counter_ = time;
|
||||||
|
stopped_counter_s_ = static_cast<float>(time) / 60.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Balloon::getStoppedTimer() const -> Uint16 {
|
||||||
|
return stopped_counter_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Balloon::getScore() const -> Uint16 {
|
||||||
|
return score_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el circulo de colisión
|
||||||
|
auto Balloon::getCollider() -> Circle & {
|
||||||
|
return collider_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alinea el circulo de colisión con la posición del objeto globo
|
||||||
|
void Balloon::updateColliders() {
|
||||||
|
collider_.x = Uint16(pos_x_ + collider_.r);
|
||||||
|
collider_.y = pos_y_ + collider_.r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene le valor de la variable
|
||||||
|
auto Balloon::getMenace() const -> Uint8 {
|
||||||
|
if (isEnabled()) {
|
||||||
|
return menace_;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene le valor de la variable
|
||||||
|
auto Balloon::getPower() const -> Uint8 {
|
||||||
|
return power_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Balloon::bounceStart() {
|
||||||
|
bouncing_.enabled = true;
|
||||||
|
bouncing_.zoom_width = 1;
|
||||||
|
bouncing_.zoom_height = 1;
|
||||||
|
sprite_->setZoomW(bouncing_.zoom_width);
|
||||||
|
sprite_->setZoomH(bouncing_.zoom_height);
|
||||||
|
bouncing_.desp_x = 0;
|
||||||
|
bouncing_.desp_y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Balloon::bounceStop() {
|
||||||
|
bouncing_.enabled = false;
|
||||||
|
bouncing_.counter = 0;
|
||||||
|
bouncing_.zoom_width = 1.0F;
|
||||||
|
bouncing_.zoom_height = 1.0F;
|
||||||
|
sprite_->setZoomW(bouncing_.zoom_width);
|
||||||
|
sprite_->setZoomH(bouncing_.zoom_height);
|
||||||
|
bouncing_.desp_x = 0.0F;
|
||||||
|
bouncing_.desp_y = 0.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Balloon::updateBounce(float dt_s) {
|
||||||
|
if (!bouncing_.enabled) { return; }
|
||||||
|
|
||||||
|
bounce_phase_s_ += dt_s;
|
||||||
|
const float STEP_S = static_cast<float>(bouncing_.speed) * BOUNCE_STEP_S;
|
||||||
|
while (bounce_phase_s_ >= STEP_S) {
|
||||||
|
bounce_phase_s_ -= STEP_S;
|
||||||
|
bouncing_.counter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int IDX = bouncing_.counter / bouncing_.speed;
|
||||||
|
if (IDX > (MAX_BOUNCE - 1)) {
|
||||||
|
bounceStop();
|
||||||
|
bounce_phase_s_ = 0.0F;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bouncing_.zoom_width = bouncing_.w[IDX];
|
||||||
|
bouncing_.zoom_height = bouncing_.h[IDX];
|
||||||
|
sprite_->setZoomW(bouncing_.zoom_width);
|
||||||
|
sprite_->setZoomH(bouncing_.zoom_height);
|
||||||
|
bouncing_.desp_x = (sprite_->getSpriteClip().w - (sprite_->getSpriteClip().w * bouncing_.zoom_width));
|
||||||
|
bouncing_.desp_y = (sprite_->getSpriteClip().h - (sprite_->getSpriteClip().h * bouncing_.zoom_height));
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <string> // for string
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include "utils/utils.h" // for Circle
|
||||||
|
class AnimatedSprite;
|
||||||
|
class Texture;
|
||||||
|
|
||||||
|
// Clase Balloon
|
||||||
|
class Balloon {
|
||||||
|
public:
|
||||||
|
// Tipos de globo
|
||||||
|
static constexpr int BALLOON_1 = 1;
|
||||||
|
static constexpr int BALLOON_2 = 2;
|
||||||
|
static constexpr int BALLOON_3 = 3;
|
||||||
|
static constexpr int BALLOON_4 = 4;
|
||||||
|
static constexpr int HEXAGON_1 = 5;
|
||||||
|
static constexpr int HEXAGON_2 = 6;
|
||||||
|
static constexpr int HEXAGON_3 = 7;
|
||||||
|
static constexpr int HEXAGON_4 = 8;
|
||||||
|
static constexpr int POWER_BALL = 9;
|
||||||
|
|
||||||
|
// Puntos de globo
|
||||||
|
static constexpr int SCORE_1 = 50;
|
||||||
|
static constexpr int SCORE_2 = 100;
|
||||||
|
static constexpr int SCORE_3 = 200;
|
||||||
|
static constexpr int SCORE_4 = 400;
|
||||||
|
|
||||||
|
// Tamaños de globo
|
||||||
|
static constexpr int SIZE_1 = 1;
|
||||||
|
static constexpr int SIZE_2 = 2;
|
||||||
|
static constexpr int SIZE_3 = 3;
|
||||||
|
static constexpr int SIZE_4 = 4;
|
||||||
|
|
||||||
|
// Clases de globo
|
||||||
|
static constexpr int BALLOON_CLASS = 0;
|
||||||
|
static constexpr int HEXAGON_CLASS = 1;
|
||||||
|
|
||||||
|
// Velocitat horitzontal en px/s (era 0.7 px/frame * 60).
|
||||||
|
static constexpr float VELX_POSITIVE = 42.0F;
|
||||||
|
static constexpr float VELX_NEGATIVE = -42.0F;
|
||||||
|
|
||||||
|
// Velocidades a las que se mueven los globos
|
||||||
|
static constexpr float SPEED_1 = 0.60F;
|
||||||
|
static constexpr float SPEED_2 = 0.70F;
|
||||||
|
static constexpr float SPEED_3 = 0.80F;
|
||||||
|
static constexpr float SPEED_4 = 0.90F;
|
||||||
|
static constexpr float SPEED_5 = 1.00F;
|
||||||
|
|
||||||
|
// Tamaño de los globos
|
||||||
|
static constexpr int WIDTH_1 = 8;
|
||||||
|
static constexpr int WIDTH_2 = 13;
|
||||||
|
static constexpr int WIDTH_3 = 21;
|
||||||
|
static constexpr int WIDTH_4 = 37;
|
||||||
|
|
||||||
|
// PowerBall
|
||||||
|
static constexpr int POWERBALL_SCREENPOWER_MINIMUM = 10;
|
||||||
|
static constexpr int POWERBALL_COUNTER = 8;
|
||||||
|
|
||||||
|
Balloon(float x, float y, Uint8 kind, float velx, float speed, Uint16 creationtimer, Texture *texture, const std::vector<std::string> *animation, SDL_Renderer *renderer); // Constructor
|
||||||
|
~Balloon(); // Destructor
|
||||||
|
|
||||||
|
Balloon(const Balloon &) = delete;
|
||||||
|
auto operator=(const Balloon &) -> Balloon & = delete;
|
||||||
|
|
||||||
|
void allignTo(int x); // Centra el globo en la posición X
|
||||||
|
void render(); // Pinta el globo en la pantalla
|
||||||
|
void move(float dt_s); // Actualiza la posición y estados del globo
|
||||||
|
void disable(); // Deshabilita el globo y pone a cero todos los valores
|
||||||
|
void pop(); // Explosiona el globo
|
||||||
|
void update(float dt_s); // Actualiza al globo
|
||||||
|
|
||||||
|
[[nodiscard]] auto isEnabled() const -> bool; // Comprueba si el globo está habilitado
|
||||||
|
[[nodiscard]] auto isStopped() const -> bool; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto isBlinking() const -> bool; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto isVisible() const -> bool; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto isInvulnerable() const -> bool; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto isBeingCreated() const -> bool; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto isPopping() const -> bool; // Obtiene del valor de la variable
|
||||||
|
|
||||||
|
[[nodiscard]] auto getPosX() const -> float; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto getPosY() const -> float; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto getWidth() const -> int; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto getHeight() const -> int; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto getKind() const -> int; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto getSize() const -> Uint8; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto getClass() const -> Uint8; // Obtiene la clase a la que pertenece el globo
|
||||||
|
[[nodiscard]] auto getStoppedTimer() const -> Uint16; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto getScore() const -> Uint16; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto getMenace() const -> Uint8; // Obtiene le valor de la variable
|
||||||
|
[[nodiscard]] auto getPower() const -> Uint8; // Obtiene le valor de la variable
|
||||||
|
|
||||||
|
void setVelY(float vel_y); // Establece el valor de la variable
|
||||||
|
void setSpeed(float speed); // Establece el valor de la variable
|
||||||
|
void setStop(bool state); // Establece el valor de la variable
|
||||||
|
void setBlink(bool value); // Establece el valor de la variable
|
||||||
|
void setVisible(bool value); // Establece el valor de la variable
|
||||||
|
void setInvulnerable(bool value); // Establece el valor de la variable
|
||||||
|
void setPopping(bool value); // Establece el valor de la variable
|
||||||
|
void setStoppedTimer(Uint16 time); // Establece el valor de la variable
|
||||||
|
|
||||||
|
auto getCollider() -> Circle &; // Obtiene el circulo de colisión
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Cantidad de elementos del vector con los valores de la deformación del globo al rebotar
|
||||||
|
static constexpr int MAX_BOUNCE = 10;
|
||||||
|
|
||||||
|
// Time-based: la creació "desplaça" el globus cada 10 frames a 60Hz ⇒ 1/6 s.
|
||||||
|
static constexpr float CREATION_STEP_S = 10.0F / 60.0F;
|
||||||
|
// Time-based: el bounce avança w/h cada `speed_` frames a 60Hz; mantenim el mateix temps per pas.
|
||||||
|
static constexpr float BOUNCE_STEP_S = 1.0F / 60.0F; // 1 frame
|
||||||
|
|
||||||
|
// 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 zoom_width; // Zoom aplicado a la anchura
|
||||||
|
float zoom_height; // Zoom aplicado a la altura
|
||||||
|
float desp_x; // Desplazamiento de pixeles en el eje X antes de pintar el objeto con zoom
|
||||||
|
float desp_y; // 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 pos_x_; // Posición en el eje X
|
||||||
|
float pos_y_; // Posición en el eje Y
|
||||||
|
Uint8 width_; // Ancho
|
||||||
|
Uint8 height_; // Alto
|
||||||
|
float vel_x_s_{0.0F}; // Velocidad en X (px/s)
|
||||||
|
float vel_y_s_{0.0F}; // Velocidad en Y (px/s)
|
||||||
|
float gravity_s_{0.0F}; // Aceleración Y (px/s²)
|
||||||
|
float default_vel_y_s_{0.0F}; // Velocitat inicial al rebotar (px/s)
|
||||||
|
float max_vel_y_s_{0.0F}; // Velocitat màxima en Y (px/s, no aplicada en time-based actual)
|
||||||
|
bool being_created_; // 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 collider_; // Circulo de colisión del objeto
|
||||||
|
Uint16 creation_counter_; // Temporizador (frames, derivat de creation_counter_s_ per a render alpha)
|
||||||
|
Uint16 creation_counter_ini_; // Valor inicial del temporizador (frames)
|
||||||
|
float creation_counter_s_{0.0F}; // Temporizador (font de veritat, segons)
|
||||||
|
float creation_counter_ini_s_{0.0F}; // Valor inicial del temporizador (segons)
|
||||||
|
float creation_phase_s_{0.0F}; // Acumulador de fase per als steps de creació
|
||||||
|
Uint16 score_; // Puntos que da el globo al ser destruido
|
||||||
|
Uint16 stopped_counter_; // Contador (frames, derivat de stopped_counter_s_)
|
||||||
|
float stopped_counter_s_{0.0F}; // Contador (font de veritat, segons)
|
||||||
|
Uint8 kind_; // Tipo de globo
|
||||||
|
Uint8 menace_; // Cantidad de amenaza que genera el globo
|
||||||
|
float speed_; // Tempo del joc (multiplicador adimensional)
|
||||||
|
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
|
||||||
|
float bounce_phase_s_{0.0F}; // Fase del bounce
|
||||||
|
|
||||||
|
void updateColliders(); // Alinea el circulo de colisión con la posición del objeto globo
|
||||||
|
void bounceStart(); // Activa el efecto
|
||||||
|
void bounceStop(); // Detiene el efecto
|
||||||
|
void updateBounce(float dt_s); // Aplica el efecto
|
||||||
|
void updateAnimation(float dt_s); // Establece la animación correspondiente
|
||||||
|
void setBeingCreated(bool value); // Establece el valor de la variable
|
||||||
|
|
||||||
|
void updateState(float dt_s); // Actualiza los estados del globo
|
||||||
|
|
||||||
|
// Helpers de updateState, uno por cada rama de estado
|
||||||
|
void updateStatePopping();
|
||||||
|
void updateStateBeingCreated(float dt_s);
|
||||||
|
void updateStateStopped(float dt_s);
|
||||||
|
};
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
#include "game/entities/bullet.h"
|
||||||
|
|
||||||
|
#include "core/rendering/sprite.h" // for Sprite
|
||||||
|
#include "game/defaults.hpp" // for PLAY_AREA_LEFT, PLAY_AREA_RIGHT, PLAY_A...
|
||||||
|
class Texture;
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Bullet::Bullet(int x, int y, Bullet::Kind kind, bool powered_up, int owner, Texture *texture, SDL_Renderer *renderer) {
|
||||||
|
sprite_ = new Sprite({x, y, 10, 10}, texture, renderer);
|
||||||
|
|
||||||
|
// Posición inicial del objeto
|
||||||
|
pos_x_ = x;
|
||||||
|
pos_y_ = y;
|
||||||
|
pos_x_f_ = static_cast<float>(x);
|
||||||
|
pos_y_f_ = static_cast<float>(y);
|
||||||
|
|
||||||
|
// Alto y ancho del objeto
|
||||||
|
width_ = 10;
|
||||||
|
height_ = 10;
|
||||||
|
|
||||||
|
// Velocidad inicial en el eje Y
|
||||||
|
vel_y_s_ = VEL_Y_PX_PER_S;
|
||||||
|
|
||||||
|
// 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::Kind::UP:
|
||||||
|
// Establece la velocidad inicial
|
||||||
|
vel_x_s_ = 0.0F;
|
||||||
|
|
||||||
|
// Rectangulo con los gráficos del objeto
|
||||||
|
if (!powered_up) {
|
||||||
|
sprite_->setSpriteClip(0 * width_, 0, sprite_->getWidth(), sprite_->getHeight());
|
||||||
|
} else {
|
||||||
|
sprite_->setSpriteClip((0 + 3) * width_, 0, sprite_->getWidth(), sprite_->getHeight());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Bullet::Kind::LEFT:
|
||||||
|
// Establece la velocidad inicial
|
||||||
|
vel_x_s_ = VEL_X_LEFT_PX_PER_S;
|
||||||
|
|
||||||
|
// Rectangulo con los gráficos del objeto
|
||||||
|
if (!powered_up) {
|
||||||
|
sprite_->setSpriteClip(1 * width_, 0, sprite_->getWidth(), sprite_->getHeight());
|
||||||
|
} else {
|
||||||
|
sprite_->setSpriteClip((1 + 3) * width_, 0, sprite_->getWidth(), sprite_->getHeight());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Bullet::Kind::RIGHT:
|
||||||
|
// Establece la velocidad inicial
|
||||||
|
vel_x_s_ = VEL_X_RIGHT_PX_PER_S;
|
||||||
|
|
||||||
|
// Rectangulo con los gráficos del objeto
|
||||||
|
if (!powered_up) {
|
||||||
|
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
|
||||||
|
auto Bullet::move(float dt_s) -> MoveResult {
|
||||||
|
MoveResult msg = MoveResult::OK;
|
||||||
|
|
||||||
|
pos_x_f_ += vel_x_s_ * dt_s;
|
||||||
|
pos_x_ = static_cast<int>(pos_x_f_);
|
||||||
|
|
||||||
|
if ((pos_x_ < PLAY_AREA_LEFT - width_) || (pos_x_ > PLAY_AREA_RIGHT)) {
|
||||||
|
kind_ = Bullet::Kind::NONE;
|
||||||
|
msg = MoveResult::OUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos_y_f_ += vel_y_s_ * dt_s;
|
||||||
|
pos_y_ = static_cast<int>(pos_y_f_);
|
||||||
|
|
||||||
|
if (pos_y_ < PLAY_AREA_TOP - height_) {
|
||||||
|
kind_ = Bullet::Kind::NONE;
|
||||||
|
msg = MoveResult::OUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
sprite_->setPosX(pos_x_);
|
||||||
|
sprite_->setPosY(pos_y_);
|
||||||
|
|
||||||
|
shiftColliders();
|
||||||
|
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si el objeto está habilitado
|
||||||
|
auto Bullet::isEnabled() const -> bool {
|
||||||
|
return kind_ != Bullet::Kind::NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deshabilita el objeto
|
||||||
|
void Bullet::disable() {
|
||||||
|
kind_ = Bullet::Kind::NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto Bullet::getPosX() const -> int {
|
||||||
|
return pos_x_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto Bullet::getPosY() const -> int {
|
||||||
|
return pos_y_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Bullet::setPosX(int x) {
|
||||||
|
pos_x_ = x;
|
||||||
|
pos_x_f_ = static_cast<float>(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establece el valor de la variable
|
||||||
|
void Bullet::setPosY(int y) {
|
||||||
|
pos_y_ = y;
|
||||||
|
pos_y_f_ = static_cast<float>(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto Bullet::getKind() const -> Bullet::Kind {
|
||||||
|
return kind_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto Bullet::getOwner() const -> int {
|
||||||
|
return owner_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el circulo de colisión
|
||||||
|
auto Bullet::getCollider() -> Circle & {
|
||||||
|
return collider_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alinea el circulo de colisión con el objeto
|
||||||
|
void Bullet::shiftColliders() {
|
||||||
|
collider_.x = pos_x_ + collider_.r;
|
||||||
|
collider_.y = pos_y_ + collider_.r;
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint> // for uint8_t
|
||||||
|
|
||||||
|
#include "utils/utils.h" // for Circle
|
||||||
|
class Sprite;
|
||||||
|
class Texture;
|
||||||
|
|
||||||
|
// Clase Bullet
|
||||||
|
class Bullet {
|
||||||
|
public:
|
||||||
|
// Tipus de bala. Enum class fortament tipat per evitar confusió accidental
|
||||||
|
// amb altres `int`/`Uint8` (p.ex. el `owner` a `createBullet`).
|
||||||
|
enum class Kind : std::uint8_t {
|
||||||
|
NONE = 0, // bala desactivada / fora de pantalla
|
||||||
|
UP = 1,
|
||||||
|
LEFT = 2,
|
||||||
|
RIGHT = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resultado de Bullet::move()
|
||||||
|
enum class MoveResult : std::uint8_t {
|
||||||
|
OK = 0,
|
||||||
|
OUT = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
Bullet(int x, int y, Kind kind, bool powered_up, int owner, Texture *texture, SDL_Renderer *renderer); // Constructor
|
||||||
|
~Bullet(); // Destructor
|
||||||
|
|
||||||
|
Bullet(const Bullet &) = delete;
|
||||||
|
auto operator=(const Bullet &) -> Bullet & = delete;
|
||||||
|
|
||||||
|
void render(); // Pinta el objeto en pantalla
|
||||||
|
auto move(float dt_s) -> MoveResult; // Actualiza la posición y estado del objeto
|
||||||
|
void disable(); // Deshabilita el objeto
|
||||||
|
|
||||||
|
[[nodiscard]] auto isEnabled() const -> bool; // Comprueba si el objeto está habilitado
|
||||||
|
[[nodiscard]] auto getPosX() const -> int; // Obtiene el valor de la variable
|
||||||
|
[[nodiscard]] auto getPosY() const -> int; // Obtiene el valor de la variable
|
||||||
|
[[nodiscard]] auto getKind() const -> Kind; // Obtiene el valor de la variable
|
||||||
|
[[nodiscard]] auto getOwner() const -> int; // Obtiene el valor de la variable
|
||||||
|
|
||||||
|
void setPosX(int x); // Establece el valor de la variable
|
||||||
|
void setPosY(int y); // Establece el valor de la variable
|
||||||
|
|
||||||
|
auto getCollider() -> Circle &; // Obtiene el circulo de colisión
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Velocitats en px/s (derivades de les antigues px/frame * 60).
|
||||||
|
static constexpr float VEL_Y_PX_PER_S = -180.0F; // Era -3 px/frame
|
||||||
|
static constexpr float VEL_X_LEFT_PX_PER_S = -120.0F; // Era -2 px/frame
|
||||||
|
static constexpr float VEL_X_RIGHT_PX_PER_S = 120.0F; // Era +2 px/frame
|
||||||
|
|
||||||
|
// Objetos y punteros
|
||||||
|
Sprite *sprite_; // Sprite con los graficos y métodos de pintado
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
int pos_x_; // Posición en el eje X (px enters per al sprite/collider)
|
||||||
|
int pos_y_; // Posición en el eje Y
|
||||||
|
float pos_x_f_{0}; // Acumulador subpíxel
|
||||||
|
float pos_y_f_{0}; // Acumulador subpíxel
|
||||||
|
Uint8 width_; // Ancho del objeto
|
||||||
|
Uint8 height_; // Alto del objeto
|
||||||
|
float vel_x_s_{0}; // Velocidad en el eje X (px/s)
|
||||||
|
float vel_y_s_{0}; // Velocidad en el eje Y (px/s)
|
||||||
|
Kind kind_; // Tipo de objeto
|
||||||
|
int owner_; // Identificador del dueño del objeto
|
||||||
|
Circle collider_; // Circulo de colisión del objeto
|
||||||
|
|
||||||
|
void shiftColliders(); // Alinea el circulo de colisión con el objeto
|
||||||
|
};
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
#include "game/entities/item.h"
|
||||||
|
|
||||||
|
#include <algorithm> // for max
|
||||||
|
#include <cstdlib> // for rand
|
||||||
|
|
||||||
|
#include "core/rendering/animatedsprite.h" // for AnimatedSprite
|
||||||
|
#include "game/defaults.hpp" // for PLAY_AREA_LEFT, PLAY_AREA_RIGHT, PLAY_AR...
|
||||||
|
class Texture;
|
||||||
|
|
||||||
|
// Constructor
|
||||||
|
Item::Item(Id id, float x, float y, Texture *texture, const std::vector<std::string> *animation, SDL_Renderer *renderer) {
|
||||||
|
sprite_ = new AnimatedSprite(texture, renderer, "", animation);
|
||||||
|
|
||||||
|
this->id_ = id;
|
||||||
|
enabled_ = true;
|
||||||
|
time_to_live_ = 600;
|
||||||
|
time_to_live_s_ = TIME_TO_LIVE_S;
|
||||||
|
accel_x_s_ = 0.0F;
|
||||||
|
floor_collision_ = false;
|
||||||
|
|
||||||
|
if (id == Item::Id::COFFEE_MACHINE) {
|
||||||
|
width_ = 23;
|
||||||
|
height_ = 29;
|
||||||
|
pos_x_ = (((int)x + (PLAY_AREA_WIDTH / 2)) % (PLAY_AREA_WIDTH - width_ - 5)) + 2;
|
||||||
|
pos_y_ = PLAY_AREA_TOP - height_;
|
||||||
|
vel_x_s_ = 0.0F;
|
||||||
|
vel_y_s_ = COFFEE_VEL_Y_PX_PER_S;
|
||||||
|
accel_y_s_ = COFFEE_ACCEL_Y_PX_PER_S2;
|
||||||
|
collider_.r = 10;
|
||||||
|
} else {
|
||||||
|
width_ = 16;
|
||||||
|
height_ = 16;
|
||||||
|
pos_x_ = x;
|
||||||
|
pos_y_ = y;
|
||||||
|
// Distribució original: -1.0, -0.5, 0.0, 0.5, 1.0 px/frame ⇒ -60..60 px/s en passos de 30.
|
||||||
|
const int RAND_STEP = rand() % 5;
|
||||||
|
vel_x_s_ = (-2.0F + static_cast<float>(RAND_STEP)) * ITEM_VEL_X_STEP_PX_PER_S;
|
||||||
|
vel_y_s_ = ITEM_VEL_Y_PX_PER_S;
|
||||||
|
accel_y_s_ = ITEM_ACCEL_Y_PX_PER_S2;
|
||||||
|
collider_.r = width_ / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
sprite_->setPosX(pos_x_);
|
||||||
|
sprite_->setPosY(pos_y_);
|
||||||
|
shiftColliders();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
Item::~Item() {
|
||||||
|
delete sprite_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Centra el objeto en la posición X
|
||||||
|
void Item::allignTo(int x) {
|
||||||
|
pos_x_ = float(x - (width_ / 2));
|
||||||
|
|
||||||
|
if (pos_x_ < PLAY_AREA_LEFT) {
|
||||||
|
pos_x_ = PLAY_AREA_LEFT + 1;
|
||||||
|
} else if ((pos_x_ + width_) > PLAY_AREA_RIGHT) {
|
||||||
|
pos_x_ = float(PLAY_AREA_RIGHT - width_ - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Posición X,Y del sprite
|
||||||
|
sprite_->setPosX(int(pos_x_));
|
||||||
|
sprite_->setPosY(int(pos_y_));
|
||||||
|
|
||||||
|
// Alinea el circulo de colisión con el objeto
|
||||||
|
shiftColliders();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pinta el objeto en la pantalla
|
||||||
|
void Item::render() {
|
||||||
|
// Mentre quede temps de sobra (>200) es renderitza sempre; quan està a
|
||||||
|
// punt d'expirar, parpalleja alternant 10 frames visibles i 10 invisibles.
|
||||||
|
if (enabled_ && (time_to_live_ > 200 || time_to_live_ % 20 > 10)) {
|
||||||
|
sprite_->render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualiza la posición y estados del objeto
|
||||||
|
void Item::move(float dt_s) {
|
||||||
|
floor_collision_ = false;
|
||||||
|
|
||||||
|
// Posició
|
||||||
|
pos_x_ += vel_x_s_ * dt_s;
|
||||||
|
pos_y_ += vel_y_s_ * dt_s;
|
||||||
|
|
||||||
|
// Acceleració
|
||||||
|
vel_x_s_ += accel_x_s_ * dt_s;
|
||||||
|
vel_y_s_ += accel_y_s_ * dt_s;
|
||||||
|
|
||||||
|
// Si surt per laterals, corregeix i inverteix
|
||||||
|
if ((pos_x_ < PLAY_AREA_LEFT) || (pos_x_ + width_ > PLAY_AREA_RIGHT)) {
|
||||||
|
pos_x_ -= vel_x_s_ * dt_s;
|
||||||
|
vel_x_s_ = -vel_x_s_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebot per dalt (excepte la màquina de cafè)
|
||||||
|
if ((pos_y_ < PLAY_AREA_TOP) && !(id_ == Item::Id::COFFEE_MACHINE)) {
|
||||||
|
pos_y_ -= vel_y_s_ * dt_s;
|
||||||
|
vel_y_s_ = -vel_y_s_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Topa amb el terra
|
||||||
|
if (pos_y_ + height_ > PLAY_AREA_BOTTOM) {
|
||||||
|
vel_y_s_ = 0;
|
||||||
|
vel_x_s_ = 0;
|
||||||
|
accel_x_s_ = 0;
|
||||||
|
accel_y_s_ = 0;
|
||||||
|
pos_y_ = PLAY_AREA_BOTTOM - height_;
|
||||||
|
if (id_ == Item::Id::COFFEE_MACHINE) {
|
||||||
|
floor_collision_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sprite_->setPosX(int(pos_x_));
|
||||||
|
sprite_->setPosY(int(pos_y_));
|
||||||
|
shiftColliders();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pone a cero todos los valores del objeto
|
||||||
|
void Item::disable() {
|
||||||
|
enabled_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualiza el objeto a su posicion, animación y controla los contadores
|
||||||
|
void Item::update(float dt_s) {
|
||||||
|
move(dt_s);
|
||||||
|
sprite_->animate(dt_s);
|
||||||
|
updateTimeToLive(dt_s);
|
||||||
|
checkTimeToLive();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualiza el contador. Manté time_to_live_ (frames) sincronitzat amb el
|
||||||
|
// segons per a que render() segueixi funcionant amb la mateixa condició de
|
||||||
|
// parpelleig.
|
||||||
|
void Item::updateTimeToLive(float dt_s) {
|
||||||
|
if (time_to_live_s_ > 0.0F) {
|
||||||
|
time_to_live_s_ = std::max(0.0F, time_to_live_s_ - dt_s);
|
||||||
|
}
|
||||||
|
constexpr float FRAMES_PER_S = 60.0F;
|
||||||
|
time_to_live_ = static_cast<Uint16>(time_to_live_s_ * FRAMES_PER_S);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprueba si el objeto sigue vivo
|
||||||
|
void Item::checkTimeToLive() {
|
||||||
|
if (time_to_live_ == 0) {
|
||||||
|
disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Item::getPosX() const -> float {
|
||||||
|
return pos_x_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Item::getPosY() const -> float {
|
||||||
|
return pos_y_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Item::getWidth() const -> int {
|
||||||
|
return width_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Item::getHeight() const -> int {
|
||||||
|
return height_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene del valor de la variable
|
||||||
|
auto Item::getId() const -> Id {
|
||||||
|
return id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el valor de la variable
|
||||||
|
auto Item::isEnabled() const -> bool {
|
||||||
|
return enabled_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtiene el circulo de colisión
|
||||||
|
auto Item::getCollider() -> Circle & {
|
||||||
|
return collider_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alinea el circulo de colisión con la posición del objeto
|
||||||
|
void Item::shiftColliders() {
|
||||||
|
collider_.x = int(pos_x_ + (width_ / 2));
|
||||||
|
collider_.y = int(pos_y_ + (height_ / 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Informa si el objeto ha colisionado con el suelo
|
||||||
|
auto Item::isOnFloor() const -> bool {
|
||||||
|
return floor_collision_;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <string> // for string
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
|
#include "utils/utils.h" // for Circle
|
||||||
|
class AnimatedSprite;
|
||||||
|
class Texture;
|
||||||
|
|
||||||
|
// Clase Item
|
||||||
|
class Item {
|
||||||
|
public:
|
||||||
|
// Tipos de objetos
|
||||||
|
enum class Id : Uint8 {
|
||||||
|
NONE = 0,
|
||||||
|
DISK = 1,
|
||||||
|
GAVINA = 2,
|
||||||
|
PACMAR = 3,
|
||||||
|
CLOCK = 4,
|
||||||
|
COFFEE = 5,
|
||||||
|
COFFEE_MACHINE = 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
Item(Id id, float x, float y, Texture *texture, const std::vector<std::string> *animation, SDL_Renderer *renderer); // Constructor
|
||||||
|
~Item(); // Destructor
|
||||||
|
|
||||||
|
Item(const Item &) = delete;
|
||||||
|
auto operator=(const Item &) -> Item & = delete;
|
||||||
|
|
||||||
|
void allignTo(int x); // Centra el objeto en la posición X
|
||||||
|
void render(); // Pinta el objeto en la pantalla
|
||||||
|
void disable(); // Pone a cero todos los valores del objeto
|
||||||
|
void update(float dt_s); // Actualiza al objeto
|
||||||
|
void updateTimeToLive(float dt_s); // Actualiza el contador
|
||||||
|
void checkTimeToLive(); // Comprueba si el objeto sigue vivo
|
||||||
|
|
||||||
|
[[nodiscard]] auto getPosX() const -> float; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto getPosY() const -> float; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto getWidth() const -> int; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto getHeight() const -> int; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto getId() const -> Id; // Obtiene del valor de la variable
|
||||||
|
[[nodiscard]] auto isEnabled() const -> bool; // Obtiene el valor de la variable
|
||||||
|
[[nodiscard]] auto isOnFloor() const -> bool; // Informa si el objeto ha colisionado con el suelo
|
||||||
|
|
||||||
|
auto getCollider() -> Circle &; // Obtiene el circulo de colisión
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Time-based: equivalents en unitats físiques precalculades a 60Hz.
|
||||||
|
static constexpr float ITEM_VEL_Y_PX_PER_S = -240.0F; // Era -4.0 px/frame
|
||||||
|
static constexpr float ITEM_ACCEL_Y_PX_PER_S2 = 720.0F; // Era +0.2 px/frame²
|
||||||
|
static constexpr float ITEM_VEL_X_STEP_PX_PER_S = 30.0F; // Era 0.5 px/frame, 5 valors en [-1.0, 1.0]
|
||||||
|
static constexpr float COFFEE_VEL_Y_PX_PER_S = -6.0F; // Era -0.1 px/frame
|
||||||
|
static constexpr float COFFEE_ACCEL_Y_PX_PER_S2 = 360.0F; // Era +0.1 px/frame²
|
||||||
|
static constexpr float TIME_TO_LIVE_S = 10.0F; // Era 600 frames
|
||||||
|
static constexpr float BLINK_START_S = TIME_TO_LIVE_S - (200.0F / 60.0F); // Era time_to_live_ > 200
|
||||||
|
static constexpr float BLINK_PERIOD_S = 20.0F / 60.0F; // Era % 20
|
||||||
|
static constexpr float BLINK_OFF_S = 10.0F / 60.0F; // Era % 20 <= 10
|
||||||
|
|
||||||
|
// Objetos y punteros
|
||||||
|
AnimatedSprite *sprite_; // Sprite con los graficos del objeto
|
||||||
|
|
||||||
|
// Variables
|
||||||
|
float pos_x_; // Posición X del objeto
|
||||||
|
float pos_y_; // Posición Y del objeto
|
||||||
|
Uint8 width_; // Ancho del objeto
|
||||||
|
Uint8 height_; // Alto del objeto
|
||||||
|
float vel_x_s_{0.0F}; // Velocidad en el eje X (px/s)
|
||||||
|
float vel_y_s_{0.0F}; // Velocidad en el eje Y (px/s)
|
||||||
|
float accel_x_s_{0.0F}; // Aceleración en el eje X (px/s²)
|
||||||
|
float accel_y_s_{0.0F}; // Aceleración en el eje Y (px/s²)
|
||||||
|
bool floor_collision_; // Indica si el objeto colisiona con el suelo
|
||||||
|
Id id_; // Especifica el tipo de objeto que es
|
||||||
|
bool enabled_; // Especifica si el objeto está habilitado
|
||||||
|
Uint16 time_to_live_; // Temporizador (frames, derivat de time_to_live_s_ per a render())
|
||||||
|
float time_to_live_s_{0.0F}; // Temporizador (font de veritat, segons)
|
||||||
|
Circle collider_; // Circulo de colisión del objeto
|
||||||
|
|
||||||
|
void shiftColliders(); // Alinea el circulo de colisión con la posición del objeto
|
||||||
|
void move(float dt_s); // Actualiza la posición y estados del objeto
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user