Compare commits
33 Commits
v1.2
...
d89141e014
| Author | SHA1 | Date | |
|---|---|---|---|
| d89141e014 | |||
| b65a615be2 | |||
| da56a81bc3 | |||
| a95b4bd1b6 | |||
| b0c95111a2 | |||
| 96763847fb | |||
| dcb004b5a7 | |||
| 70aa58ec46 | |||
| e1bc1b597f | |||
| b984e6041e | |||
| ae359f4a1e | |||
| ae89b252e2 | |||
| 35cdd88cbb | |||
| 4cac807ce2 | |||
| bbcc10da81 | |||
| 9d30dd538c | |||
| 1e00f5c3a4 | |||
| 7789c1c217 | |||
| ec3cb78f6b | |||
| f37308a5f0 | |||
| 1ce0d9c56c | |||
| 08f587ffe4 | |||
| bf7be3a7f1 | |||
| a48fe51f73 | |||
| 0b82be193f | |||
| 8676c0e773 | |||
| b7a551c158 | |||
| 358e91ea30 | |||
| 1aa0e96a91 | |||
| 6ac16ebfeb | |||
| 5dcda36553 | |||
| 41d429fc10 | |||
| 4435bc4942 |
+71
-43
@@ -8,72 +8,100 @@ Checks:
|
||||
- -bugprone-integer-division
|
||||
- -bugprone-easily-swappable-parameters
|
||||
- -bugprone-narrowing-conversions
|
||||
- -modernize-avoid-c-arrays,-warnings-as-errors
|
||||
- -modernize-avoid-c-arrays
|
||||
|
||||
WarningsAsErrors: '*'
|
||||
# Solo headers del propio código fuente (external/ y spv/ tienen su propio .clang-tidy dummy)
|
||||
HeaderFilterRegex: 'source/.*'
|
||||
# Headers nostres (excloem source/external/ que conté dependències de tercers no editables)
|
||||
HeaderFilterRegex: 'source/(core|game|utils)/'
|
||||
FormatStyle: file
|
||||
|
||||
CheckOptions:
|
||||
# bugprone-empty-catch: aceptar catches vacíos marcados con @INTENTIONAL en un comentario
|
||||
- { key: bugprone-empty-catch.IgnoreCatchWithKeywords, value: '@INTENTIONAL' }
|
||||
|
||||
# Variables locales en snake_case
|
||||
# =====================================================================
|
||||
# CONSTANTES → UPPER_CASE (compile-time y runtime, en cualquier scope)
|
||||
# =====================================================================
|
||||
# Todo lo que sea const o constexpr se identifica visualmente en UPPER_CASE,
|
||||
# sin importar si es global, local, miembro o static.
|
||||
|
||||
# constexpr en cualquier scope (globales y locales)
|
||||
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE }
|
||||
|
||||
# Constantes globales (const no-constexpr)
|
||||
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
|
||||
|
||||
# Constantes locales (const en función)
|
||||
- { key: readability-identifier-naming.LocalConstantCase, value: UPPER_CASE }
|
||||
|
||||
# Static const a nivel de archivo/namespace
|
||||
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }
|
||||
|
||||
# Miembros static const/constexpr de clase (p.ej. static constexpr int MAX = 100;)
|
||||
- { key: readability-identifier-naming.ClassConstantCase, value: UPPER_CASE }
|
||||
|
||||
# Miembros const no-static de clase (p.ej. const int limit;)
|
||||
- { key: readability-identifier-naming.ConstantMemberCase, value: UPPER_CASE }
|
||||
|
||||
# Valores de enums
|
||||
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
|
||||
|
||||
# NOTA: Los parámetros const NO se tratan como constantes aquí.
|
||||
# Un parámetro sigue siendo un parámetro aunque sea const → hereda ParameterCase.
|
||||
|
||||
# =====================================================================
|
||||
# VARIABLES NO-CONST
|
||||
# =====================================================================
|
||||
|
||||
# Variables locales
|
||||
- { key: readability-identifier-naming.VariableCase, value: lower_case }
|
||||
- { key: readability-identifier-naming.LocalVariableCase, value: lower_case }
|
||||
|
||||
# Miembros privados en snake_case con sufijo _
|
||||
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
|
||||
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
|
||||
# Parámetros de función
|
||||
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
|
||||
|
||||
# Miembros protegidos en snake_case con sufijo _
|
||||
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
|
||||
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
|
||||
|
||||
# Miembros públicos en snake_case (sin sufijo)
|
||||
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
|
||||
|
||||
# Namespaces en CamelCase
|
||||
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
|
||||
|
||||
# Variables estáticas privadas como miembros privados
|
||||
# Variables estáticas no-const (static locales, static file-scope,
|
||||
# y static members no-const de clase como el instance_ de un Singleton).
|
||||
# Sufijo _ para marcar que tienen storage estático.
|
||||
- { key: readability-identifier-naming.StaticVariableCase, value: lower_case }
|
||||
- { key: readability-identifier-naming.StaticVariableSuffix, value: _ }
|
||||
|
||||
# Constantes estáticas sin sufijo
|
||||
- { key: readability-identifier-naming.StaticConstantCase, value: UPPER_CASE }
|
||||
# =====================================================================
|
||||
# MIEMBROS DE CLASE NO-CONST
|
||||
# =====================================================================
|
||||
# Privados: snake_case con sufijo _
|
||||
- { key: readability-identifier-naming.PrivateMemberCase, value: lower_case }
|
||||
- { key: readability-identifier-naming.PrivateMemberSuffix, value: _ }
|
||||
|
||||
# Constantes globales en UPPER_CASE
|
||||
- { key: readability-identifier-naming.GlobalConstantCase, value: UPPER_CASE }
|
||||
# Protegidos: snake_case con sufijo _
|
||||
- { key: readability-identifier-naming.ProtectedMemberCase, value: lower_case }
|
||||
- { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ }
|
||||
|
||||
# Variables constexpr globales en UPPER_CASE
|
||||
- { key: readability-identifier-naming.ConstexprVariableCase, value: UPPER_CASE }
|
||||
# Públicos: snake_case sin sufijo
|
||||
- { key: readability-identifier-naming.PublicMemberCase, value: lower_case }
|
||||
|
||||
# Constantes locales en UPPER_CASE
|
||||
- { key: readability-identifier-naming.LocalConstantCase, value: UPPER_CASE }
|
||||
|
||||
# Constexpr miembros en UPPER_CASE (sin sufijo)
|
||||
- { key: readability-identifier-naming.ConstexprMemberCase, value: UPPER_CASE }
|
||||
|
||||
# Constexpr miembros privados/protegidos con sufijo _
|
||||
- { key: readability-identifier-naming.ConstexprMethodCase, value: UPPER_CASE }
|
||||
|
||||
# Clases, structs y enums en CamelCase
|
||||
# =====================================================================
|
||||
# TIPOS
|
||||
# =====================================================================
|
||||
- { key: readability-identifier-naming.ClassCase, value: CamelCase }
|
||||
- { key: readability-identifier-naming.StructCase, value: CamelCase }
|
||||
- { key: readability-identifier-naming.EnumCase, value: CamelCase }
|
||||
- { key: readability-identifier-naming.UnionCase, value: CamelCase }
|
||||
- { key: readability-identifier-naming.TypeAliasCase, value: CamelCase }
|
||||
- { key: readability-identifier-naming.TypedefCase, value: CamelCase }
|
||||
- { key: readability-identifier-naming.TemplateParameterCase, value: CamelCase }
|
||||
|
||||
# Valores de enums en UPPER_CASE
|
||||
- { key: readability-identifier-naming.EnumConstantCase, value: UPPER_CASE }
|
||||
# Namespaces
|
||||
- { key: readability-identifier-naming.NamespaceCase, value: CamelCase }
|
||||
|
||||
# Métodos en camelBack (sin sufijos)
|
||||
# =====================================================================
|
||||
# FUNCIONES Y MÉTODOS (incluyendo constexpr)
|
||||
# =====================================================================
|
||||
# Un método/función constexpr es un invocable, no una constante → camelBack.
|
||||
- { key: readability-identifier-naming.FunctionCase, value: camelBack }
|
||||
- { key: readability-identifier-naming.ConstexprFunctionCase, value: camelBack }
|
||||
- { key: readability-identifier-naming.MethodCase, value: camelBack }
|
||||
- { key: readability-identifier-naming.PrivateMethodCase, value: camelBack }
|
||||
- { key: readability-identifier-naming.ProtectedMethodCase, value: camelBack }
|
||||
- { key: readability-identifier-naming.PublicMethodCase, value: camelBack }
|
||||
|
||||
# Funciones en camelBack
|
||||
- { key: readability-identifier-naming.FunctionCase, value: camelBack }
|
||||
|
||||
# Parámetros en lower_case
|
||||
- { key: readability-identifier-naming.ParameterCase, value: lower_case }
|
||||
- { key: readability-identifier-naming.ConstexprMethodCase, value: camelBack }
|
||||
|
||||
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
|
||||
+46
-27
@@ -79,23 +79,23 @@ set(APP_SOURCES
|
||||
source/core/system/director.cpp
|
||||
|
||||
# Scenes (cinemàtiques i menús reescrits)
|
||||
source/scenes/timeline.cpp
|
||||
source/scenes/sprite_mover.cpp
|
||||
source/scenes/frame_animator.cpp
|
||||
source/scenes/palette_fade.cpp
|
||||
source/scenes/surface_handle.cpp
|
||||
source/scenes/scene_registry.cpp
|
||||
source/scenes/scene_utils.cpp
|
||||
source/scenes/boot_loader_scene.cpp
|
||||
source/scenes/mort_scene.cpp
|
||||
source/scenes/banner_scene.cpp
|
||||
source/scenes/menu_scene.cpp
|
||||
source/scenes/intro_new_logo_scene.cpp
|
||||
source/scenes/intro_scene.cpp
|
||||
source/scenes/intro_sprites_scene.cpp
|
||||
source/scenes/slides_scene.cpp
|
||||
source/scenes/credits_scene.cpp
|
||||
source/scenes/secreta_scene.cpp
|
||||
source/game/scenes/timeline.cpp
|
||||
source/game/scenes/sprite_mover.cpp
|
||||
source/game/scenes/frame_animator.cpp
|
||||
source/game/scenes/palette_fade.cpp
|
||||
source/game/scenes/surface_handle.cpp
|
||||
source/game/scenes/scene_registry.cpp
|
||||
source/game/scenes/scene_utils.cpp
|
||||
source/game/scenes/boot_loader_scene.cpp
|
||||
source/game/scenes/mort_scene.cpp
|
||||
source/game/scenes/banner_scene.cpp
|
||||
source/game/scenes/menu_scene.cpp
|
||||
source/game/scenes/intro_new_logo_scene.cpp
|
||||
source/game/scenes/intro_scene.cpp
|
||||
source/game/scenes/intro_sprites_scene.cpp
|
||||
source/game/scenes/slides_scene.cpp
|
||||
source/game/scenes/credits_scene.cpp
|
||||
source/game/scenes/secreta_scene.cpp
|
||||
|
||||
# Game
|
||||
source/game/options.cpp
|
||||
@@ -153,14 +153,12 @@ if(NOT APPLE AND NOT EMSCRIPTEN)
|
||||
"${HEADERS_DIR}/postfx_vert_spv.h"
|
||||
"${HEADERS_DIR}/postfx_frag_spv.h"
|
||||
"${HEADERS_DIR}/upscale_frag_spv.h"
|
||||
"${HEADERS_DIR}/downscale_frag_spv.h"
|
||||
"${HEADERS_DIR}/crtpi_frag_spv.h"
|
||||
)
|
||||
set(ALL_SHADER_SOURCES
|
||||
"${SHADERS_DIR}/postfx.vert"
|
||||
"${SHADERS_DIR}/postfx.frag"
|
||||
"${SHADERS_DIR}/upscale.frag"
|
||||
"${SHADERS_DIR}/downscale.frag"
|
||||
"${SHADERS_DIR}/crtpi_frag.glsl"
|
||||
)
|
||||
|
||||
@@ -218,6 +216,12 @@ target_include_directories(${PROJECT_NAME} PUBLIC
|
||||
"${CMAKE_BINARY_DIR}"
|
||||
)
|
||||
|
||||
# Capçaleres de tercers a source/external/ — tractades com a sistema per
|
||||
# silenciar warnings (gif.h, etc.) que no controlem.
|
||||
target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC
|
||||
"${CMAKE_SOURCE_DIR}/source/external"
|
||||
)
|
||||
|
||||
# Enlazar SDL3
|
||||
if(APPLE AND MACOS_BUNDLE)
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE MACOS_BUNDLE)
|
||||
@@ -234,7 +238,7 @@ else()
|
||||
endif()
|
||||
|
||||
# --- FLAGS DE COMPILACIÓN ---
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE -Wall)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE $<$<CONFIG:RELEASE>:-Os -ffunction-sections -fdata-sections>)
|
||||
|
||||
# --- CONFIGURACIÓN POR PLATAFORMA ---
|
||||
@@ -260,11 +264,6 @@ elseif(EMSCRIPTEN)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".html")
|
||||
endif()
|
||||
|
||||
# Ejecutable en la raíz del proyecto (solo nativos). A Emscripten queda dins build/.
|
||||
if(NOT EMSCRIPTEN)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR})
|
||||
endif()
|
||||
|
||||
# --- EINA STANDALONE: pack_resources ---
|
||||
# Executable auxiliar que empaqueta `data/` a `resources.pack` (format AEE1).
|
||||
# No es compila per defecte (EXCLUDE_FROM_ALL). Build explícit:
|
||||
@@ -276,14 +275,14 @@ if(NOT EMSCRIPTEN)
|
||||
source/core/resources/resource_pack.cpp
|
||||
)
|
||||
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
|
||||
target_compile_options(pack_resources PRIVATE -Wall)
|
||||
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
|
||||
|
||||
# --- Regeneració automàtica de resources.pack ---
|
||||
# Cada `cmake --build build` torna a empaquetar `data/` si algun fitxer ha
|
||||
# canviat. Evita debugar amb un pack obsolet. CONFIGURE_DEPENDS força CMake
|
||||
# a re-globbar a la pròxima invocació (recull fitxers nous afegits a data/).
|
||||
file(GLOB_RECURSE DATA_FILES CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/data/*")
|
||||
set(RESOURCE_PACK "${CMAKE_SOURCE_DIR}/resources.pack")
|
||||
set(RESOURCE_PACK "${CMAKE_BINARY_DIR}/resources.pack")
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${RESOURCE_PACK}
|
||||
@@ -298,6 +297,26 @@ if(NOT EMSCRIPTEN)
|
||||
|
||||
add_custom_target(resource_pack ALL DEPENDS ${RESOURCE_PACK})
|
||||
add_dependencies(${PROJECT_NAME} resource_pack)
|
||||
|
||||
# --- CÒPIA DE gamecontrollerdb.txt AL COSTAT DEL BINARI ---
|
||||
# SDL_AddGamepadMappingsFromFile només llegeix del filesystem real (no del
|
||||
# pack), així que el fitxer ha de viure al directori del binari. Es copia
|
||||
# només si existeix per no fallar la build d'algú que encara no ha fet
|
||||
# `make controllerdb`.
|
||||
if(EXISTS "${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt")
|
||||
set(CONTROLLER_DB "${CMAKE_BINARY_DIR}/gamecontrollerdb.txt")
|
||||
add_custom_command(
|
||||
OUTPUT ${CONTROLLER_DB}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt"
|
||||
"${CONTROLLER_DB}"
|
||||
DEPENDS "${CMAKE_SOURCE_DIR}/gamecontrollerdb.txt"
|
||||
COMMENT "Copiant gamecontrollerdb.txt → build/"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(controller_db ALL DEPENDS ${CONTROLLER_DB})
|
||||
add_dependencies(${PROJECT_NAME} controller_db)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# --- STATIC ANALYSIS TARGETS ---
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
# DIRECTORIES
|
||||
# ==============================================================================
|
||||
DIR_ROOT := $(dir $(abspath $(MAKEFILE_LIST)))
|
||||
DIR_BIN := $(addsuffix /, $(DIR_ROOT))
|
||||
BUILDDIR := build
|
||||
|
||||
# ==============================================================================
|
||||
# TOOLS
|
||||
# ==============================================================================
|
||||
SHADER_CMAKE := $(DIR_ROOT)tools/shaders/compile_spirv.cmake
|
||||
SHADERS_DIR := $(DIR_ROOT)data/shaders
|
||||
HEADERS_DIR := $(DIR_ROOT)source/core/rendering/sdl3gpu
|
||||
HEADERS_DIR := $(DIR_ROOT)source/core/rendering/sdl3gpu/spv
|
||||
ifeq ($(OS),Windows_NT)
|
||||
GLSLC := $(shell where glslc 2>NUL)
|
||||
else
|
||||
@@ -20,7 +20,7 @@ endif
|
||||
# TARGET NAMES
|
||||
# ==============================================================================
|
||||
TARGET_NAME := aee
|
||||
TARGET_FILE := $(DIR_BIN)$(TARGET_NAME)
|
||||
TARGET_FILE := $(BUILDDIR)/$(TARGET_NAME)
|
||||
APP_NAME := Aventures en Egipte
|
||||
DIST_DIR := dist
|
||||
RELEASE_FOLDER := dist/_tmp
|
||||
@@ -61,7 +61,7 @@ endif
|
||||
# WINDOWS-SPECIFIC VARIABLES
|
||||
# ==============================================================================
|
||||
ifeq ($(OS),Windows_NT)
|
||||
WIN_TARGET_FILE := $(DIR_BIN)$(APP_NAME)
|
||||
WIN_TARGET_FILE := $(BUILDDIR)/$(APP_NAME)
|
||||
WIN_RELEASE_FILE := $(RELEASE_FOLDER)/$(APP_NAME)
|
||||
# Escapa apòstrofs per a PowerShell (duplica ' → ''). Sense això, APP_NAMEs
|
||||
# com "JailDoctor's Dilemma" trencarien el parsing de -Destination '...'.
|
||||
@@ -96,12 +96,31 @@ else
|
||||
endif
|
||||
|
||||
# ==============================================================================
|
||||
# CMAKE GENERATOR (Windows needs explicit MinGW Makefiles generator)
|
||||
# CMAKE GENERATOR (usa Ninja si está disponible; si no, MinGW Makefiles en
|
||||
# Windows / generador por defecto en Linux/macOS). Ninja paraleliza mejor.
|
||||
# ==============================================================================
|
||||
ifeq ($(OS),Windows_NT)
|
||||
CMAKE_GEN := -G "MinGW Makefiles"
|
||||
# Dins MSYS2/Git Bash/MinGW, $(shell ...) usa sh.exe i "NUL" NO és
|
||||
# dispositiu — un redirect "2>NUL" crearia un fitxer literal anomenat
|
||||
# NUL al cwd. Detectem MSYSTEM per usar /dev/null en aquests entorns.
|
||||
ifneq ($(MSYSTEM),)
|
||||
NULDEV := /dev/null
|
||||
else
|
||||
NULDEV := NUL
|
||||
endif
|
||||
HAS_NINJA := $(shell ninja --version 2>$(NULDEV))
|
||||
ifneq ($(HAS_NINJA),)
|
||||
CMAKE_GEN := -G "Ninja"
|
||||
else
|
||||
CMAKE_GEN := -G "MinGW Makefiles"
|
||||
endif
|
||||
else
|
||||
CMAKE_GEN :=
|
||||
HAS_NINJA := $(shell ninja --version 2>/dev/null)
|
||||
ifneq ($(HAS_NINJA),)
|
||||
CMAKE_GEN := -G "Ninja"
|
||||
else
|
||||
CMAKE_GEN :=
|
||||
endif
|
||||
endif
|
||||
|
||||
# ==============================================================================
|
||||
@@ -115,10 +134,21 @@ debug:
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Debug -DGIT_HASH=$(GIT_HASH)
|
||||
@cmake --build build
|
||||
|
||||
run: all
|
||||
@./$(TARGET_FILE)
|
||||
|
||||
run-debug: debug
|
||||
@./$(TARGET_FILE)
|
||||
|
||||
clean:
|
||||
@rm -rf $(BUILDDIR)
|
||||
|
||||
rebuild: clean all
|
||||
|
||||
# ==============================================================================
|
||||
# REGLAS PARA COMPILACIÓN DE SHADERS (multiplataforma via cmake)
|
||||
# ==============================================================================
|
||||
compile_shaders:
|
||||
compile-shaders:
|
||||
ifdef GLSLC
|
||||
@cmake -D GLSLC=$(GLSLC) -D SHADERS_DIR=$(SHADERS_DIR) -D HEADERS_DIR=$(HEADERS_DIR) -P $(SHADER_CMAKE)
|
||||
else
|
||||
@@ -129,26 +159,26 @@ endif
|
||||
pack:
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||
@cmake --build build --target pack_resources
|
||||
@./build/pack_resources data resources.pack
|
||||
@./build/pack_resources data build/resources.pack
|
||||
|
||||
# ==============================================================================
|
||||
# RELEASE AUTOMÁTICO (detecta SO)
|
||||
# ==============================================================================
|
||||
release:
|
||||
ifeq ($(OS),Windows_NT)
|
||||
@"$(MAKE)" _windows_release
|
||||
@"$(MAKE)" _windows-release
|
||||
else
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
@$(MAKE) _macos_release
|
||||
@$(MAKE) _macos-release
|
||||
else
|
||||
@$(MAKE) _linux_release
|
||||
@$(MAKE) _linux-release
|
||||
endif
|
||||
endif
|
||||
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN PARA WINDOWS (RELEASE)
|
||||
# ==============================================================================
|
||||
_windows_release: pack
|
||||
_windows-release: pack
|
||||
@echo off
|
||||
@echo Creando release para Windows - Version: $(VERSION)
|
||||
|
||||
@@ -162,7 +192,7 @@ _windows_release: pack
|
||||
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
|
||||
|
||||
# Copia ficheros (resources.pack substitueix la carpeta data/)
|
||||
@powershell -Command "Copy-Item 'resources.pack' -Destination '$(RELEASE_FOLDER)'"
|
||||
@powershell -Command "Copy-Item 'build/resources.pack' -Destination '$(RELEASE_FOLDER)'"
|
||||
@powershell -Command "Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)'"
|
||||
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
|
||||
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
|
||||
@@ -181,15 +211,31 @@ _windows_release: pack
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN PARA MACOS (RELEASE)
|
||||
# ==============================================================================
|
||||
_macos_release: pack
|
||||
_macos-release: pack
|
||||
@echo "Creando release para macOS - Version: $(VERSION)"
|
||||
|
||||
# Verificar e instalar create-dmg si es necesario
|
||||
@which create-dmg > /dev/null || (echo "Instalando create-dmg..." && brew install create-dmg)
|
||||
|
||||
# Compila la versión para procesadores Intel con cmake
|
||||
@cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 -DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH)
|
||||
@cmake --build build/intel
|
||||
# Verifica dependencias necesarias (create-dmg). Si falta, intenta instalarla
|
||||
# con brew; si brew tampoco está, indica el comando exacto al usuario.
|
||||
@command -v create-dmg >/dev/null 2>&1 || { \
|
||||
echo ""; \
|
||||
echo "============================================"; \
|
||||
echo " Falta la dependencia: create-dmg"; \
|
||||
echo "============================================"; \
|
||||
if command -v brew >/dev/null 2>&1; then \
|
||||
echo " Instalando con: brew install create-dmg"; \
|
||||
brew install create-dmg || { \
|
||||
echo ""; \
|
||||
echo " ERROR: 'brew install create-dmg' ha fallado."; \
|
||||
echo " Ejecuta el comando manualmente y vuelve a probar."; \
|
||||
exit 1; \
|
||||
}; \
|
||||
else \
|
||||
echo " Homebrew no está instalado."; \
|
||||
echo " Instálalo desde https://brew.sh y luego ejecuta:"; \
|
||||
echo " brew install create-dmg"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
}
|
||||
|
||||
# Elimina datos de compilaciones anteriores
|
||||
$(RMDIR) "$(RELEASE_FOLDER)"
|
||||
@@ -204,7 +250,7 @@ _macos_release: pack
|
||||
$(MKDIR) "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||
|
||||
# Copia carpetas y ficheros (resources.pack substitueix la carpeta data/)
|
||||
cp resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||
cp build/resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
|
||||
cp release/icons/*.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||
@@ -218,30 +264,49 @@ _macos_release: pack
|
||||
sed -i '' '/<key>CFBundleShortVersionString<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"; \
|
||||
sed -i '' '/<key>CFBundleVersion<\/key>/{n;s|<string>.*</string>|<string>'"$$RAW_VERSION"'</string>|;}' "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Info.plist"
|
||||
|
||||
# Copia el ejecutable Intel al bundle
|
||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
||||
|
||||
# Firma la aplicación
|
||||
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"
|
||||
|
||||
# Empaqueta el .dmg de la versión Intel con create-dmg
|
||||
@echo "Creando DMG Intel con iconos de 96x96..."
|
||||
create-dmg \
|
||||
--volname "$(APP_NAME)" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 720 300 \
|
||||
--icon-size 96 \
|
||||
--text-size 12 \
|
||||
--icon "$(APP_NAME).app" 278 102 \
|
||||
--icon "LICENSE" 441 102 \
|
||||
--icon "README.md" 604 102 \
|
||||
--app-drop-link 115 102 \
|
||||
--hide-extension "$(APP_NAME).app" \
|
||||
"$(MACOS_INTEL_RELEASE)" \
|
||||
"$(RELEASE_FOLDER)" || true
|
||||
@echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"
|
||||
# Compila y empaqueta la versión Intel (best-effort: si falla, se omite el
|
||||
# DMG Intel y continúa con la build de Apple Silicon).
|
||||
@echo ""
|
||||
@echo "============================================"
|
||||
@echo " Compilando version Intel (x86_64)"
|
||||
@echo "============================================"
|
||||
@if cmake -S . -B build/intel -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
|
||||
-DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \
|
||||
-DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH) \
|
||||
&& cmake --build build/intel; then \
|
||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"; \
|
||||
codesign --deep --force --sign - --timestamp=none "$(RELEASE_FOLDER)/$(APP_NAME).app"; \
|
||||
echo "Creando DMG Intel con iconos de 96x96..."; \
|
||||
create-dmg \
|
||||
--volname "$(APP_NAME)" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 720 300 \
|
||||
--icon-size 96 \
|
||||
--text-size 12 \
|
||||
--icon "$(APP_NAME).app" 278 102 \
|
||||
--icon "LICENSE" 441 102 \
|
||||
--icon "README.md" 604 102 \
|
||||
--app-drop-link 115 102 \
|
||||
--hide-extension "$(APP_NAME).app" \
|
||||
"$(MACOS_INTEL_RELEASE)" \
|
||||
"$(RELEASE_FOLDER)" || true; \
|
||||
echo "Release Intel creado: $(MACOS_INTEL_RELEASE)"; \
|
||||
else \
|
||||
echo ""; \
|
||||
echo "============================================"; \
|
||||
echo " WARNING: la build Intel ha fallado."; \
|
||||
echo " Se omite el DMG Intel y se continúa con"; \
|
||||
echo " la build de Apple Silicon."; \
|
||||
echo "============================================"; \
|
||||
echo ""; \
|
||||
fi
|
||||
|
||||
# Compila la versión para procesadores Apple Silicon con cmake
|
||||
@echo ""
|
||||
@echo "============================================"
|
||||
@echo " Compilando version Apple Silicon (arm64)"
|
||||
@echo "============================================"
|
||||
@cmake -S . -B build/arm -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DMACOS_BUNDLE=ON -DGIT_HASH=$(GIT_HASH)
|
||||
@cmake --build build/arm
|
||||
cp "$(TARGET_FILE)" "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/MacOS/$(TARGET_NAME)"
|
||||
@@ -298,26 +363,26 @@ 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:
|
||||
# Versió Debug del build wasm: build local sense deploy. Sortida a dist/wasm-debug/.
|
||||
wasm-debug:
|
||||
@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/$(TARGET_NAME).html"
|
||||
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/$(TARGET_NAME).html"
|
||||
|
||||
# ==============================================================================
|
||||
# COMPILACIÓN PARA LINUX (RELEASE)
|
||||
# ==============================================================================
|
||||
_linux_release: pack
|
||||
_linux-release: pack
|
||||
@echo "Creando release para Linux - Version: $(VERSION)"
|
||||
|
||||
# Compila con cmake
|
||||
@@ -329,7 +394,7 @@ _linux_release: pack
|
||||
$(MKDIR) "$(RELEASE_FOLDER)"
|
||||
|
||||
# Copia ficheros (resources.pack substitueix la carpeta data/)
|
||||
cp resources.pack "$(RELEASE_FOLDER)"
|
||||
cp build/resources.pack "$(RELEASE_FOLDER)"
|
||||
cp LICENSE "$(RELEASE_FOLDER)"
|
||||
cp README.md "$(RELEASE_FOLDER)"
|
||||
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
|
||||
@@ -368,6 +433,13 @@ cppcheck:
|
||||
@cmake $(CMAKE_GEN) -S . -B build -DCMAKE_BUILD_TYPE=Release -DGIT_HASH=$(GIT_HASH)
|
||||
@cmake --build build --target cppcheck
|
||||
|
||||
# ==============================================================================
|
||||
# GIT HOOKS
|
||||
# ==============================================================================
|
||||
hooks-install:
|
||||
@git config core.hooksPath .githooks
|
||||
@echo "Git hooks activats: $(shell pwd)/.githooks"
|
||||
|
||||
# DESCÀRREGA DE GAMECONTROLLERDB
|
||||
# ==============================================================================
|
||||
controllerdb:
|
||||
@@ -387,14 +459,18 @@ help:
|
||||
@echo " make - Compilar amb cmake (Release)"
|
||||
@echo " make debug - Compilar amb cmake (Debug)"
|
||||
@echo ""
|
||||
@echo " Execucio:"
|
||||
@echo " make run - Compilar (Release) i executar"
|
||||
@echo " make run-debug - Compilar (Debug) i executar"
|
||||
@echo ""
|
||||
@echo " Release:"
|
||||
@echo " make release - Crear release (detecta SO automaticament)"
|
||||
@echo " make wasm - Build WebAssembly (requereix Docker) + deploy a maverick"
|
||||
@echo " make wasm_debug - Build WebAssembly Debug local (sense deploy)"
|
||||
@echo " make wasm-debug - Build WebAssembly Debug local (sense deploy)"
|
||||
@echo ""
|
||||
@echo " Eines:"
|
||||
@echo " make compile_shaders - Compilar shaders SPIR-V"
|
||||
@echo " make pack - Empaquetar data/ a resources.pack (format AEE1)"
|
||||
@echo " make compile-shaders - Compilar shaders SPIR-V"
|
||||
@echo " make pack - Empaquetar data/ a $(BUILDDIR)/resources.pack (format AEE1)"
|
||||
@echo " make controllerdb - Actualitzar gamecontrollerdb.txt des de SDL_GameControllerDB"
|
||||
@echo ""
|
||||
@echo " Qualitat de codi:"
|
||||
@@ -405,8 +481,11 @@ help:
|
||||
@echo " make cppcheck - Anàlisi estàtic amb cppcheck"
|
||||
@echo ""
|
||||
@echo " Altres:"
|
||||
@echo " make clean - Esborrar carpeta $(BUILDDIR)/"
|
||||
@echo " make rebuild - clean + all"
|
||||
@echo " make hooks-install - Activar git hooks del projecte"
|
||||
@echo " make help - Mostrar esta ajuda"
|
||||
@echo ""
|
||||
@echo " Versio actual: $(VERSION) ($(GIT_HASH))"
|
||||
|
||||
.PHONY: all debug pack release wasm wasm_debug _windows_release _linux_release _macos_release compile_shaders controllerdb format format-check tidy tidy-fix cppcheck help
|
||||
.PHONY: all debug run run-debug clean rebuild pack release wasm wasm-debug _windows-release _linux-release _macos-release compile-shaders controllerdb format format-check tidy tidy-fix cppcheck hooks-install help
|
||||
|
||||
@@ -27,9 +27,6 @@ keys:
|
||||
- id: toggle_aspect_ratio
|
||||
code: "F5"
|
||||
desc: "Aspecte 4:3 / pixels quadrats"
|
||||
- id: toggle_supersampling
|
||||
code: "F6"
|
||||
desc: "Activa/desactiva supersampling"
|
||||
- id: next_shader
|
||||
code: "F7"
|
||||
desc: "Tipus de shader (PostFX / CRT-Pi)"
|
||||
|
||||
@@ -26,7 +26,6 @@ menu:
|
||||
screen: "Pantalla"
|
||||
shader: "Shader"
|
||||
aspect_4_3: "Aspecte 4:3"
|
||||
supersampling: "Supersampling"
|
||||
vsync: "Vsync"
|
||||
scaling_mode: "Escala"
|
||||
shader_type: "Tipus shader"
|
||||
@@ -79,8 +78,6 @@ notifications:
|
||||
shader_off: "Shader off"
|
||||
aspect_43: "4:3 CRT"
|
||||
aspect_square: "Píxels quadrats"
|
||||
ss_on: "Supersampling on"
|
||||
ss_off: "Supersampling off"
|
||||
preset_fmt: "Preset: %s"
|
||||
filter_linear: "Filtre: linear"
|
||||
filter_nearest: "Filtre: nearest"
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
#version 450
|
||||
layout(location = 0) in vec2 v_uv;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
layout(set = 2, binding = 0) uniform sampler2D source;
|
||||
|
||||
layout(set = 3, binding = 0) uniform DownscaleUniforms {
|
||||
int algorithm; // 0 = Lanczos2 (ventana 2, ±2 taps), 1 = Lanczos3 (ventana 3, ±3 taps)
|
||||
float pad0;
|
||||
float pad1;
|
||||
float pad2;
|
||||
} u;
|
||||
|
||||
// Kernel Lanczos normalizado: sinc(t) * sinc(t/a) para |t| < a, 0 fuera.
|
||||
float lanczos(float t, float a) {
|
||||
t = abs(t);
|
||||
if (t < 0.0001) { return 1.0; }
|
||||
if (t >= a) { return 0.0; }
|
||||
const float PI = 3.14159265358979;
|
||||
float pt = PI * t;
|
||||
return (a * sin(pt) * sin(pt / a)) / (pt * pt);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 src_size = vec2(textureSize(source, 0));
|
||||
// Posición en coordenadas de texel (centros de texel en N+0.5)
|
||||
vec2 p = v_uv * src_size;
|
||||
vec2 p_floor = floor(p);
|
||||
|
||||
float a = (u.algorithm == 0) ? 2.0 : 3.0;
|
||||
int win = int(a);
|
||||
|
||||
vec4 color = vec4(0.0);
|
||||
float weight_sum = 0.0;
|
||||
|
||||
for (int j = -win; j <= win; j++) {
|
||||
for (int i = -win; i <= win; i++) {
|
||||
// Centro del texel (i,j) relativo a p_floor
|
||||
vec2 tap_center = p_floor + vec2(float(i), float(j)) + 0.5;
|
||||
vec2 offset = tap_center - p;
|
||||
float w = lanczos(offset.x, a) * lanczos(offset.y, a);
|
||||
color += texture(source, tap_center / src_size) * w;
|
||||
weight_sum += w;
|
||||
}
|
||||
}
|
||||
|
||||
out_color = (weight_sum > 0.0) ? (color / weight_sum) : vec4(0.0, 0.0, 0.0, 1.0);
|
||||
}
|
||||
+47
-25
@@ -6,7 +6,9 @@
|
||||
// xxd -i postfx.frag.spv > ../../source/core/rendering/sdl3gpu/postfx_frag_spv.h
|
||||
//
|
||||
// PostFXUniforms must match exactly the C++ struct in sdl3gpu_shader.hpp
|
||||
// (8 floats, 32 bytes, std140/scalar layout).
|
||||
// (16 floats = 4 × vec4 = 64 bytes, std140/scalar layout).
|
||||
// IMPORTANT: Qualsevol canvi ací cal replicar-lo a mà a
|
||||
// source/core/rendering/sdl3gpu/msl/postfx_frag.msl.h (no hi ha generador).
|
||||
|
||||
layout(location = 0) in vec2 v_uv;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
@@ -15,7 +17,7 @@ layout(set = 2, binding = 0) uniform sampler2D scene;
|
||||
|
||||
layout(set = 3, binding = 0) uniform PostFXUniforms {
|
||||
float vignette_strength;
|
||||
float chroma_strength;
|
||||
float chroma_min; // intensitat mínima de l'aberració cromàtica
|
||||
float scanline_strength;
|
||||
float screen_height;
|
||||
float mask_strength;
|
||||
@@ -24,10 +26,28 @@ layout(set = 3, binding = 0) uniform PostFXUniforms {
|
||||
float bleeding;
|
||||
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||||
float time; // seconds since SDL init
|
||||
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
|
||||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — 48 bytes total (3 × 16)
|
||||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
|
||||
float chroma_max; // intensitat màxima; si == chroma_min → chroma estàtic
|
||||
// vec4 #3 — paràmetres de scanlines (exposats per preset YAML)
|
||||
float scan_dark_ratio; // fracció de subfila fosca per fila lògica (1/3 ≈ 0.333)
|
||||
float scan_dark_floor; // multiplicador de brillantor de la subfila fosca
|
||||
float scan_edge_soft; // 0 = step dur; 1 = suavitzat d'1 píxel físic (estil crtpi)
|
||||
float pad3; // padding per tancar a 64 bytes (4 × vec4)
|
||||
} u;
|
||||
|
||||
// Mostreig bilinear horitzontal d'un canal RGB. Evita el "tic-tac" del sampler
|
||||
// NEAREST quan l'offset de chroma és subpíxel: sense interpolar, l'offset
|
||||
// arrodonia entre 1 i 2 píxels i el drift temporal feia un parpelleig discret.
|
||||
float sampleBilinearX(vec2 uv_target, int channel) {
|
||||
vec2 tex_size = vec2(textureSize(scene, 0));
|
||||
float px = uv_target.x * tex_size.x - 0.5;
|
||||
float p_floor = floor(px);
|
||||
float f = px - p_floor;
|
||||
vec4 c0 = texture(scene, vec2((p_floor + 0.5) / tex_size.x, uv_target.y));
|
||||
vec4 c1 = texture(scene, vec2((p_floor + 1.5) / tex_size.x, uv_target.y));
|
||||
return mix(c0[channel], c1[channel], f);
|
||||
}
|
||||
|
||||
// YCbCr helpers for NTSC bleeding
|
||||
vec3 rgb_to_ycc(vec3 rgb) {
|
||||
return vec3(
|
||||
@@ -69,11 +89,11 @@ void main() {
|
||||
vec3 base = texture(scene, uv).rgb;
|
||||
|
||||
// Sangrado NTSC — difuminado horizontal de crominancia.
|
||||
// step = 1 pixel lógico de juego en UV (corrige SS: textureSize.x = game_w * oversample).
|
||||
// step = 1 pixel lógico de juego en UV.
|
||||
vec3 colour;
|
||||
if (u.bleeding > 0.0) {
|
||||
float tw = float(textureSize(scene, 0).x);
|
||||
float step = u.oversample / tw; // 1 pixel lógico en UV
|
||||
float step = 1.0 / tw; // 1 pixel lógico en UV
|
||||
vec3 ycc = rgb_to_ycc(base);
|
||||
vec3 ycc_l2 = rgb_to_ycc(texture(scene, uv - vec2(2.0*step, 0.0)).rgb);
|
||||
vec3 ycc_l1 = rgb_to_ycc(texture(scene, uv - vec2(1.0*step, 0.0)).rgb);
|
||||
@@ -85,10 +105,14 @@ void main() {
|
||||
colour = base;
|
||||
}
|
||||
|
||||
// Aberración cromática (drift animado con time para efecto NTSC real)
|
||||
float ca = u.chroma_strength * 0.005 * (1.0 + 0.15 * sin(u.time * 7.3));
|
||||
colour.r = texture(scene, uv + vec2(ca, 0.0)).r;
|
||||
colour.b = texture(scene, uv - vec2(ca, 0.0)).b;
|
||||
// Aberración cromática — intensitat varia entre chroma_min i chroma_max amb
|
||||
// una sinusoidal (si min == max, queda estàtica). Mostreig bilinear horitzontal
|
||||
// per evitar el "tic-tac" del NEAREST sampler quan l'offset és subpíxel.
|
||||
if (u.chroma_min > 0.0 || u.chroma_max > 0.0) {
|
||||
float ca = mix(u.chroma_min, u.chroma_max, 0.5 + 0.5 * sin(u.time * 7.3)) * 0.005;
|
||||
colour.r = sampleBilinearX(uv + vec2(ca, 0.0), 0);
|
||||
colour.b = sampleBilinearX(uv - vec2(ca, 0.0), 2);
|
||||
}
|
||||
|
||||
// Corrección gamma (linealizar antes de scanlines, codificar después)
|
||||
if (u.gamma_strength > 0.0) {
|
||||
@@ -96,22 +120,20 @@ void main() {
|
||||
colour = mix(colour, lin, u.gamma_strength);
|
||||
}
|
||||
|
||||
// Scanlines — proporción 2/3 brillantes + 1/3 oscuras por fila lógica.
|
||||
// Casos especiales: 1 subfila → sin efecto; 2 subfilas → 1+1 (50/50).
|
||||
// Constantes ajustables:
|
||||
const float SCAN_DARK_RATIO = 0.333; // fracción de subfilas oscuras (ps >= 3)
|
||||
const float SCAN_DARK_FLOOR = 0.42; // multiplicador de brillo de subfilas oscuras
|
||||
// Scanlines — tècnica dels 3 subpíxels verticals per píxel lògic (aee/projecte_2026):
|
||||
// franja fosca ocupant `scan_dark_ratio` al final de cada fila lògica. La transició es
|
||||
// suavitza amb smoothstep d'ample ≈ 1 píxel físic (estil crtpi: filtratge analític
|
||||
// continu), controlat per `scan_edge_soft`. A 0 és equivalent al step dur antic.
|
||||
if (u.scanline_strength > 0.0) {
|
||||
float ps = max(1.0, round(u.pixel_scale));
|
||||
float frac_in_row = fract(uv.y * u.screen_height);
|
||||
float row_pos = floor(frac_in_row * ps);
|
||||
// bright_rows: cuántas subfilas son brillantes
|
||||
// ps==1 → ps (todo brillante → is_dark nunca se activa)
|
||||
// ps==2 → 1 brillante + 1 oscura
|
||||
// ps>=3 → floor(ps * (1 - DARK_RATIO)) brillantes
|
||||
float bright_rows = (ps < 2.0) ? ps : ((ps < 3.0) ? 1.0 : floor(ps * (1.0 - SCAN_DARK_RATIO)));
|
||||
float is_dark = step(bright_rows, row_pos);
|
||||
float scan = mix(1.0, SCAN_DARK_FLOOR, is_dark);
|
||||
float ps = max(u.pixel_scale, 1.0);
|
||||
float sub = fract(uv.y * u.screen_height); // [0,1) dins la fila lògica
|
||||
float dark_center = 1.0 - u.scan_dark_ratio * 0.5; // centre de la franja fosca
|
||||
float d = abs(sub - dark_center);
|
||||
d = min(d, 1.0 - d); // wrap a la fila següent
|
||||
float half_width = u.scan_dark_ratio * 0.5;
|
||||
float softness = u.scan_edge_soft * 0.5 / ps; // mig píxel físic a cada costat
|
||||
float band = 1.0 - smoothstep(half_width - softness, half_width + softness, d);
|
||||
float scan = mix(1.0, u.scan_dark_floor, band);
|
||||
colour *= mix(1.0, scan, u.scanline_strength);
|
||||
}
|
||||
|
||||
|
||||
+41
-35
@@ -8,7 +8,7 @@
|
||||
// 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.c"
|
||||
#include "external/stb_vorbis.c" // NOLINT(bugprone-suspicious-include): stb header-only library
|
||||
// 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
|
||||
@@ -40,15 +40,15 @@ Audio::Audio() { initSDLAudio(); }
|
||||
|
||||
// Destructor
|
||||
Audio::~Audio() {
|
||||
JA_Quit();
|
||||
Ja::quit();
|
||||
}
|
||||
|
||||
// Método principal
|
||||
void Audio::update() {
|
||||
JA_Update();
|
||||
Ja::update();
|
||||
|
||||
// Sincronizar estado: detectar cuando la música se para (ej. fade-out completado)
|
||||
if (instance && instance->music_.state == MusicState::PLAYING && JA_GetMusicState() != JA_MUSIC_PLAYING) {
|
||||
if (instance && instance->music_.state == MusicState::PLAYING && Ja::getMusicState() != Ja::MusicState::PLAYING) {
|
||||
instance->music_.state = MusicState::STOPPED;
|
||||
}
|
||||
}
|
||||
@@ -62,18 +62,22 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
|
||||
return;
|
||||
}
|
||||
|
||||
if (!music_enabled_) return;
|
||||
if (!music_enabled_) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* resource = AudioResource::getMusic(name);
|
||||
if (resource == nullptr) return;
|
||||
if (resource == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
|
||||
JA_CrossfadeMusic(resource, crossfade_ms, loop);
|
||||
Ja::crossfadeMusic(resource, crossfade_ms, loop);
|
||||
} else {
|
||||
if (music_.state == MusicState::PLAYING) {
|
||||
JA_StopMusic();
|
||||
Ja::stopMusic();
|
||||
}
|
||||
JA_PlayMusic(resource, loop);
|
||||
Ja::playMusic(resource, loop);
|
||||
}
|
||||
|
||||
music_.name = name;
|
||||
@@ -82,16 +86,18 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
|
||||
}
|
||||
|
||||
// Reproduce la música por puntero (con crossfade opcional)
|
||||
void Audio::playMusic(JA_Music_t* music, const int loop, const int crossfade_ms) {
|
||||
if (!music_enabled_ || music == nullptr) return;
|
||||
void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms) {
|
||||
if (!music_enabled_ || music == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (crossfade_ms > 0 && music_.state == MusicState::PLAYING) {
|
||||
JA_CrossfadeMusic(music, crossfade_ms, loop);
|
||||
Ja::crossfadeMusic(music, crossfade_ms, loop);
|
||||
} else {
|
||||
if (music_.state == MusicState::PLAYING) {
|
||||
JA_StopMusic();
|
||||
Ja::stopMusic();
|
||||
}
|
||||
JA_PlayMusic(music, loop);
|
||||
Ja::playMusic(music, loop);
|
||||
}
|
||||
|
||||
music_.name.clear(); // nom desconegut quan es passa per punter
|
||||
@@ -102,7 +108,7 @@ void Audio::playMusic(JA_Music_t* music, const int loop, const int crossfade_ms)
|
||||
// Pausa la música
|
||||
void Audio::pauseMusic() {
|
||||
if (music_enabled_ && music_.state == MusicState::PLAYING) {
|
||||
JA_PauseMusic();
|
||||
Ja::pauseMusic();
|
||||
music_.state = MusicState::PAUSED;
|
||||
}
|
||||
}
|
||||
@@ -110,7 +116,7 @@ void Audio::pauseMusic() {
|
||||
// Continua la música pausada
|
||||
void Audio::resumeMusic() {
|
||||
if (music_enabled_ && music_.state == MusicState::PAUSED) {
|
||||
JA_ResumeMusic();
|
||||
Ja::resumeMusic();
|
||||
music_.state = MusicState::PLAYING;
|
||||
}
|
||||
}
|
||||
@@ -118,7 +124,7 @@ void Audio::resumeMusic() {
|
||||
// Detiene la música
|
||||
void Audio::stopMusic() {
|
||||
if (music_enabled_) {
|
||||
JA_StopMusic();
|
||||
Ja::stopMusic();
|
||||
music_.state = MusicState::STOPPED;
|
||||
}
|
||||
}
|
||||
@@ -126,42 +132,42 @@ void Audio::stopMusic() {
|
||||
// Reproduce un sonido por nombre
|
||||
void Audio::playSound(const std::string& name, Group group) const {
|
||||
if (sound_enabled_) {
|
||||
JA_PlaySound(AudioResource::getSound(name), 0, static_cast<int>(group));
|
||||
Ja::playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Reproduce un sonido por puntero directo
|
||||
void Audio::playSound(JA_Sound_t* sound, Group group) const {
|
||||
void Audio::playSound(Ja::Sound* sound, Group group) const {
|
||||
if (sound_enabled_ && sound != nullptr) {
|
||||
JA_PlaySound(sound, 0, static_cast<int>(group));
|
||||
Ja::playSound(sound, 0, static_cast<int>(group));
|
||||
}
|
||||
}
|
||||
|
||||
// Detiene todos los sonidos
|
||||
void Audio::stopAllSounds() const {
|
||||
if (sound_enabled_) {
|
||||
JA_StopChannel(-1);
|
||||
Ja::stopChannel(-1);
|
||||
}
|
||||
}
|
||||
|
||||
// Realiza un fundido de salida de la música
|
||||
void Audio::fadeOutMusic(int milliseconds) const {
|
||||
if (music_enabled_ && getRealMusicState() == MusicState::PLAYING) {
|
||||
JA_FadeOutMusic(milliseconds);
|
||||
Ja::fadeOutMusic(milliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
// Consulta directamente el estado real de la música en jailaudio
|
||||
auto Audio::getRealMusicState() -> MusicState {
|
||||
JA_Music_state ja_state = JA_GetMusicState();
|
||||
Ja::MusicState ja_state = Ja::getMusicState();
|
||||
switch (ja_state) {
|
||||
case JA_MUSIC_PLAYING:
|
||||
case Ja::MusicState::PLAYING:
|
||||
return MusicState::PLAYING;
|
||||
case JA_MUSIC_PAUSED:
|
||||
case Ja::MusicState::PAUSED:
|
||||
return MusicState::PAUSED;
|
||||
case JA_MUSIC_STOPPED:
|
||||
case JA_MUSIC_INVALID:
|
||||
case JA_MUSIC_DISABLED:
|
||||
case Ja::MusicState::STOPPED:
|
||||
case Ja::MusicState::INVALID:
|
||||
case Ja::MusicState::DISABLED:
|
||||
default:
|
||||
return MusicState::STOPPED;
|
||||
}
|
||||
@@ -170,17 +176,17 @@ auto Audio::getRealMusicState() -> MusicState {
|
||||
// Establece el volumen de los sonidos (float 0.0..1.0)
|
||||
void Audio::setSoundVolume(float sound_volume, Group group) const {
|
||||
sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
|
||||
const bool active = enabled_ && sound_enabled_;
|
||||
const float CONVERTED_VOLUME = active ? sound_volume * Options::audio.volume : 0.0F;
|
||||
JA_SetSoundVolume(CONVERTED_VOLUME, static_cast<int>(group));
|
||||
const bool ACTIVE = enabled_ && sound_enabled_;
|
||||
const float CONVERTED_VOLUME = ACTIVE ? sound_volume * Options::audio.volume : 0.0F;
|
||||
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 {
|
||||
music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
|
||||
const bool active = enabled_ && music_enabled_;
|
||||
const float CONVERTED_VOLUME = active ? music_volume * Options::audio.volume : 0.0F;
|
||||
JA_SetMusicVolume(CONVERTED_VOLUME);
|
||||
const bool ACTIVE = enabled_ && music_enabled_;
|
||||
const float CONVERTED_VOLUME = ACTIVE ? music_volume * Options::audio.volume : 0.0F;
|
||||
Ja::setMusicVolume(CONVERTED_VOLUME);
|
||||
}
|
||||
|
||||
// Aplica la configuración
|
||||
@@ -201,7 +207,7 @@ void Audio::initSDLAudio() {
|
||||
if (!SDL_Init(SDL_INIT_AUDIO)) {
|
||||
std::cout << "SDL_AUDIO could not initialize! SDL Error: " << SDL_GetError() << '\n';
|
||||
} else {
|
||||
JA_Init(FREQUENCY, SDL_AUDIO_S16LE, 2);
|
||||
Ja::init(FREQUENCY, SDL_AUDIO_S16LE, 2);
|
||||
enable(Options::audio.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
+16
-10
@@ -1,10 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <cmath> // Para std::lround
|
||||
#include <cstdint> // Para int8_t, uint8_t
|
||||
#include <memory> // Para std::unique_ptr
|
||||
#include <string> // Para string
|
||||
#include <utility> // Para move
|
||||
|
||||
namespace Ja {
|
||||
struct Music;
|
||||
struct Sound;
|
||||
} // namespace Ja
|
||||
|
||||
// --- Clase Audio: gestor de audio (singleton) ---
|
||||
// Implementació canònica, byte-idèntica entre projectes.
|
||||
// Els volums es manegen internament com a float 0.0–1.0; la capa de
|
||||
@@ -43,17 +49,17 @@ class Audio {
|
||||
static void update(); // Actualización del sistema de audio
|
||||
|
||||
// --- Control de música ---
|
||||
void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional)
|
||||
void playMusic(struct JA_Music_t* music, int loop = -1, int crossfade_ms = 0); // Reproducir música por puntero (con crossfade opcional)
|
||||
void pauseMusic(); // Pausar reproducción de música
|
||||
void resumeMusic(); // Continua la música pausada
|
||||
void stopMusic(); // Detener completamente la música
|
||||
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
|
||||
void playMusic(const std::string& name, int loop = -1, int crossfade_ms = 0); // Reproducir música por nombre (con crossfade opcional)
|
||||
void playMusic(Ja::Music* music, int loop = -1, int crossfade_ms = 0); // Reproducir música por puntero (con crossfade opcional)
|
||||
void pauseMusic(); // Pausar reproducción de música
|
||||
void resumeMusic(); // Continua la música pausada
|
||||
void stopMusic(); // Detener completamente la música
|
||||
void fadeOutMusic(int milliseconds) const; // Fundido de salida de la música
|
||||
|
||||
// --- Control de sonidos ---
|
||||
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
|
||||
void playSound(struct JA_Sound_t* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
|
||||
void stopAllSounds() const; // Detener todos los sonidos
|
||||
void playSound(const std::string& name, Group group = Group::GAME) const; // Reproducir sonido puntual por nombre
|
||||
void playSound(Ja::Sound* sound, Group group = Group::GAME) const; // Reproducir sonido puntual por puntero
|
||||
void stopAllSounds() const; // Detener todos los sonidos
|
||||
|
||||
// --- Control de volumen (API interna: float 0.0..1.0) ---
|
||||
void setSoundVolume(float volume, Group group = Group::ALL) const; // Ajustar volumen de efectos
|
||||
@@ -62,7 +68,7 @@ class Audio {
|
||||
// --- Helpers de conversió per a la capa de presentació ---
|
||||
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
|
||||
static constexpr auto toPercent(float volume) -> int {
|
||||
return static_cast<int>(volume * 100.0F + 0.5F);
|
||||
return static_cast<int>(std::lround(volume * 100.0F));
|
||||
}
|
||||
static constexpr auto fromPercent(int percent) -> float {
|
||||
return static_cast<float>(percent) / 100.0F;
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
namespace AudioResource {
|
||||
|
||||
JA_Music_t* getMusic(const std::string& name) {
|
||||
auto getMusic(const std::string& name) -> Ja::Music* {
|
||||
return Resource::Cache::get()->getMusic(name);
|
||||
}
|
||||
|
||||
JA_Sound_t* getSound(const std::string& name) {
|
||||
auto getSound(const std::string& name) -> Ja::Sound* {
|
||||
return Resource::Cache::get()->getSound(name);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
// --- Audio Resource Adapter ---
|
||||
// Aquest fitxer exposa una interfície comuna a Audio per obtenir JA_Music_t* /
|
||||
// JA_Sound_t* per nom. Cada projecte la implementa en audio_adapter.cpp
|
||||
// Aquest fitxer exposa una interfície comuna a Audio per obtenir Ja::Music* /
|
||||
// Ja::Sound* per nom. Cada projecte la implementa en audio_adapter.cpp
|
||||
// delegant al seu singleton de recursos (Resource::get(), Resource::Cache::get(),
|
||||
// etc.). Això permet que audio.hpp/audio.cpp siguin idèntics entre projectes.
|
||||
|
||||
#include <string> // Para string
|
||||
|
||||
struct JA_Music_t;
|
||||
struct JA_Sound_t;
|
||||
namespace Ja {
|
||||
struct Music;
|
||||
struct Sound;
|
||||
} // namespace Ja
|
||||
|
||||
namespace AudioResource {
|
||||
JA_Music_t* getMusic(const std::string& name);
|
||||
JA_Sound_t* getSound(const std::string& name);
|
||||
auto getMusic(const std::string& name) -> Ja::Music*;
|
||||
auto getSound(const std::string& name) -> Ja::Sound*;
|
||||
} // namespace AudioResource
|
||||
|
||||
+595
-575
File diff suppressed because it is too large
Load Diff
+125
-112
@@ -11,8 +11,8 @@
|
||||
|
||||
namespace Gamepad {
|
||||
|
||||
static SDL_Gamepad* pad_ = nullptr;
|
||||
static SDL_JoystickID pad_id_ = 0;
|
||||
static SDL_Gamepad* pad = nullptr;
|
||||
static SDL_JoystickID pad_id = 0;
|
||||
|
||||
// Emscripten-only: SDL 3.4+ ja no casa el GUID dels mandos web (el gamepad.id
|
||||
// de Chrome/Android no porta Vendor/Product, el parser extreu valors
|
||||
@@ -52,11 +52,11 @@ namespace Gamepad {
|
||||
// Recorta el nom visible del mando: trim des del primer '(' o '['
|
||||
// (per a evitar coses com "Retroid Controller (vendor: 1001) ..."),
|
||||
// elimina espais finals i talla a 25 caràcters.
|
||||
static std::string prettyName(const char* raw) {
|
||||
std::string name = (raw && *raw) ? raw : "Gamepad";
|
||||
const auto pos = name.find_first_of("([");
|
||||
if (pos != std::string::npos) {
|
||||
name.erase(pos);
|
||||
static auto prettyName(const char* raw) -> std::string {
|
||||
std::string name = ((raw != nullptr) && (*raw != 0)) ? raw : "Gamepad";
|
||||
const auto POS = name.find_first_of("([");
|
||||
if (POS != std::string::npos) {
|
||||
name.erase(POS);
|
||||
}
|
||||
while (!name.empty() && name.back() == ' ') {
|
||||
name.pop_back();
|
||||
@@ -64,7 +64,9 @@ namespace Gamepad {
|
||||
if (name.size() > 25) {
|
||||
name.resize(25);
|
||||
}
|
||||
if (name.empty()) name = "Gamepad";
|
||||
if (name.empty()) {
|
||||
name = "Gamepad";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -72,16 +74,16 @@ namespace Gamepad {
|
||||
static constexpr Sint16 STICK_DEADZONE = 12000;
|
||||
|
||||
// Estat previ per a detecció de flanc (edge-triggered)
|
||||
static bool prev_up_ = false;
|
||||
static bool prev_down_ = false;
|
||||
static bool prev_left_ = false;
|
||||
static bool prev_right_ = false;
|
||||
static bool prev_south_ = false;
|
||||
static bool prev_east_ = false;
|
||||
static bool prev_west_ = false;
|
||||
static bool prev_north_ = false;
|
||||
static bool prev_start_ = false;
|
||||
static bool prev_back_ = false;
|
||||
static bool prev_up = false;
|
||||
static bool prev_down = false;
|
||||
static bool prev_left = false;
|
||||
static bool prev_right = false;
|
||||
static bool prev_south = false;
|
||||
static bool prev_east = false;
|
||||
static bool prev_west = false;
|
||||
static bool prev_north = false;
|
||||
static bool prev_start = false;
|
||||
static bool prev_back = false;
|
||||
|
||||
static void notify(const std::string& name, const char* status_key) {
|
||||
std::string msg = name.empty() ? "Gamepad" : name;
|
||||
@@ -98,14 +100,16 @@ namespace Gamepad {
|
||||
static void openFirstGamepad() {
|
||||
int count = 0;
|
||||
SDL_JoystickID* ids = SDL_GetJoysticks(&count);
|
||||
if (ids) {
|
||||
if (ids != nullptr) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
installWebStandardMapping(ids[i]);
|
||||
if (!SDL_IsGamepad(ids[i])) continue;
|
||||
pad_ = SDL_OpenGamepad(ids[i]);
|
||||
if (pad_) {
|
||||
pad_id_ = ids[i];
|
||||
SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad_));
|
||||
if (!SDL_IsGamepad(ids[i])) {
|
||||
continue;
|
||||
}
|
||||
pad = SDL_OpenGamepad(ids[i]);
|
||||
if (pad != nullptr) {
|
||||
pad_id = ids[i];
|
||||
SDL_Log("Gamepad connectat: %s", SDL_GetGamepadName(pad));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -128,16 +132,16 @@ namespace Gamepad {
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
if (pad_) {
|
||||
SDL_CloseGamepad(pad_);
|
||||
pad_ = nullptr;
|
||||
pad_id_ = 0;
|
||||
if (pad != nullptr) {
|
||||
SDL_CloseGamepad(pad);
|
||||
pad = nullptr;
|
||||
pad_id = 0;
|
||||
}
|
||||
SDL_QuitSubSystem(SDL_INIT_GAMEPAD);
|
||||
}
|
||||
|
||||
auto isConnected() -> bool {
|
||||
return pad_ != nullptr;
|
||||
return pad != nullptr;
|
||||
}
|
||||
|
||||
void handleEvent(const SDL_Event& event) {
|
||||
@@ -145,30 +149,32 @@ namespace Gamepad {
|
||||
// GAMEPAD_ADDED) perquè SDL no reconeix el GUID. Escoltem els dos i
|
||||
// injectem el mapping estàndard abans d'obrir el mando.
|
||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_JOYSTICK_ADDED) {
|
||||
if (!pad_) {
|
||||
if (pad == nullptr) {
|
||||
SDL_JoystickID jid = event.jdevice.which;
|
||||
installWebStandardMapping(jid);
|
||||
if (!SDL_IsGamepad(jid)) return;
|
||||
pad_ = SDL_OpenGamepad(jid);
|
||||
if (pad_) {
|
||||
pad_id_ = jid;
|
||||
std::string name = prettyName(SDL_GetGamepadName(pad_));
|
||||
if (!SDL_IsGamepad(jid)) {
|
||||
return;
|
||||
}
|
||||
pad = SDL_OpenGamepad(jid);
|
||||
if (pad != nullptr) {
|
||||
pad_id = jid;
|
||||
std::string name = prettyName(SDL_GetGamepadName(pad));
|
||||
SDL_Log("Gamepad connectat: %s", name.c_str());
|
||||
notifyConnected(name);
|
||||
}
|
||||
}
|
||||
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED || event.type == SDL_EVENT_JOYSTICK_REMOVED) {
|
||||
if (pad_ && event.jdevice.which == pad_id_) {
|
||||
std::string saved_name = prettyName(SDL_GetGamepadName(pad_));
|
||||
if ((pad != nullptr) && event.jdevice.which == pad_id) {
|
||||
std::string saved_name = prettyName(SDL_GetGamepadName(pad));
|
||||
SDL_Log("Gamepad desconnectat: %s", saved_name.c_str());
|
||||
SDL_CloseGamepad(pad_);
|
||||
pad_ = nullptr;
|
||||
pad_id_ = 0;
|
||||
SDL_CloseGamepad(pad);
|
||||
pad = nullptr;
|
||||
pad_id = 0;
|
||||
// Neteja qualsevol tecla virtual que poguera estar premuda
|
||||
JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, false);
|
||||
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, false);
|
||||
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, false);
|
||||
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, false);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, false);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, false);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, false);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, false);
|
||||
notifyDisconnected(saved_name);
|
||||
}
|
||||
}
|
||||
@@ -189,79 +195,86 @@ namespace Gamepad {
|
||||
SDL_PushEvent(&e);
|
||||
}
|
||||
|
||||
void update() {
|
||||
if (!pad_) return;
|
||||
// Estat agregat d'un frame: D-pad i stick combinats, més botons frontals.
|
||||
struct PadState {
|
||||
bool up;
|
||||
bool down;
|
||||
bool left;
|
||||
bool right;
|
||||
bool south;
|
||||
bool east;
|
||||
bool west;
|
||||
bool north;
|
||||
bool start;
|
||||
bool back;
|
||||
};
|
||||
|
||||
// D-pad
|
||||
bool dup = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_UP);
|
||||
bool ddn = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
|
||||
bool dlt = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
|
||||
bool drt = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
|
||||
static auto readPadState() -> PadState {
|
||||
const Sint16 LX = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX);
|
||||
const Sint16 LY = SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY);
|
||||
return PadState{
|
||||
.up = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_UP) || LY < -STICK_DEADZONE,
|
||||
.down = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_DOWN) || LY > STICK_DEADZONE,
|
||||
.left = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_LEFT) || LX < -STICK_DEADZONE,
|
||||
.right = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT) || LX > STICK_DEADZONE,
|
||||
.south = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_SOUTH),
|
||||
.east = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_EAST),
|
||||
.west = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_WEST),
|
||||
.north = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_NORTH),
|
||||
.start = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_START),
|
||||
.back = SDL_GetGamepadButton(pad, SDL_GAMEPAD_BUTTON_BACK),
|
||||
};
|
||||
}
|
||||
|
||||
// Stick esquerre amb dead-zone
|
||||
Sint16 lx = SDL_GetGamepadAxis(pad_, SDL_GAMEPAD_AXIS_LEFTX);
|
||||
Sint16 ly = SDL_GetGamepadAxis(pad_, SDL_GAMEPAD_AXIS_LEFTY);
|
||||
bool sup = ly < -STICK_DEADZONE;
|
||||
bool sdn = ly > STICK_DEADZONE;
|
||||
bool slt = lx < -STICK_DEADZONE;
|
||||
bool srt = lx > STICK_DEADZONE;
|
||||
static void handleMenuNavigation(const PadState& s) {
|
||||
if (s.up && !prev_up) { pushKey(SDL_SCANCODE_UP); }
|
||||
if (s.down && !prev_down) { pushKey(SDL_SCANCODE_DOWN); }
|
||||
if (s.left && !prev_left) { pushKey(SDL_SCANCODE_LEFT); }
|
||||
if (s.right && !prev_right) { pushKey(SDL_SCANCODE_RIGHT); }
|
||||
if (s.east && !prev_east) { pushKey(SDL_SCANCODE_RETURN); }
|
||||
if (s.south && !prev_south) { pushKey(SDL_SCANCODE_BACKSPACE); }
|
||||
// Mentre el menú està obert, el joc no ha de rebre moviment.
|
||||
Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, false);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, false);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, false);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, false);
|
||||
}
|
||||
|
||||
bool up = dup || sup;
|
||||
bool dn = ddn || sdn;
|
||||
bool lt = dlt || slt;
|
||||
bool rt = drt || srt;
|
||||
|
||||
// Botons frontals (layout SDL: SOUTH=A/Cross, EAST=B/Circle, WEST=X/Square, NORTH=Y/Triangle)
|
||||
bool south = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_SOUTH);
|
||||
bool east = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_EAST);
|
||||
bool west = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_WEST);
|
||||
bool north = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_NORTH);
|
||||
bool start = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_START);
|
||||
bool back = SDL_GetGamepadButton(pad_, SDL_GAMEPAD_BUTTON_BACK);
|
||||
|
||||
// Select (Back) → obre/tanca menú de servei (flanc)
|
||||
if (back && !prev_back_) pushKey(KeyConfig::scancode("menu_toggle"));
|
||||
// Start → pausa (flanc)
|
||||
if (start && !prev_start_) pushKey(KeyConfig::scancode("pause_toggle"));
|
||||
|
||||
if (Menu::isOpen()) {
|
||||
// Navegació del menú per flanc
|
||||
if (up && !prev_up_) pushKey(SDL_SCANCODE_UP);
|
||||
if (dn && !prev_down_) pushKey(SDL_SCANCODE_DOWN);
|
||||
if (lt && !prev_left_) pushKey(SDL_SCANCODE_LEFT);
|
||||
if (rt && !prev_right_) pushKey(SDL_SCANCODE_RIGHT);
|
||||
// EAST accepta, SOUTH cancela / endarrere
|
||||
if (east && !prev_east_) pushKey(SDL_SCANCODE_RETURN);
|
||||
if (south && !prev_south_) pushKey(SDL_SCANCODE_BACKSPACE);
|
||||
|
||||
// Assegura que el joc no rep tecles de moviment mentre el menú està obert
|
||||
JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, false);
|
||||
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, false);
|
||||
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, false);
|
||||
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, false);
|
||||
} else {
|
||||
// Moviment al joc — level-triggered (polling)
|
||||
JI_SetVirtualKey(SDL_SCANCODE_UP, JI_VSRC_GAMEPAD, up);
|
||||
JI_SetVirtualKey(SDL_SCANCODE_DOWN, JI_VSRC_GAMEPAD, dn);
|
||||
JI_SetVirtualKey(SDL_SCANCODE_LEFT, JI_VSRC_GAMEPAD, lt);
|
||||
JI_SetVirtualKey(SDL_SCANCODE_RIGHT, JI_VSRC_GAMEPAD, rt);
|
||||
// Qualsevol dels 4 botons frontals avança escenes (JI_AnyKey via Enter sintètic)
|
||||
if ((south && !prev_south_) || (east && !prev_east_) ||
|
||||
(west && !prev_west_) || (north && !prev_north_)) {
|
||||
pushKey(SDL_SCANCODE_RETURN);
|
||||
}
|
||||
static void handleGameInput(const PadState& s) {
|
||||
Ji::setVirtualKey(SDL_SCANCODE_UP, Ji::VirtualSource::GAMEPAD, s.up);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_DOWN, Ji::VirtualSource::GAMEPAD, s.down);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_LEFT, Ji::VirtualSource::GAMEPAD, s.left);
|
||||
Ji::setVirtualKey(SDL_SCANCODE_RIGHT, Ji::VirtualSource::GAMEPAD, s.right);
|
||||
const bool ANY_FRONT_EDGE = (s.south && !prev_south) || (s.east && !prev_east) ||
|
||||
(s.west && !prev_west) || (s.north && !prev_north);
|
||||
if (ANY_FRONT_EDGE) {
|
||||
pushKey(SDL_SCANCODE_RETURN);
|
||||
}
|
||||
}
|
||||
|
||||
prev_up_ = up;
|
||||
prev_down_ = dn;
|
||||
prev_left_ = lt;
|
||||
prev_right_ = rt;
|
||||
prev_south_ = south;
|
||||
prev_east_ = east;
|
||||
prev_west_ = west;
|
||||
prev_north_ = north;
|
||||
prev_start_ = start;
|
||||
prev_back_ = back;
|
||||
void update() {
|
||||
if (pad == nullptr) {
|
||||
return;
|
||||
}
|
||||
const PadState S = readPadState();
|
||||
// Flancs globals: Select i Start sempre operen.
|
||||
if (S.back && !prev_back) { pushKey(KeyConfig::scancode("menu_toggle")); }
|
||||
if (S.start && !prev_start) { pushKey(KeyConfig::scancode("pause_toggle")); }
|
||||
if (Menu::isOpen()) {
|
||||
handleMenuNavigation(S);
|
||||
} else {
|
||||
handleGameInput(S);
|
||||
}
|
||||
prev_up = S.up;
|
||||
prev_down = S.down;
|
||||
prev_left = S.left;
|
||||
prev_right = S.right;
|
||||
prev_south = S.south;
|
||||
prev_east = S.east;
|
||||
prev_west = S.west;
|
||||
prev_north = S.north;
|
||||
prev_start = S.start;
|
||||
prev_back = S.back;
|
||||
}
|
||||
|
||||
} // namespace Gamepad
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "core/input/global_inputs.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "core/input/key_config.hpp"
|
||||
@@ -17,117 +18,71 @@ namespace GlobalInputs {
|
||||
static bool fullscreen_prev = false;
|
||||
static bool shader_prev = false;
|
||||
static bool aspect_prev = false;
|
||||
static bool ss_prev = false;
|
||||
static bool next_shader_prev = false;
|
||||
static bool next_preset_prev = false;
|
||||
static bool texture_filter_prev = false;
|
||||
static bool render_info_prev = false;
|
||||
|
||||
// Patró comú: lectura amb detecció de flanc + acumulació al flag "consumed".
|
||||
// `on_press` només s'executa al flanc puja; `prev` es manté actualitzat.
|
||||
static auto edgeTrigger(const char* key_id, bool& prev, const std::function<void()>& on_press) -> bool {
|
||||
const bool PRESSED = Ji::keyPressed(KeyConfig::scancode(key_id));
|
||||
if (PRESSED && !prev) {
|
||||
on_press();
|
||||
}
|
||||
prev = PRESSED;
|
||||
return PRESSED;
|
||||
}
|
||||
|
||||
auto handle() -> bool {
|
||||
bool consumed = false;
|
||||
|
||||
// F1 — Reduir zoom
|
||||
bool dec_zoom = JI_KeyPressed(KeyConfig::scancode("dec_zoom"));
|
||||
if (dec_zoom && !dec_zoom_prev) {
|
||||
consumed |= edgeTrigger("dec_zoom", dec_zoom_prev, [] {
|
||||
Screen::get()->decZoom();
|
||||
char msg[32];
|
||||
snprintf(msg, sizeof(msg), Locale::get("notifications.zoom_fmt"), Screen::get()->getZoom());
|
||||
Overlay::showNotification(msg);
|
||||
}
|
||||
if (dec_zoom) consumed = true;
|
||||
dec_zoom_prev = dec_zoom;
|
||||
|
||||
// F2 — Augmentar zoom
|
||||
bool inc_zoom = JI_KeyPressed(KeyConfig::scancode("inc_zoom"));
|
||||
if (inc_zoom && !inc_zoom_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("inc_zoom", inc_zoom_prev, [] {
|
||||
Screen::get()->incZoom();
|
||||
char msg[32];
|
||||
snprintf(msg, sizeof(msg), Locale::get("notifications.zoom_fmt"), Screen::get()->getZoom());
|
||||
Overlay::showNotification(msg);
|
||||
}
|
||||
if (inc_zoom) consumed = true;
|
||||
inc_zoom_prev = inc_zoom;
|
||||
|
||||
// F3 — Toggle pantalla completa
|
||||
bool fullscreen = JI_KeyPressed(KeyConfig::scancode("fullscreen"));
|
||||
if (fullscreen && !fullscreen_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("fullscreen", fullscreen_prev, [] {
|
||||
Screen::get()->toggleFullscreen();
|
||||
Overlay::showNotification(Screen::get()->isFullscreen() ? Locale::get("notifications.fullscreen") : Locale::get("notifications.windowed"));
|
||||
}
|
||||
if (fullscreen) consumed = true;
|
||||
fullscreen_prev = fullscreen;
|
||||
|
||||
// F4 — Toggle shaders
|
||||
bool shader = JI_KeyPressed(KeyConfig::scancode("toggle_shader"));
|
||||
if (shader && !shader_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("toggle_shader", shader_prev, [] {
|
||||
Screen::get()->toggleShaders();
|
||||
Overlay::showNotification(Options::video.shader_enabled ? Locale::get("notifications.shader_on") : Locale::get("notifications.shader_off"));
|
||||
}
|
||||
if (shader) consumed = true;
|
||||
shader_prev = shader;
|
||||
|
||||
// F5 — Toggle aspect ratio 4:3
|
||||
bool aspect = JI_KeyPressed(KeyConfig::scancode("toggle_aspect_ratio"));
|
||||
if (aspect && !aspect_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("toggle_aspect_ratio", aspect_prev, [] {
|
||||
Screen::get()->toggleAspectRatio();
|
||||
Overlay::showNotification(Options::video.aspect_ratio_4_3 ? Locale::get("notifications.aspect_43") : Locale::get("notifications.aspect_square"));
|
||||
}
|
||||
if (aspect) consumed = true;
|
||||
aspect_prev = aspect;
|
||||
|
||||
// F6 — Toggle supersampling
|
||||
bool ss = JI_KeyPressed(KeyConfig::scancode("toggle_supersampling"));
|
||||
if (ss && !ss_prev) {
|
||||
if (Screen::get()->toggleSupersampling()) {
|
||||
Overlay::showNotification(Options::video.supersampling ? Locale::get("notifications.ss_on") : Locale::get("notifications.ss_off"));
|
||||
}
|
||||
}
|
||||
if (ss) consumed = true;
|
||||
ss_prev = ss;
|
||||
|
||||
// F7 — Canviar tipus de shader (PostFX ↔ CrtPi)
|
||||
bool next_shader = JI_KeyPressed(KeyConfig::scancode("next_shader"));
|
||||
if (next_shader && !next_shader_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("next_shader", next_shader_prev, [] {
|
||||
if (Screen::get()->nextShaderType()) {
|
||||
char msg[64];
|
||||
snprintf(msg, sizeof(msg), "%s: %s", Screen::get()->getActiveShaderName(), Screen::get()->getCurrentPresetName());
|
||||
Overlay::showNotification(msg);
|
||||
}
|
||||
}
|
||||
if (next_shader) consumed = true;
|
||||
next_shader_prev = next_shader;
|
||||
|
||||
// F8 — Pròxim preset del shader actiu
|
||||
bool next_preset = JI_KeyPressed(KeyConfig::scancode("next_shader_preset"));
|
||||
if (next_preset && !next_preset_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("next_shader_preset", next_preset_prev, [] {
|
||||
if (Screen::get()->nextPreset()) {
|
||||
char msg[64];
|
||||
snprintf(msg, sizeof(msg), Locale::get("notifications.preset_fmt"), Screen::get()->getCurrentPresetName());
|
||||
Overlay::showNotification(msg);
|
||||
}
|
||||
}
|
||||
if (next_preset) consumed = true;
|
||||
next_preset_prev = next_preset;
|
||||
|
||||
// F9 — Cicla filtre de textura (NEAREST ↔ LINEAR), sempre aplicat
|
||||
bool texture_filter = JI_KeyPressed(KeyConfig::scancode("cycle_texture_filter"));
|
||||
if (texture_filter && !texture_filter_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("cycle_texture_filter", texture_filter_prev, [] {
|
||||
Screen::get()->cycleTextureFilter(+1);
|
||||
Overlay::showNotification(Options::video.texture_filter == Options::TextureFilter::LINEAR
|
||||
? Locale::get("notifications.filter_linear")
|
||||
: Locale::get("notifications.filter_nearest"));
|
||||
}
|
||||
if (texture_filter) consumed = true;
|
||||
texture_filter_prev = texture_filter;
|
||||
|
||||
// F10 — Toggle render info (FPS, driver, shader)
|
||||
bool render_info = JI_KeyPressed(KeyConfig::scancode("toggle_render_info"));
|
||||
if (render_info && !render_info_prev) {
|
||||
});
|
||||
consumed |= edgeTrigger("toggle_render_info", render_info_prev, [] {
|
||||
Overlay::toggleRenderInfo();
|
||||
}
|
||||
if (render_info) consumed = true;
|
||||
render_info_prev = render_info;
|
||||
|
||||
});
|
||||
return consumed;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
namespace GlobalInputs {
|
||||
// Comprovar una vegada per frame, després de JI_Update()
|
||||
// Comprovar una vegada per frame, després de Ji::update()
|
||||
// Retorna true si ha consumit alguna tecla (per suprimir-la de la capa de joc)
|
||||
auto handle() -> bool;
|
||||
} // namespace GlobalInputs
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "core/input/key_config.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <utility>
|
||||
@@ -10,13 +11,15 @@
|
||||
namespace KeyConfig {
|
||||
|
||||
namespace {
|
||||
std::vector<KeyEntry> entries_;
|
||||
std::unordered_map<std::string, size_t> index_;
|
||||
std::string overrides_path_;
|
||||
std::vector<KeyEntry> key_entries;
|
||||
std::unordered_map<std::string, size_t> index_table;
|
||||
std::string overrides_path;
|
||||
|
||||
auto findIndex(const std::string& id) -> size_t {
|
||||
auto it = index_.find(id);
|
||||
if (it == index_.end()) return SIZE_MAX;
|
||||
auto it = index_table.find(id);
|
||||
if (it == index_table.end()) {
|
||||
return SIZE_MAX;
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
@@ -30,7 +33,9 @@ namespace KeyConfig {
|
||||
std::string content(buf.begin(), buf.end());
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(content);
|
||||
if (!yaml.contains("keys")) return;
|
||||
if (!yaml.contains("keys")) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& node : yaml["keys"]) {
|
||||
KeyEntry entry;
|
||||
@@ -47,10 +52,10 @@ namespace KeyConfig {
|
||||
entry.scancode = sc;
|
||||
entry.default_scancode = sc;
|
||||
|
||||
index_[entry.id] = entries_.size();
|
||||
entries_.push_back(std::move(entry));
|
||||
index_table[entry.id] = key_entries.size();
|
||||
key_entries.push_back(std::move(entry));
|
||||
}
|
||||
std::cout << "KeyConfig: " << entries_.size() << " tecles carregades de "
|
||||
std::cout << "KeyConfig: " << key_entries.size() << " tecles carregades de "
|
||||
<< defaults_resource_path << '\n';
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "KeyConfig: error parsejant YAML: " << e.what() << '\n';
|
||||
@@ -59,7 +64,9 @@ namespace KeyConfig {
|
||||
|
||||
void applyOverrides(const std::string& disk_path) {
|
||||
std::ifstream file(disk_path);
|
||||
if (!file.good()) return;
|
||||
if (!file.good()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string content((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
@@ -67,7 +74,9 @@ namespace KeyConfig {
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(content);
|
||||
if (!yaml.contains("overrides")) return;
|
||||
if (!yaml.contains("overrides")) {
|
||||
return;
|
||||
}
|
||||
|
||||
int applied = 0;
|
||||
for (const auto& kv : yaml["overrides"].as_map()) {
|
||||
@@ -84,8 +93,8 @@ namespace KeyConfig {
|
||||
<< "' per '" << id << "'\n";
|
||||
continue;
|
||||
}
|
||||
entries_[idx].scancode = sc;
|
||||
entries_[idx].code = code;
|
||||
key_entries[idx].scancode = sc;
|
||||
key_entries[idx].code = code;
|
||||
applied++;
|
||||
}
|
||||
if (applied > 0) {
|
||||
@@ -100,66 +109,75 @@ namespace KeyConfig {
|
||||
|
||||
void init(const std::string& defaults_resource_path,
|
||||
const std::string& user_overrides_disk_path) {
|
||||
entries_.clear();
|
||||
index_.clear();
|
||||
overrides_path_ = user_overrides_disk_path;
|
||||
key_entries.clear();
|
||||
index_table.clear();
|
||||
overrides_path = user_overrides_disk_path;
|
||||
|
||||
loadDefaults(defaults_resource_path);
|
||||
if (!overrides_path_.empty()) {
|
||||
applyOverrides(overrides_path_);
|
||||
if (!overrides_path.empty()) {
|
||||
applyOverrides(overrides_path);
|
||||
}
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
entries_.clear();
|
||||
index_.clear();
|
||||
overrides_path_.clear();
|
||||
key_entries.clear();
|
||||
index_table.clear();
|
||||
overrides_path.clear();
|
||||
}
|
||||
|
||||
auto scancode(const std::string& id) -> SDL_Scancode {
|
||||
auto idx = findIndex(id);
|
||||
if (idx == SIZE_MAX) return SDL_SCANCODE_UNKNOWN;
|
||||
return entries_[idx].scancode;
|
||||
if (idx == SIZE_MAX) {
|
||||
return SDL_SCANCODE_UNKNOWN;
|
||||
}
|
||||
return key_entries[idx].scancode;
|
||||
}
|
||||
|
||||
auto scancodePtr(const std::string& id) -> SDL_Scancode* {
|
||||
auto idx = findIndex(id);
|
||||
if (idx == SIZE_MAX) return nullptr;
|
||||
return &entries_[idx].scancode;
|
||||
if (idx == SIZE_MAX) {
|
||||
return nullptr;
|
||||
}
|
||||
return &key_entries[idx].scancode;
|
||||
}
|
||||
|
||||
void setScancode(const std::string& id, SDL_Scancode sc) {
|
||||
auto idx = findIndex(id);
|
||||
if (idx == SIZE_MAX) return;
|
||||
entries_[idx].scancode = sc;
|
||||
if (idx == SIZE_MAX) {
|
||||
return;
|
||||
}
|
||||
key_entries[idx].scancode = sc;
|
||||
const char* name = SDL_GetScancodeName(sc);
|
||||
entries_[idx].code = (name != nullptr) ? name : "";
|
||||
key_entries[idx].code = (name != nullptr) ? name : "";
|
||||
}
|
||||
|
||||
auto isGuiKey(SDL_Scancode sc) -> bool {
|
||||
if (sc == SDL_SCANCODE_UNKNOWN) return false;
|
||||
for (const auto& e : entries_) {
|
||||
if (e.scancode == sc) return true;
|
||||
if (sc == SDL_SCANCODE_UNKNOWN) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
return std::ranges::any_of(key_entries, [sc](const auto& e) { return e.scancode == sc; });
|
||||
}
|
||||
|
||||
auto entries() -> const std::vector<KeyEntry>& {
|
||||
return entries_;
|
||||
return key_entries;
|
||||
}
|
||||
|
||||
auto saveOverrides() -> bool {
|
||||
if (overrides_path_.empty()) return false;
|
||||
if (overrides_path.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recull només les entrades remapeades.
|
||||
std::vector<const KeyEntry*> changed;
|
||||
for (const auto& e : entries_) {
|
||||
if (e.scancode != e.default_scancode) changed.push_back(&e);
|
||||
for (const auto& e : key_entries) {
|
||||
if (e.scancode != e.default_scancode) {
|
||||
changed.push_back(&e);
|
||||
}
|
||||
}
|
||||
|
||||
std::ofstream file(overrides_path_);
|
||||
std::ofstream file(overrides_path);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "KeyConfig: no es pot escriure " << overrides_path_ << '\n';
|
||||
std::cerr << "KeyConfig: no es pot escriure " << overrides_path << '\n';
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,15 +9,17 @@ namespace KeyRemap {
|
||||
|
||||
static void mirror(SDL_Scancode custom, SDL_Scancode standard, const bool* ks) {
|
||||
if (custom == standard || custom == SDL_SCANCODE_UNKNOWN) {
|
||||
JI_SetVirtualKey(standard, JI_VSRC_REMAP, false);
|
||||
Ji::setVirtualKey(standard, Ji::VirtualSource::REMAP, false);
|
||||
return;
|
||||
}
|
||||
JI_SetVirtualKey(standard, JI_VSRC_REMAP, ks[custom]);
|
||||
Ji::setVirtualKey(standard, Ji::VirtualSource::REMAP, ks[custom]);
|
||||
}
|
||||
|
||||
void update() {
|
||||
const bool* ks = SDL_GetKeyboardState(nullptr);
|
||||
if (!ks) return;
|
||||
if (ks == nullptr) {
|
||||
return;
|
||||
}
|
||||
mirror(Options::keys_game.up, SDL_SCANCODE_UP, ks);
|
||||
mirror(Options::keys_game.down, SDL_SCANCODE_DOWN, ks);
|
||||
mirror(Options::keys_game.left, SDL_SCANCODE_LEFT, ks);
|
||||
|
||||
+91
-70
@@ -12,24 +12,26 @@
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
|
||||
#endif
|
||||
#include "external/gif.h"
|
||||
// NOLINTBEGIN(clang-analyzer-unix.Malloc): codi extern de tercers, no l'auditem.
|
||||
#include <gif.h> // tercer-part: resolt via SYSTEM include path (source/external/)
|
||||
// NOLINTEND(clang-analyzer-unix.Malloc)
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic pop
|
||||
#elif defined(__GNUC__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
|
||||
JD8_Surface screen = nullptr;
|
||||
JD8_Palette main_palette = nullptr;
|
||||
Jd8::Surface screen = nullptr;
|
||||
Jd8::Palette main_palette = nullptr;
|
||||
Uint32* pixel_data = nullptr;
|
||||
|
||||
void JD8_Init() {
|
||||
void Jd8::init() {
|
||||
screen = new Uint8[64000]{};
|
||||
main_palette = new Color[256]{};
|
||||
pixel_data = new Uint32[320 * 200]{};
|
||||
pixel_data = new Uint32[std::size_t{320} * 200]{};
|
||||
}
|
||||
|
||||
void JD8_Quit() {
|
||||
void Jd8::quit() {
|
||||
delete[] screen;
|
||||
delete[] main_palette;
|
||||
delete[] pixel_data;
|
||||
@@ -38,63 +40,64 @@ void JD8_Quit() {
|
||||
pixel_data = nullptr;
|
||||
}
|
||||
|
||||
void JD8_ClearScreen(Uint8 color) {
|
||||
void Jd8::clearScreen(Uint8 color) {
|
||||
memset(screen, color, 64000);
|
||||
}
|
||||
|
||||
JD8_Surface JD8_NewSurface() {
|
||||
auto Jd8::newSurface() -> Jd8::Surface {
|
||||
return new Uint8[64000]{};
|
||||
}
|
||||
|
||||
// Helper intern: deriva el basename d'una ruta per a buscar al Cache.
|
||||
static std::string jd8_basename(const char* file) {
|
||||
static auto pathBasename(const char* file) -> std::string {
|
||||
std::string s = file;
|
||||
auto pos = s.find_last_of("/\\");
|
||||
return pos == std::string::npos ? s : s.substr(pos + 1);
|
||||
}
|
||||
|
||||
JD8_Surface JD8_LoadSurface(const char* file) {
|
||||
auto Jd8::loadSurface(const char* file) -> Jd8::Surface {
|
||||
// Prova primer el Resource::Cache. Si l'asset és precarregat, copiem
|
||||
// els 64KB des del cache (microsegons) i ens estalviem la decodificació
|
||||
// GIF. Mantenim el contracte de la funció: el caller rep un buffer
|
||||
// fresc que ha d'alliberar amb JD8_FreeSurface.
|
||||
// fresc que ha d'alliberar amb Jd8::freeSurface.
|
||||
if (Resource::Cache::get() != nullptr) {
|
||||
try {
|
||||
const auto& cached = Resource::Cache::get()->getSurfacePixels(jd8_basename(file));
|
||||
JD8_Surface image = JD8_NewSurface();
|
||||
const auto& cached = Resource::Cache::get()->getSurfacePixels(pathBasename(file));
|
||||
Jd8::Surface image = Jd8::newSurface();
|
||||
memcpy(image, cached.data(), 64000);
|
||||
return image;
|
||||
} catch (const std::exception&) {
|
||||
// No està al cache (asset no llistat al manifest). Fallback.
|
||||
// @INTENTIONAL: no està al cache (asset no llistat al manifest), fallback al loader.
|
||||
}
|
||||
}
|
||||
|
||||
auto buffer = ResourceHelper::loadFile(file);
|
||||
unsigned short w, h;
|
||||
unsigned short w;
|
||||
unsigned short h;
|
||||
Uint8* pixels = LoadGif(buffer.data(), &w, &h);
|
||||
if (pixels == nullptr) {
|
||||
printf("Unable to load bitmap: %s\n", SDL_GetError());
|
||||
exit(1);
|
||||
}
|
||||
JD8_Surface image = JD8_NewSurface();
|
||||
Jd8::Surface image = Jd8::newSurface();
|
||||
memcpy(image, pixels, 64000);
|
||||
free(pixels);
|
||||
return image;
|
||||
}
|
||||
|
||||
JD8_Palette JD8_LoadPalette(const char* file) {
|
||||
auto Jd8::loadPalette(const char* file) -> Jd8::Palette {
|
||||
// Sempre retorna un buffer de 256 colors reservat amb `new Color[256]`
|
||||
// — el caller és responsable d'alliberar-lo amb `delete[]` (o lliurar-ne
|
||||
// l'ownership a `JD8_SetScreenPalette`).
|
||||
JD8_Palette palette = new Color[256];
|
||||
// l'ownership a `Jd8::setScreenPalette`).
|
||||
auto* palette = new Color[256];
|
||||
|
||||
if (Resource::Cache::get() != nullptr) {
|
||||
try {
|
||||
const auto& cached = Resource::Cache::get()->getPaletteBytes(jd8_basename(file));
|
||||
const auto& cached = Resource::Cache::get()->getPaletteBytes(pathBasename(file));
|
||||
memcpy(palette, cached.data(), 768);
|
||||
return palette;
|
||||
} catch (const std::exception&) {
|
||||
// No està al cache — fallback a lectura + LoadPalette.
|
||||
// @INTENTIONAL: no està al cache, fallback a lectura + LoadPalette.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,19 +108,21 @@ JD8_Palette JD8_LoadPalette(const char* file) {
|
||||
return palette;
|
||||
}
|
||||
|
||||
void JD8_SetScreenPalette(JD8_Palette palette) {
|
||||
if (main_palette == palette) return;
|
||||
void Jd8::setScreenPalette(Jd8::Palette palette) {
|
||||
if (main_palette == palette) {
|
||||
return;
|
||||
}
|
||||
delete[] main_palette;
|
||||
main_palette = palette;
|
||||
}
|
||||
|
||||
void JD8_FillSquare(int ini, int height, Uint8 color) {
|
||||
const int offset = ini * 320;
|
||||
const int size = height * 320;
|
||||
memset(&screen[offset], color, size);
|
||||
void Jd8::fillSquare(int ini, int height, Uint8 color) {
|
||||
const int OFFSET = ini * 320;
|
||||
const int SIZE = height * 320;
|
||||
memset(&screen[OFFSET], color, SIZE);
|
||||
}
|
||||
|
||||
void JD8_FillRect(int x, int y, int w, int h, Uint8 color) {
|
||||
void Jd8::fillRect(int x, int y, int w, int h, Uint8 color) {
|
||||
if (x < 0) {
|
||||
w += x;
|
||||
x = 0;
|
||||
@@ -126,19 +131,25 @@ void JD8_FillRect(int x, int y, int w, int h, Uint8 color) {
|
||||
h += y;
|
||||
y = 0;
|
||||
}
|
||||
if (x + w > 320) w = 320 - x;
|
||||
if (y + h > 200) h = 200 - y;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
if (x + w > 320) {
|
||||
w = 320 - x;
|
||||
}
|
||||
if (y + h > 200) {
|
||||
h = 200 - y;
|
||||
}
|
||||
if (w <= 0 || h <= 0) {
|
||||
return;
|
||||
}
|
||||
for (int row = y; row < y + h; ++row) {
|
||||
memset(&screen[x + (row * 320)], color, w);
|
||||
}
|
||||
}
|
||||
|
||||
void JD8_Blit(JD8_Surface surface) {
|
||||
void Jd8::blit(const Uint8* surface) {
|
||||
memcpy(screen, surface, 64000);
|
||||
}
|
||||
|
||||
void JD8_Blit(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh) {
|
||||
void Jd8::blit(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh) {
|
||||
int src_pointer = sx + (sy * 320);
|
||||
int dst_pointer = x + (y * 320);
|
||||
for (int i = 0; i < sh; i++) {
|
||||
@@ -148,7 +159,7 @@ void JD8_Blit(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh)
|
||||
}
|
||||
}
|
||||
|
||||
void JD8_BlitToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, JD8_Surface dest) {
|
||||
void Jd8::blitToSurface(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Jd8::Surface dest) {
|
||||
int src_pointer = sx + (sy * 320);
|
||||
int dst_pointer = x + (y * 320);
|
||||
for (int i = 0; i < sh; i++) {
|
||||
@@ -158,54 +169,62 @@ void JD8_BlitToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int sw
|
||||
}
|
||||
}
|
||||
|
||||
void JD8_BlitCK(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, Uint8 colorkey) {
|
||||
void Jd8::blitCK(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Uint8 colorkey) {
|
||||
int src_pointer = sx + (sy * 320);
|
||||
int dst_pointer = x + (y * 320);
|
||||
for (int j = 0; j < sh; j++) {
|
||||
for (int i = 0; i < sw; i++) {
|
||||
if (surface[src_pointer + i] != colorkey) screen[dst_pointer + i] = surface[src_pointer + i];
|
||||
if (surface[src_pointer + i] != colorkey) {
|
||||
screen[dst_pointer + i] = surface[src_pointer + i];
|
||||
}
|
||||
}
|
||||
src_pointer += 320;
|
||||
dst_pointer += 320;
|
||||
}
|
||||
}
|
||||
|
||||
void JD8_BlitCKCut(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, Uint8 colorkey) {
|
||||
void Jd8::blitCKCut(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Uint8 colorkey) {
|
||||
int src_pointer = sx + (sy * 320);
|
||||
int dst_pointer = x + (y * 320);
|
||||
for (int j = 0; j < sh; j++) {
|
||||
for (int i = 0; i < sw; i++) {
|
||||
if (surface[src_pointer + i] != colorkey && (x + i >= 0) && (y + j >= 0) && (x + i < 320) && (y + j < 200)) screen[dst_pointer + i] = surface[src_pointer + i];
|
||||
if (surface[src_pointer + i] != colorkey && (x + i >= 0) && (y + j >= 0) && (x + i < 320) && (y + j < 200)) {
|
||||
screen[dst_pointer + i] = surface[src_pointer + i];
|
||||
}
|
||||
}
|
||||
src_pointer += 320;
|
||||
dst_pointer += 320;
|
||||
}
|
||||
}
|
||||
|
||||
void JD8_BlitCKScroll(int y, JD8_Surface surface, int sx, int sy, int sh, Uint8 colorkey) {
|
||||
void Jd8::blitCKScroll(int y, const Uint8* surface, int sx, int sy, int sh, Uint8 colorkey) {
|
||||
int dst_pointer = y * 320;
|
||||
for (int j = sy; j < sy + sh; j++) {
|
||||
for (int i = 0; i < 320; i++) {
|
||||
int x = (i + sx) % 320;
|
||||
if (surface[x + j * 320] != colorkey) screen[dst_pointer] = surface[x + j * 320];
|
||||
if (surface[x + (j * 320)] != colorkey) {
|
||||
screen[dst_pointer] = surface[x + (j * 320)];
|
||||
}
|
||||
dst_pointer++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void JD8_BlitCKToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, JD8_Surface dest, Uint8 colorkey) {
|
||||
void Jd8::blitCKToSurface(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Jd8::Surface dest, Uint8 colorkey) {
|
||||
int src_pointer = sx + (sy * 320);
|
||||
int dst_pointer = x + (y * 320);
|
||||
for (int j = 0; j < sh; j++) {
|
||||
for (int i = 0; i < sw; i++) {
|
||||
if (surface[src_pointer + i] != colorkey) dest[dst_pointer + i] = surface[src_pointer + i];
|
||||
if (surface[src_pointer + i] != colorkey) {
|
||||
dest[dst_pointer + i] = surface[src_pointer + i];
|
||||
}
|
||||
}
|
||||
src_pointer += 320;
|
||||
dst_pointer += 320;
|
||||
}
|
||||
}
|
||||
|
||||
void JD8_Flip() {
|
||||
void Jd8::flip() {
|
||||
// Converteix el framebuffer indexat (paletted) a ARGB (pixel_data).
|
||||
// El Director crida aquesta funció després del tick de cada escena
|
||||
// per preparar el frame abans de presentar-lo. Ja no fa yield —
|
||||
@@ -218,23 +237,23 @@ void JD8_Flip() {
|
||||
}
|
||||
}
|
||||
|
||||
Uint32* JD8_GetFramebuffer() {
|
||||
auto Jd8::getFramebuffer() -> Uint32* {
|
||||
return pixel_data;
|
||||
}
|
||||
|
||||
void JD8_FreeSurface(JD8_Surface surface) {
|
||||
void Jd8::freeSurface(Jd8::Surface surface) { // NOLINT(readability-non-const-parameter): allibera memòria, no pot ser const
|
||||
delete[] surface;
|
||||
}
|
||||
|
||||
Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y) {
|
||||
auto Jd8::getPixel(const Uint8* surface, int x, int y) -> Uint8 {
|
||||
return surface[x + (y * 320)];
|
||||
}
|
||||
|
||||
void JD8_PutPixel(JD8_Surface surface, int x, int y, Uint8 pixel) {
|
||||
void Jd8::putPixel(Jd8::Surface surface, int x, int y, Uint8 pixel) {
|
||||
surface[x + (y * 320)] = pixel;
|
||||
}
|
||||
|
||||
void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b) {
|
||||
void Jd8::setPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b) {
|
||||
main_palette[index].r = r << 2;
|
||||
main_palette[index].g = g << 2;
|
||||
main_palette[index].b = b << 2;
|
||||
@@ -245,26 +264,26 @@ void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b) {
|
||||
// el caller decideix quan fer Flip.
|
||||
namespace {
|
||||
|
||||
enum class FadeType {
|
||||
None = 0,
|
||||
Out,
|
||||
ToPal,
|
||||
enum class FadeType : std::uint8_t {
|
||||
NONE = 0,
|
||||
OUT,
|
||||
TO_PAL,
|
||||
};
|
||||
|
||||
constexpr int FADE_STEPS = 32;
|
||||
|
||||
FadeType fade_type = FadeType::None;
|
||||
FadeType fade_type = FadeType::NONE;
|
||||
Color fade_target[256];
|
||||
int fade_step = 0;
|
||||
|
||||
void apply_fade_step() {
|
||||
if (fade_type == FadeType::Out) {
|
||||
void applyFadeStep() {
|
||||
if (fade_type == FadeType::OUT) {
|
||||
for (int i = 0; i < 256; i++) {
|
||||
main_palette[i].r = main_palette[i].r >= 8 ? main_palette[i].r - 8 : 0;
|
||||
main_palette[i].g = main_palette[i].g >= 8 ? main_palette[i].g - 8 : 0;
|
||||
main_palette[i].b = main_palette[i].b >= 8 ? main_palette[i].b - 8 : 0;
|
||||
}
|
||||
} else if (fade_type == FadeType::ToPal) {
|
||||
} else if (fade_type == FadeType::TO_PAL) {
|
||||
for (int i = 0; i < 256; i++) {
|
||||
main_palette[i].r = main_palette[i].r <= int(fade_target[i].r) - 8
|
||||
? main_palette[i].r + 8
|
||||
@@ -281,37 +300,39 @@ namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
void JD8_FadeStartOut() {
|
||||
fade_type = FadeType::Out;
|
||||
void Jd8::fadeStartOut() {
|
||||
fade_type = FadeType::OUT;
|
||||
fade_step = 0;
|
||||
}
|
||||
|
||||
void JD8_FadeStartToPal(JD8_Palette pal) {
|
||||
fade_type = FadeType::ToPal;
|
||||
void Jd8::fadeStartToPal(const Color* pal) {
|
||||
fade_type = FadeType::TO_PAL;
|
||||
memcpy(fade_target, pal, sizeof(Color) * 256);
|
||||
fade_step = 0;
|
||||
}
|
||||
|
||||
bool JD8_FadeIsActive() {
|
||||
return fade_type != FadeType::None;
|
||||
auto Jd8::fadeIsActive() -> bool {
|
||||
return fade_type != FadeType::NONE;
|
||||
}
|
||||
|
||||
bool JD8_FadeTickStep() {
|
||||
if (fade_type == FadeType::None) return true;
|
||||
auto Jd8::fadeTickStep() -> bool {
|
||||
if (fade_type == FadeType::NONE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
apply_fade_step();
|
||||
applyFadeStep();
|
||||
fade_step++;
|
||||
|
||||
if (fade_step >= FADE_STEPS) {
|
||||
fade_type = FadeType::None;
|
||||
fade_type = FadeType::NONE;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Els shims bloquejants `JD8_FadeOut` i `JD8_FadeToPal` han estat
|
||||
// eliminats a Phase B.2: feien un bucle de 32 iteracions amb `JD8_Flip`
|
||||
// eliminats a Phase B.2: feien un bucle de 32 iteracions amb `Jd8::flip`
|
||||
// entre cada una que només funcionava mentre l'entorn tenia fibers i
|
||||
// `JD8_Flip` cedia el control al Director. Ara tot fade es fa tick a
|
||||
// tick via `scenes::PaletteFade` (que encapsula `JD8_FadeStartOut` /
|
||||
// `JD8_FadeStartToPal` + `JD8_FadeTickStep`).
|
||||
// `Jd8::flip` cedia el control al Director. Ara tot fade es fa tick a
|
||||
// tick via `Scenes::PaletteFade` (que encapsula `Jd8::fadeStartOut` /
|
||||
// `Jd8::fadeStartToPal` + `Jd8::fadeTickStep`).
|
||||
|
||||
+78
-80
@@ -1,80 +1,78 @@
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
struct Color {
|
||||
Uint8 r;
|
||||
Uint8 g;
|
||||
Uint8 b;
|
||||
};
|
||||
|
||||
using JD8_Surface = Uint8*;
|
||||
using JD8_Palette = Color*;
|
||||
|
||||
void JD8_Init();
|
||||
|
||||
void JD8_Quit();
|
||||
|
||||
void JD8_ClearScreen(Uint8 color);
|
||||
|
||||
JD8_Surface JD8_NewSurface();
|
||||
|
||||
JD8_Surface JD8_LoadSurface(const char* file);
|
||||
|
||||
JD8_Palette JD8_LoadPalette(const char* file);
|
||||
|
||||
void JD8_SetScreenPalette(JD8_Palette palette);
|
||||
|
||||
void JD8_FillSquare(int ini, int height, Uint8 color);
|
||||
|
||||
// Omple un rectangle arbitrari de la pantalla amb un color paletat.
|
||||
// Pensat per a UI senzilla (barra de progrés del BootLoader, etc.).
|
||||
void JD8_FillRect(int x, int y, int w, int h, Uint8 color);
|
||||
|
||||
void JD8_Blit(JD8_Surface surface);
|
||||
|
||||
void JD8_Blit(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh);
|
||||
|
||||
void JD8_BlitToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, JD8_Surface dest);
|
||||
|
||||
void JD8_BlitCK(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, Uint8 colorkey);
|
||||
|
||||
void JD8_BlitCKCut(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, Uint8 colorkey);
|
||||
|
||||
void JD8_BlitCKScroll(int y, JD8_Surface surface, int sx, int sy, int sh, Uint8 colorkey);
|
||||
|
||||
void JD8_BlitCKToSurface(int x, int y, JD8_Surface surface, int sx, int sy, int sw, int sh, JD8_Surface dest, Uint8 colorkey);
|
||||
|
||||
// Converteix la pantalla indexada a ARGB. El Director crida aquesta
|
||||
// funció al final de cada tick i després llegeix el framebuffer via
|
||||
// JD8_GetFramebuffer() per presentar-lo.
|
||||
void JD8_Flip();
|
||||
|
||||
// Accés al framebuffer ARGB de 320x200 actualitzat per l'última crida a
|
||||
// JD8_Flip(). Propietat de jdraw8 — el caller no ha de lliberar-lo.
|
||||
Uint32* JD8_GetFramebuffer();
|
||||
|
||||
void JD8_FreeSurface(JD8_Surface surface);
|
||||
|
||||
Uint8 JD8_GetPixel(JD8_Surface surface, int x, int y);
|
||||
|
||||
void JD8_PutPixel(JD8_Surface surface, int x, int y, Uint8 pixel);
|
||||
|
||||
void JD8_SetPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b);
|
||||
|
||||
// API de fade no bloquejant (màquina d'estats). `FadeStart*` inicia el
|
||||
// fade; `FadeTickStep` aplica un pas i retorna `true` quan el fade ha
|
||||
// acabat. Un pas correspon visualment a una iteració del fade original
|
||||
// (32 passos en total). El caller és responsable de fer el Flip entre
|
||||
// passos si el vol veure animat. `FadeIsActive` permet saber si hi ha
|
||||
// un fade en curs per a enllaçar-lo amb un altre subsistema.
|
||||
// L'embolcall `scenes::PaletteFade` ho fa més idiomàtic per a escenes.
|
||||
void JD8_FadeStartOut();
|
||||
void JD8_FadeStartToPal(JD8_Palette pal);
|
||||
bool JD8_FadeTickStep();
|
||||
bool JD8_FadeIsActive();
|
||||
|
||||
// JD_Font JD_LoadFont( char *file, int width, int height);
|
||||
|
||||
// void JD_DrawText( int x, int y, JD_Font *source, char *text);
|
||||
|
||||
// char *JD_GetFPS();
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
struct Color {
|
||||
Uint8 r;
|
||||
Uint8 g;
|
||||
Uint8 b;
|
||||
};
|
||||
|
||||
namespace Jd8 {
|
||||
|
||||
using Surface = Uint8*;
|
||||
using Palette = Color*;
|
||||
|
||||
void init();
|
||||
|
||||
void quit();
|
||||
|
||||
void clearScreen(Uint8 color);
|
||||
|
||||
auto newSurface() -> Surface;
|
||||
|
||||
auto loadSurface(const char* file) -> Surface;
|
||||
|
||||
auto loadPalette(const char* file) -> Palette;
|
||||
|
||||
void setScreenPalette(Palette palette);
|
||||
|
||||
void fillSquare(int ini, int height, Uint8 color);
|
||||
|
||||
// Omple un rectangle arbitrari de la pantalla amb un color paletat.
|
||||
// Pensat per a UI senzilla (barra de progrés del BootLoader, etc.).
|
||||
void fillRect(int x, int y, int w, int h, Uint8 color);
|
||||
|
||||
void blit(const Uint8* surface);
|
||||
|
||||
void blit(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh);
|
||||
|
||||
void blitToSurface(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Surface dest);
|
||||
|
||||
void blitCK(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Uint8 colorkey);
|
||||
|
||||
void blitCKCut(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Uint8 colorkey);
|
||||
|
||||
void blitCKScroll(int y, const Uint8* surface, int sx, int sy, int sh, Uint8 colorkey);
|
||||
|
||||
void blitCKToSurface(int x, int y, const Uint8* surface, int sx, int sy, int sw, int sh, Surface dest, Uint8 colorkey);
|
||||
|
||||
// Converteix la pantalla indexada a ARGB. El Director crida aquesta
|
||||
// funció al final de cada tick i després llegeix el framebuffer via
|
||||
// getFramebuffer() per presentar-lo.
|
||||
void flip();
|
||||
|
||||
// Accés al framebuffer ARGB de 320x200 actualitzat per l'última crida a
|
||||
// flip(). Propietat de jdraw8 — el caller no ha de lliberar-lo.
|
||||
auto getFramebuffer() -> Uint32*;
|
||||
|
||||
void freeSurface(Surface surface);
|
||||
|
||||
auto getPixel(const Uint8* surface, int x, int y) -> Uint8;
|
||||
|
||||
void putPixel(Surface surface, int x, int y, Uint8 pixel);
|
||||
|
||||
void setPaletteColor(Uint8 index, Uint8 r, Uint8 g, Uint8 b);
|
||||
|
||||
// API de fade no bloquejant (màquina d'estats). `fadeStart*` inicia el
|
||||
// fade; `fadeTickStep` aplica un pas i retorna `true` quan el fade ha
|
||||
// acabat. Un pas correspon visualment a una iteració del fade original
|
||||
// (32 passos en total). El caller és responsable de fer el Flip entre
|
||||
// passos si el vol veure animat. `fadeIsActive` permet saber si hi ha
|
||||
// un fade en curs per a enllaçar-lo amb un altre subsistema.
|
||||
// L'embolcall `Scenes::PaletteFade` ho fa més idiomàtic per a escenes.
|
||||
void fadeStartOut();
|
||||
void fadeStartToPal(const Color* pal);
|
||||
auto fadeTickStep() -> bool;
|
||||
auto fadeIsActive() -> bool;
|
||||
|
||||
} // namespace Jd8
|
||||
|
||||
+55
-43
@@ -3,6 +3,7 @@
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
@@ -14,33 +15,39 @@
|
||||
|
||||
namespace {
|
||||
|
||||
struct keyvalue {
|
||||
struct Keyvalue {
|
||||
std::string key;
|
||||
std::string value;
|
||||
};
|
||||
|
||||
std::vector<keyvalue> config;
|
||||
std::vector<Keyvalue> config;
|
||||
std::string resource_folder;
|
||||
std::string config_folder;
|
||||
|
||||
void load_config_values() {
|
||||
void loadConfigValues() {
|
||||
config.clear();
|
||||
const std::string config_file = config_folder + "/config.txt";
|
||||
std::ifstream fi(config_file);
|
||||
if (!fi.is_open()) return;
|
||||
const std::string CONFIG_FILE = config_folder + "/config.txt";
|
||||
std::ifstream fi(CONFIG_FILE);
|
||||
if (!fi.is_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(fi, line)) {
|
||||
const auto eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
config.push_back({line.substr(0, eq), line.substr(eq + 1)});
|
||||
const auto EQ = line.find('=');
|
||||
if (EQ == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
config.push_back({line.substr(0, EQ), line.substr(EQ + 1)});
|
||||
}
|
||||
}
|
||||
|
||||
void save_config_values() {
|
||||
const std::string config_file = config_folder + "/config.txt";
|
||||
std::ofstream fo(config_file);
|
||||
if (!fo.is_open()) return;
|
||||
void saveConfigValues() {
|
||||
const std::string CONFIG_FILE = config_folder + "/config.txt";
|
||||
std::ofstream fo(CONFIG_FILE);
|
||||
if (!fo.is_open()) {
|
||||
return;
|
||||
}
|
||||
for (const auto& pair : config) {
|
||||
fo << pair.key << '=' << pair.value << '\n';
|
||||
}
|
||||
@@ -48,17 +55,17 @@ namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
void file_setresourcefolder(const char* str) {
|
||||
void Jf::setResourceFolder(const char* str) {
|
||||
resource_folder = str;
|
||||
}
|
||||
|
||||
const char* file_getresourcefolder() {
|
||||
auto Jf::getResourceFolder() -> const char* {
|
||||
return resource_folder.c_str();
|
||||
}
|
||||
|
||||
// Crea la carpeta del sistema on guardar les dades.
|
||||
// Accepta rutes amb subdirectoris (ex: "jailgames/aee") i crea tota la jerarquia.
|
||||
void file_setconfigfolder(const char* foldername) {
|
||||
void Jf::setConfigFolder(const char* foldername) {
|
||||
#ifdef _WIN32
|
||||
const char* base = getenv("APPDATA");
|
||||
if (!base) base = "C:/";
|
||||
@@ -76,15 +83,18 @@ void file_setconfigfolder(const char* foldername) {
|
||||
// arranque dins del navegador. La config no persistirà entre recàrregues
|
||||
// (MEMFS és volàtil); caldria IDBFS si volguéssem persistència a web.
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
const char* homedir = (pw && pw->pw_dir && pw->pw_dir[0]) ? pw->pw_dir : nullptr;
|
||||
if (!homedir || !homedir[0]) homedir = getenv("HOME");
|
||||
if (!homedir || !homedir[0]) homedir = "/tmp";
|
||||
const char* homedir = ((pw != nullptr) && (pw->pw_dir != nullptr) && (pw->pw_dir[0] != 0)) ? pw->pw_dir : nullptr;
|
||||
if ((homedir == nullptr) || (homedir[0] == 0)) {
|
||||
homedir = getenv("HOME");
|
||||
}
|
||||
if ((homedir == nullptr) || (homedir[0] == 0)) {
|
||||
homedir = "/tmp";
|
||||
}
|
||||
config_folder = std::string(homedir) + "/.config/" + foldername;
|
||||
#else
|
||||
config_folder = std::string("/tmp/jailgames_config/") + foldername;
|
||||
#endif
|
||||
|
||||
if (config_folder.empty()) {
|
||||
config_folder = "/tmp/jailgames_config";
|
||||
}
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(config_folder, ec);
|
||||
// A emscripten/MEMFS create_directories pot fallar (p.ex. parent
|
||||
@@ -92,33 +102,35 @@ void file_setconfigfolder(const char* foldername) {
|
||||
// volàtil al navegador de totes formes: ignorem l'error i continuem.
|
||||
}
|
||||
|
||||
const char* file_getconfigfolder() {
|
||||
thread_local std::string folder;
|
||||
folder = config_folder + "/";
|
||||
return folder.c_str();
|
||||
auto Jf::getConfigFolder() -> const char* {
|
||||
thread_local std::string folder_;
|
||||
folder_ = config_folder + "/";
|
||||
return folder_.c_str();
|
||||
}
|
||||
|
||||
const char* file_getconfigvalue(const char* key) {
|
||||
if (config.empty()) load_config_values();
|
||||
for (const auto& pair : config) {
|
||||
if (pair.key == key) {
|
||||
thread_local std::string value_cache;
|
||||
value_cache = pair.value;
|
||||
return value_cache.c_str();
|
||||
}
|
||||
auto Jf::getConfigValue(const char* key) -> const char* {
|
||||
if (config.empty()) {
|
||||
loadConfigValues();
|
||||
}
|
||||
const auto IT = std::ranges::find_if(config, [key](const Keyvalue& pair) { return pair.key == key; });
|
||||
if (IT != config.end()) {
|
||||
thread_local std::string value_cache_;
|
||||
value_cache_ = IT->value;
|
||||
return value_cache_.c_str();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void file_setconfigvalue(const char* key, const char* value) {
|
||||
if (config.empty()) load_config_values();
|
||||
for (auto& pair : config) {
|
||||
if (pair.key == key) {
|
||||
pair.value = value;
|
||||
save_config_values();
|
||||
return;
|
||||
}
|
||||
void Jf::setConfigValue(const char* key, const char* value) {
|
||||
if (config.empty()) {
|
||||
loadConfigValues();
|
||||
}
|
||||
const auto IT = std::ranges::find_if(config, [key](const Keyvalue& pair) { return pair.key == key; });
|
||||
if (IT != config.end()) {
|
||||
IT->value = value;
|
||||
saveConfigValues();
|
||||
return;
|
||||
}
|
||||
config.push_back({std::string(key), std::string(value)});
|
||||
save_config_values();
|
||||
saveConfigValues();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
void file_setconfigfolder(const char* foldername);
|
||||
const char* file_getconfigfolder();
|
||||
namespace Jf {
|
||||
|
||||
void file_setresourcefolder(const char* str);
|
||||
const char* file_getresourcefolder();
|
||||
void setConfigFolder(const char* foldername);
|
||||
auto getConfigFolder() -> const char*;
|
||||
|
||||
const char* file_getconfigvalue(const char* key);
|
||||
void file_setconfigvalue(const char* key, const char* value);
|
||||
void setResourceFolder(const char* str);
|
||||
auto getResourceFolder() -> const char*;
|
||||
|
||||
auto getConfigValue(const char* key) -> const char*;
|
||||
void setConfigValue(const char* key, const char* value);
|
||||
|
||||
} // namespace Jf
|
||||
|
||||
+19
-19
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace {
|
||||
|
||||
bool quitting = false;
|
||||
bool is_quitting = false;
|
||||
Uint32 update_ticks = 0;
|
||||
Uint32 update_time = 0;
|
||||
Uint32 cycle_counter = 0;
|
||||
@@ -10,48 +10,48 @@ namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
void JG_Init() {
|
||||
void Jg::init() {
|
||||
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
|
||||
update_time = SDL_GetTicks();
|
||||
last_delta_time = update_time;
|
||||
}
|
||||
|
||||
void JG_Finalize() {
|
||||
void Jg::finalize() {
|
||||
SDL_Quit();
|
||||
}
|
||||
|
||||
void JG_QuitSignal() {
|
||||
quitting = true;
|
||||
void Jg::quitSignal() {
|
||||
is_quitting = true;
|
||||
}
|
||||
|
||||
bool JG_Quitting() {
|
||||
return quitting;
|
||||
auto Jg::quitting() -> bool {
|
||||
return is_quitting;
|
||||
}
|
||||
|
||||
void JG_SetUpdateTicks(Uint32 milliseconds) {
|
||||
void Jg::setUpdateTicks(Uint32 milliseconds) {
|
||||
update_ticks = milliseconds;
|
||||
}
|
||||
|
||||
bool JG_ShouldUpdate() {
|
||||
const Uint32 now = SDL_GetTicks();
|
||||
if (now - update_time > update_ticks) {
|
||||
update_time = now;
|
||||
auto Jg::shouldUpdate() -> bool {
|
||||
const Uint32 NOW = SDL_GetTicks();
|
||||
if (NOW - update_time > update_ticks) {
|
||||
update_time = NOW;
|
||||
cycle_counter++;
|
||||
return true;
|
||||
}
|
||||
// No toca update — retornem false sense més. Des de Phase B.2 ja no
|
||||
// hi ha fibers: cap caller fa spin-waits (`while (!JG_ShouldUpdate())`)
|
||||
// hi ha fibers: cap caller fa spin-waits (`while (!Jg::shouldUpdate())`)
|
||||
// i el Director pren el control del main loop frame a frame.
|
||||
return false;
|
||||
}
|
||||
|
||||
Uint32 JG_GetCycleCounter() {
|
||||
auto Jg::getCycleCounter() -> Uint32 {
|
||||
return cycle_counter;
|
||||
}
|
||||
|
||||
Uint32 JG_GetDeltaMs() {
|
||||
const Uint32 now = SDL_GetTicks();
|
||||
const Uint32 delta = now - last_delta_time;
|
||||
last_delta_time = now;
|
||||
return delta;
|
||||
auto Jg::getDeltaMs() -> Uint32 {
|
||||
const Uint32 NOW = SDL_GetTicks();
|
||||
const Uint32 DELTA = NOW - last_delta_time;
|
||||
last_delta_time = NOW;
|
||||
return DELTA;
|
||||
}
|
||||
|
||||
+24
-20
@@ -1,20 +1,24 @@
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
void JG_Init();
|
||||
|
||||
void JG_Finalize();
|
||||
|
||||
void JG_QuitSignal();
|
||||
|
||||
bool JG_Quitting();
|
||||
|
||||
void JG_SetUpdateTicks(Uint32 milliseconds);
|
||||
|
||||
bool JG_ShouldUpdate();
|
||||
|
||||
Uint32 JG_GetCycleCounter();
|
||||
|
||||
// Temps transcorregut (en ms) des de l'última crida a JG_GetDeltaMs.
|
||||
// Helper per a la migració progressiva a time-based (Fase 4+).
|
||||
Uint32 JG_GetDeltaMs();
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace Jg {
|
||||
|
||||
void init();
|
||||
|
||||
void finalize();
|
||||
|
||||
void quitSignal();
|
||||
|
||||
auto quitting() -> bool;
|
||||
|
||||
void setUpdateTicks(Uint32 milliseconds);
|
||||
|
||||
auto shouldUpdate() -> bool;
|
||||
|
||||
auto getCycleCounter() -> Uint32;
|
||||
|
||||
// Temps transcorregut (en ms) des de l'última crida a Jg::getDeltaMs.
|
||||
// Helper per a la migració progressiva a time-based (Fase 4+).
|
||||
auto getDeltaMs() -> Uint32;
|
||||
|
||||
} // namespace Jg
|
||||
|
||||
+60
-41
@@ -1,5 +1,6 @@
|
||||
#include "core/jail/jinput.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "core/system/director.hpp"
|
||||
@@ -16,20 +17,20 @@ namespace {
|
||||
|
||||
bool key_pressed = false;
|
||||
|
||||
// Temps restant en mil·lisegons durant el qual JI_KeyPressed/JI_AnyKey
|
||||
// Temps restant en mil·lisegons durant el qual Ji::keyPressed/Ji::anyKey
|
||||
// retornen false. Utilitzat per a evitar que pulsacions fortuïtes
|
||||
// saltin cinemàtiques al començament.
|
||||
float wait_ms = 0.0f;
|
||||
float wait_ms = 0.0F;
|
||||
|
||||
// Per a calcular el delta entre crides a JI_Update sense que els callers
|
||||
// Per a calcular el delta entre crides a Ji::update sense que els callers
|
||||
// hagen de passar-lo explícitament. Es reinicia a la primera crida.
|
||||
Uint64 last_update_tick = 0;
|
||||
|
||||
bool input_blocked = false;
|
||||
|
||||
Uint8 virtual_keystates[JI_VSRC_COUNT][SDL_SCANCODE_COUNT] = {{0}};
|
||||
Uint8 virtual_keystates[static_cast<size_t>(Ji::VirtualSource::COUNT)][SDL_SCANCODE_COUNT] = {{0}};
|
||||
|
||||
Uint8 scancode_to_ascii(Uint8 scancode) {
|
||||
auto scancodeToAscii(Uint8 scancode) -> Uint8 {
|
||||
if (scancode >= SDL_SCANCODE_A && scancode <= SDL_SCANCODE_Z) {
|
||||
return static_cast<Uint8>('a' + (scancode - SDL_SCANCODE_A));
|
||||
}
|
||||
@@ -38,75 +39,93 @@ namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
void JI_DisableKeyboard(Uint32 time) {
|
||||
void Ji::disableKeyboard(Uint32 time) {
|
||||
wait_ms = static_cast<float>(time);
|
||||
}
|
||||
|
||||
void JI_SetInputBlocked(bool blocked) {
|
||||
void Ji::setInputBlocked(bool blocked) {
|
||||
input_blocked = blocked;
|
||||
}
|
||||
|
||||
void JI_SetVirtualKey(int scancode, int source, bool pressed) {
|
||||
if (scancode < 0 || scancode >= SDL_SCANCODE_COUNT) return;
|
||||
if (source < 0 || source >= JI_VSRC_COUNT) return;
|
||||
virtual_keystates[source][scancode] = pressed ? 1 : 0;
|
||||
void Ji::setVirtualKey(int scancode, VirtualSource source, bool pressed) {
|
||||
if (scancode < 0 || scancode >= SDL_SCANCODE_COUNT) {
|
||||
return;
|
||||
}
|
||||
const auto SRC_IDX = static_cast<size_t>(source);
|
||||
if (SRC_IDX >= static_cast<size_t>(VirtualSource::COUNT)) {
|
||||
return;
|
||||
}
|
||||
virtual_keystates[SRC_IDX][scancode] = pressed ? 1 : 0;
|
||||
}
|
||||
|
||||
void JI_moveCheats(Uint8 scancode) {
|
||||
void Ji::moveCheats(Uint8 scancode) {
|
||||
cheat[0] = cheat[1];
|
||||
cheat[1] = cheat[2];
|
||||
cheat[2] = cheat[3];
|
||||
cheat[3] = cheat[4];
|
||||
cheat[4] = scancode_to_ascii(scancode);
|
||||
cheat[4] = scancodeToAscii(scancode);
|
||||
}
|
||||
|
||||
void JI_Update() {
|
||||
void Ji::update() {
|
||||
// El director ha processat tots els events. Ací només refresquem
|
||||
// el snapshot del teclat i consumim el flag de tecla polsada.
|
||||
if (keystates == nullptr) {
|
||||
keystates = SDL_GetKeyboardState(NULL);
|
||||
keystates = SDL_GetKeyboardState(nullptr);
|
||||
}
|
||||
|
||||
const Uint64 now = SDL_GetTicks();
|
||||
if (last_update_tick == 0) last_update_tick = now;
|
||||
const float delta_ms = static_cast<float>(now - last_update_tick);
|
||||
last_update_tick = now;
|
||||
const Uint64 NOW = SDL_GetTicks();
|
||||
if (last_update_tick == 0) {
|
||||
last_update_tick = NOW;
|
||||
}
|
||||
const auto DELTA_MS = static_cast<float>(NOW - last_update_tick);
|
||||
last_update_tick = NOW;
|
||||
|
||||
if (wait_ms > 0.0f) {
|
||||
wait_ms -= delta_ms;
|
||||
if (wait_ms < 0.0f) wait_ms = 0.0f;
|
||||
if (wait_ms > 0.0F) {
|
||||
wait_ms -= DELTA_MS;
|
||||
wait_ms = std::max(wait_ms, 0.0F);
|
||||
}
|
||||
|
||||
// Consumim el flag de "alguna tecla no-GUI polsada" del director
|
||||
key_pressed = Director::get()->consumeKeyPressed();
|
||||
}
|
||||
|
||||
bool JI_KeyPressed(int key) {
|
||||
if (wait_ms > 0.0f || keystates == nullptr) return false;
|
||||
// Input bloquejat (p.ex. menú flotant obert)
|
||||
if (input_blocked) return false;
|
||||
// ESC bloquejada pel Director (primera pulsació mostra notificació)
|
||||
if (key == SDL_SCANCODE_ESCAPE && Director::get()->isEscBlocked()) return false;
|
||||
if (key < 0 || key >= SDL_SCANCODE_COUNT) return false;
|
||||
if (keystates[key] != 0) return true;
|
||||
for (int src = 0; src < JI_VSRC_COUNT; src++) {
|
||||
if (virtual_keystates[src][key] != 0) return true;
|
||||
auto Ji::keyPressed(int key) -> bool {
|
||||
if (wait_ms > 0.0F || keystates == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
// Input bloquejat (p.ex. menú flotant obert)
|
||||
if (input_blocked) {
|
||||
return false;
|
||||
}
|
||||
// ESC bloquejada pel Director (primera pulsació mostra notificació)
|
||||
if (key == SDL_SCANCODE_ESCAPE && Director::get()->isEscBlocked()) {
|
||||
return false;
|
||||
}
|
||||
if (key < 0 || key >= SDL_SCANCODE_COUNT) {
|
||||
return false;
|
||||
}
|
||||
if (static_cast<int>(keystates[key]) != 0) {
|
||||
return true;
|
||||
}
|
||||
return std::ranges::any_of(virtual_keystates, [key](const auto& vk) { return vk[key] != 0; });
|
||||
}
|
||||
|
||||
bool JI_CheatActivated(const char* cheat_code) {
|
||||
const size_t len = std::strlen(cheat_code);
|
||||
if (len > sizeof(cheat)) return false;
|
||||
auto Ji::cheatActivated(const char* cheat_code) -> bool {
|
||||
const size_t LEN = std::strlen(cheat_code);
|
||||
if (LEN > sizeof(cheat)) {
|
||||
return false;
|
||||
}
|
||||
// Compara contra els últims `len` caràcters del buffer. El buffer té
|
||||
// mida fixa 5 i acumula sempre el darrer tecle a la posició 4.
|
||||
const size_t offset = sizeof(cheat) - len;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (cheat[offset + i] != static_cast<Uint8>(cheat_code[i])) return false;
|
||||
const size_t OFFSET = sizeof(cheat) - LEN;
|
||||
for (size_t i = 0; i < LEN; i++) {
|
||||
if (cheat[OFFSET + i] != static_cast<Uint8>(cheat_code[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool JI_AnyKey() {
|
||||
return wait_ms > 0.0f ? false : key_pressed;
|
||||
auto Ji::anyKey() -> bool {
|
||||
return wait_ms > 0.0F ? false : key_pressed;
|
||||
}
|
||||
|
||||
+36
-25
@@ -1,25 +1,36 @@
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
void JI_DisableKeyboard(Uint32 time);
|
||||
|
||||
// Bloqueja tot l'input cap al joc (JI_KeyPressed retorna false per a tot)
|
||||
void JI_SetInputBlocked(bool blocked);
|
||||
|
||||
// Estableix l'estat d'una tecla virtual. Múltiples fonts (gamepad, remap)
|
||||
// s'agrupen per OR. JI_KeyPressed retorna true si el teclat real O qualsevol
|
||||
// font virtual està premuda.
|
||||
enum JI_VirtualSource {
|
||||
JI_VSRC_GAMEPAD = 0,
|
||||
JI_VSRC_REMAP = 1,
|
||||
JI_VSRC_COUNT
|
||||
};
|
||||
void JI_SetVirtualKey(int scancode, int source, bool pressed);
|
||||
|
||||
void JI_Update();
|
||||
|
||||
bool JI_KeyPressed(int key);
|
||||
|
||||
bool JI_CheatActivated(const char* cheat_code);
|
||||
|
||||
bool JI_AnyKey();
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Ji {
|
||||
|
||||
void disableKeyboard(Uint32 time);
|
||||
|
||||
// Bloqueja tot l'input cap al joc (Ji::keyPressed retorna false per a tot)
|
||||
void setInputBlocked(bool blocked);
|
||||
|
||||
// Estableix l'estat d'una tecla virtual. Múltiples fonts (gamepad, remap)
|
||||
// s'agrupen per OR. Ji::keyPressed retorna true si el teclat real O qualsevol
|
||||
// font virtual està premuda.
|
||||
enum class VirtualSource : std::uint8_t {
|
||||
GAMEPAD = 0,
|
||||
REMAP = 1,
|
||||
COUNT = 2
|
||||
};
|
||||
void setVirtualKey(int scancode, VirtualSource source, bool pressed);
|
||||
|
||||
void update();
|
||||
|
||||
// Avança el buffer rotatori de cheats afegint `scancode` per detectar
|
||||
// seqüències com "reviu", "alone", "obert". Usat pel Director quan rep
|
||||
// un KEY_DOWN; el joc no l'ha de cridar directament.
|
||||
void moveCheats(Uint8 scancode);
|
||||
|
||||
auto keyPressed(int key) -> bool;
|
||||
|
||||
auto cheatActivated(const char* cheat_code) -> bool;
|
||||
|
||||
auto anyKey() -> bool;
|
||||
|
||||
} // namespace Ji
|
||||
|
||||
@@ -9,24 +9,30 @@
|
||||
|
||||
namespace Locale {
|
||||
|
||||
static std::unordered_map<std::string, std::string> strings_;
|
||||
static std::unordered_map<std::string, std::string> strings_table;
|
||||
|
||||
// Aplana un node YAML en claus amb notació punt
|
||||
static void traverse(const fkyaml::node& node, const std::string& prefix) {
|
||||
if (node.is_mapping()) {
|
||||
for (auto it = node.begin(); it != node.end(); ++it) {
|
||||
std::string key = it.key().get_value<std::string>();
|
||||
std::string full = prefix.empty() ? key : prefix + "." + key;
|
||||
auto key = it.key().get_value<std::string>();
|
||||
std::string full = prefix;
|
||||
if (!full.empty()) {
|
||||
full += ".";
|
||||
}
|
||||
full += key;
|
||||
traverse(it.value(), full);
|
||||
}
|
||||
} else if (node.is_scalar()) {
|
||||
try {
|
||||
strings_[prefix] = node.get_value<std::string>();
|
||||
} catch (...) {}
|
||||
strings_table[prefix] = node.get_value<std::string>();
|
||||
} catch (...) {
|
||||
// @INTENTIONAL: si el valor no és string vàlid, l'ignorem i continuem.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool load(const char* filename) {
|
||||
auto load(const char* filename) -> bool {
|
||||
auto buffer = ResourceHelper::loadFile(filename);
|
||||
if (buffer.empty()) {
|
||||
std::cerr << "Locale: unable to load " << filename << '\n';
|
||||
@@ -36,9 +42,9 @@ namespace Locale {
|
||||
|
||||
try {
|
||||
auto yaml = fkyaml::node::deserialize(content);
|
||||
strings_.clear();
|
||||
strings_table.clear();
|
||||
traverse(yaml, "");
|
||||
std::cout << "Locale loaded: " << strings_.size() << " string(s) from " << filename << '\n';
|
||||
std::cout << "Locale loaded: " << strings_table.size() << " string(s) from " << filename << '\n';
|
||||
return true;
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "Locale: error parsing " << filename << ": " << e.what() << '\n';
|
||||
@@ -47,8 +53,10 @@ namespace Locale {
|
||||
}
|
||||
|
||||
auto get(const char* key) -> const char* {
|
||||
auto it = strings_.find(key);
|
||||
if (it != strings_.end()) return it->second.c_str();
|
||||
auto it = strings_table.find(key);
|
||||
if (it != strings_table.end()) {
|
||||
return it->second.c_str();
|
||||
}
|
||||
return key; // fallback: retorna la clau mateixa
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// Les claus són nested amb notació punt ("menu.items.zoom").
|
||||
// Si una clau no existeix, Locale::get torna la clau mateixa (útil per debug).
|
||||
namespace Locale {
|
||||
bool load(const char* filename);
|
||||
auto load(const char* filename) -> bool;
|
||||
|
||||
// Retorna la cadena associada a la clau. El punter és estable durant tota la
|
||||
// sessió (no canvia), per tant es pot guardar en const char*.
|
||||
|
||||
+361
-296
@@ -1,5 +1,6 @@
|
||||
#include "core/rendering/menu.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
@@ -48,21 +49,21 @@ namespace Menu {
|
||||
static constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada (~150 ms al 90%)
|
||||
|
||||
// --- Items ---
|
||||
enum class ItemKind { Toggle,
|
||||
Cycle,
|
||||
IntRange,
|
||||
Submenu,
|
||||
KeyBind,
|
||||
Action };
|
||||
enum class ItemKind : std::uint8_t { TOGGLE,
|
||||
CYCLE,
|
||||
INT_RANGE,
|
||||
SUBMENU,
|
||||
KEY_BIND,
|
||||
ACTION };
|
||||
|
||||
struct Item {
|
||||
const char* label;
|
||||
ItemKind kind;
|
||||
std::function<std::string()> getValue; // opcional
|
||||
std::function<void(int dir)> change; // per Toggle/Cycle/IntRange
|
||||
std::function<void()> enter; // per Submenu i Action
|
||||
SDL_Scancode* scancode{nullptr}; // per KeyBind
|
||||
std::function<bool()> visible; // nullptr ⇒ sempre visible
|
||||
std::function<std::string()> get_value; // opcional
|
||||
std::function<void(int dir)> change; // per TOGGLE/CYCLE/INT_RANGE
|
||||
std::function<void()> enter; // per SUBMENU i ACTION
|
||||
SDL_Scancode* scancode{nullptr}; // per KEY_BIND
|
||||
std::function<bool()> visible{nullptr}; // nullptr ⇒ sempre visible
|
||||
};
|
||||
|
||||
struct Page {
|
||||
@@ -72,96 +73,101 @@ namespace Menu {
|
||||
std::string subtitle; // opcional — si no buit, es dibuixa sota el títol
|
||||
};
|
||||
|
||||
static bool isVisible(const Item& it) { return !it.visible || it.visible(); }
|
||||
static auto isVisible(const Item& it) -> bool { return !it.visible || it.visible(); }
|
||||
|
||||
// Troba el pròxim ítem visible en direcció `dir` (±1) a partir de `from`.
|
||||
// Si cap és visible retorna `from`.
|
||||
static int nextVisibleCursor(const Page& p, int from, int dir) {
|
||||
const int n = static_cast<int>(p.items.size());
|
||||
if (n <= 0) return from;
|
||||
for (int i = 1; i <= n; ++i) {
|
||||
int idx = ((from + dir * i) % n + n) % n;
|
||||
if (isVisible(p.items[idx])) return idx;
|
||||
static auto nextVisibleCursor(const Page& p, int from, int dir) -> int {
|
||||
const int N = static_cast<int>(p.items.size());
|
||||
if (N <= 0) {
|
||||
return from;
|
||||
}
|
||||
for (int i = 1; i <= N; ++i) {
|
||||
int idx = ((from + dir * i) % N + N) % N;
|
||||
if (isVisible(p.items[idx])) {
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
return from;
|
||||
}
|
||||
|
||||
// --- Estat ---
|
||||
static std::vector<Page> stack_;
|
||||
static std::unique_ptr<Text> font_;
|
||||
static float open_anim_{0.0F}; // 0 = tancat, 1 = obert
|
||||
static float animated_h_{0.0F}; // alçada actual animada (smoothing cap al target visible)
|
||||
static Uint32 last_ticks_{0};
|
||||
static SDL_Scancode* capturing_{nullptr}; // != null → esperant tecla per assignar
|
||||
static bool closing_{false}; // true mentre l'animació de tancament és en curs
|
||||
static std::vector<Page> stack;
|
||||
static std::unique_ptr<Text> font;
|
||||
static float open_anim{0.0F}; // 0 = tancat, 1 = obert
|
||||
static float animated_h{0.0F}; // alçada actual animada (smoothing cap al target visible)
|
||||
static Uint32 last_ticks{0};
|
||||
static SDL_Scancode* capturing{nullptr}; // != null → esperant tecla per assignar
|
||||
static bool closing{false}; // true mentre l'animació de tancament és en curs
|
||||
|
||||
// --- Transició entre pàgines ---
|
||||
static constexpr float TRANSITION_SPEED = 5.5F; // ~180 ms
|
||||
static Page transition_outgoing_{"", {}, 0};
|
||||
static bool transition_active_{false};
|
||||
static float transition_progress_{1.0F};
|
||||
static int transition_dir_{+1}; // +1 endavant, -1 enrere
|
||||
static Page transition_outgoing{.title = "", .items = {}, .cursor = 0, .subtitle = ""};
|
||||
static bool transition_active{false};
|
||||
static float transition_progress{1.0F};
|
||||
static int transition_dir{+1}; // +1 endavant, -1 enrere
|
||||
|
||||
// Helpers per triggerar transicions
|
||||
static void pushPage(Page newPage) {
|
||||
transition_outgoing_ = stack_.back();
|
||||
stack_.push_back(std::move(newPage));
|
||||
transition_active_ = true;
|
||||
transition_progress_ = 0.0F;
|
||||
transition_dir_ = +1;
|
||||
static void pushPage(Page new_page) {
|
||||
transition_outgoing = stack.back();
|
||||
stack.push_back(std::move(new_page));
|
||||
transition_active = true;
|
||||
transition_progress = 0.0F;
|
||||
transition_dir = +1;
|
||||
}
|
||||
static void popPage() {
|
||||
transition_outgoing_ = stack_.back();
|
||||
stack_.pop_back();
|
||||
transition_active_ = true;
|
||||
transition_progress_ = 0.0F;
|
||||
transition_dir_ = -1;
|
||||
transition_outgoing = stack.back();
|
||||
stack.pop_back();
|
||||
transition_active = true;
|
||||
transition_progress = 0.0F;
|
||||
transition_dir = -1;
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
static std::string yesNo(bool b) { return b ? Locale::get("menu.values.yes") : Locale::get("menu.values.no"); }
|
||||
static std::string onOff(bool b) { return b ? Locale::get("menu.values.on") : Locale::get("menu.values.off"); }
|
||||
static auto yesNo(bool b) -> std::string { return b ? Locale::get("menu.values.yes") : Locale::get("menu.values.no"); }
|
||||
static auto onOff(bool b) -> std::string { return b ? Locale::get("menu.values.on") : Locale::get("menu.values.off"); }
|
||||
|
||||
// --- Builders de pàgines ---
|
||||
|
||||
static Page buildVideo();
|
||||
static Page buildAudio();
|
||||
static Page buildControls();
|
||||
static Page buildGame();
|
||||
static Page buildSystem();
|
||||
static auto buildVideo() -> Page;
|
||||
static auto buildAudio() -> Page;
|
||||
static auto buildControls() -> Page;
|
||||
static auto buildGame() -> Page;
|
||||
static auto buildSystem() -> Page;
|
||||
|
||||
static Page buildRoot() {
|
||||
Page p{Locale::get("menu.titles.root"), {}, 0};
|
||||
p.items.push_back({Locale::get("menu.items.video"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildVideo()); }, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.audio"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildAudio()); }, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.controls"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.game"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildGame()); }, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.system"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildSystem()); }, nullptr});
|
||||
static auto buildRoot() -> Page {
|
||||
Page p{.title = Locale::get("menu.titles.root"), .items = {}, .cursor = 0, .subtitle = ""};
|
||||
p.items.push_back({Locale::get("menu.items.video"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildVideo()); }, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.audio"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildAudio()); }, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.controls"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.game"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildGame()); }, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.system"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildSystem()); }, nullptr});
|
||||
return p;
|
||||
}
|
||||
|
||||
static Page buildVideo() {
|
||||
Page p{Locale::get("menu.titles.video"), {}, 0};
|
||||
static auto buildVideo() -> Page {
|
||||
Page p{.title = Locale::get("menu.titles.video"), .items = {}, .cursor = 0, .subtitle = ""};
|
||||
|
||||
// Zoom i fullscreen: sense sentit a WASM (el navegador posseix el canvas)
|
||||
#ifndef __EMSCRIPTEN__
|
||||
p.items.push_back({Locale::get("menu.items.zoom"), ItemKind::IntRange, [] {
|
||||
p.items.push_back({Locale::get("menu.items.zoom"), ItemKind::INT_RANGE, [] {
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom());
|
||||
return std::string(buf); }, [](int dir) {
|
||||
if (dir < 0) Screen::get()->decZoom();
|
||||
else if (dir > 0) Screen::get()->incZoom(); }, nullptr, nullptr});
|
||||
if (dir < 0) { Screen::get()->decZoom();
|
||||
} else if (dir > 0) { Screen::get()->incZoom();
|
||||
} }, nullptr, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.screen"), ItemKind::Toggle, [] { return std::string(Screen::get()->isFullscreen() ? Locale::get("menu.values.fullscreen") : Locale::get("menu.values.windowed")); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr, nullptr, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.screen"), ItemKind::TOGGLE, [] { return std::string(Screen::get()->isFullscreen() ? Locale::get("menu.values.fullscreen") : Locale::get("menu.values.windowed")); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr, nullptr, nullptr});
|
||||
#endif
|
||||
|
||||
// Opcions visuals generals (sempre visibles)
|
||||
p.items.push_back({Locale::get("menu.items.aspect_4_3"), ItemKind::Toggle, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr, nullptr, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.aspect_4_3"), ItemKind::TOGGLE, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr, nullptr, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.vsync"), ItemKind::Toggle, [] { return onOff(Options::video.vsync); }, [](int) { Screen::get()->toggleVSync(); }, nullptr, nullptr, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.vsync"), ItemKind::TOGGLE, [] { return onOff(Options::video.vsync); }, [](int) { Screen::get()->toggleVSync(); }, nullptr, nullptr, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.scaling_mode"), ItemKind::Cycle, [] {
|
||||
p.items.push_back({Locale::get("menu.items.scaling_mode"), ItemKind::CYCLE, [] {
|
||||
switch (Options::video.scaling_mode) {
|
||||
case Options::ScalingMode::DISABLED: return std::string(Locale::get("menu.values.scaling_disabled"));
|
||||
case Options::ScalingMode::STRETCH: return std::string(Locale::get("menu.values.scaling_stretch"));
|
||||
@@ -171,35 +177,33 @@ namespace Menu {
|
||||
}
|
||||
return std::string(Locale::get("menu.values.scaling_integer")); }, [](int dir) { Screen::get()->cycleScalingMode(dir); }, nullptr, nullptr, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.texture_filter"), ItemKind::Cycle, [] { return std::string(Options::video.texture_filter == Options::TextureFilter::LINEAR
|
||||
p.items.push_back({Locale::get("menu.items.texture_filter"), ItemKind::CYCLE, [] { return std::string(Options::video.texture_filter == Options::TextureFilter::LINEAR
|
||||
? Locale::get("menu.values.linear")
|
||||
: Locale::get("menu.values.nearest")); }, [](int dir) { Screen::get()->cycleTextureFilter(dir); }, nullptr, nullptr, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.internal_resolution"), ItemKind::IntRange, [] {
|
||||
p.items.push_back({Locale::get("menu.items.internal_resolution"), ItemKind::INT_RANGE, [] {
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof(buf), "%dX", Options::video.internal_resolution);
|
||||
return std::string(buf); }, [](int dir) { Screen::get()->changeInternalResolution(dir); }, nullptr, nullptr, nullptr});
|
||||
|
||||
// Bloc shaders: no disponible a WASM (NO_SHADERS, sense SDL3 GPU a WebGL2)
|
||||
#ifndef __EMSCRIPTEN__
|
||||
p.items.push_back({Locale::get("menu.items.shader"), ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr, nullptr, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.shader"), ItemKind::TOGGLE, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr, nullptr, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.shader_type"), ItemKind::Cycle, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) {
|
||||
if (dir < 0) Screen::get()->prevShaderType();
|
||||
else Screen::get()->nextShaderType(); }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});
|
||||
p.items.push_back({Locale::get("menu.items.shader_type"), ItemKind::CYCLE, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) {
|
||||
if (dir < 0) { Screen::get()->prevShaderType();
|
||||
} else { Screen::get()->nextShaderType();
|
||||
} }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.preset"), ItemKind::Cycle, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) {
|
||||
if (dir < 0) Screen::get()->prevPreset();
|
||||
else Screen::get()->nextPreset(); }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});
|
||||
p.items.push_back({Locale::get("menu.items.preset"), ItemKind::CYCLE, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) {
|
||||
if (dir < 0) { Screen::get()->prevPreset();
|
||||
} else { Screen::get()->nextPreset();
|
||||
} }, nullptr, nullptr, [] { return Options::video.shader_enabled; }});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.supersampling"), ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr, nullptr, [] {
|
||||
if (!Options::video.shader_enabled) return false;
|
||||
const char* name = Screen::get()->getActiveShaderName();
|
||||
return name && std::string(name) == "POSTFX"; }});
|
||||
#endif
|
||||
|
||||
// Informació de render
|
||||
p.items.push_back({Locale::get("menu.items.render_info"), ItemKind::Cycle, [] {
|
||||
p.items.push_back({Locale::get("menu.items.render_info"), ItemKind::CYCLE, [] {
|
||||
switch (Options::render_info.position) {
|
||||
case Options::RenderInfoPosition::OFF: return std::string(Locale::get("menu.values.off"));
|
||||
case Options::RenderInfoPosition::TOP: return std::string(Locale::get("menu.values.top"));
|
||||
@@ -207,88 +211,92 @@ namespace Menu {
|
||||
}
|
||||
return std::string(Locale::get("menu.values.off")); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr, nullptr, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.uptime"), ItemKind::Toggle, [] { return onOff(Options::render_info.show_time); }, [](int) { Options::render_info.show_time = !Options::render_info.show_time; }, nullptr, nullptr, [] { return Options::render_info.position != Options::RenderInfoPosition::OFF; }});
|
||||
p.items.push_back({Locale::get("menu.items.uptime"), ItemKind::TOGGLE, [] { return onOff(Options::render_info.show_time); }, [](int) { Options::render_info.show_time = !Options::render_info.show_time; }, nullptr, nullptr, [] { return Options::render_info.position != Options::RenderInfoPosition::OFF; }});
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
// Converteix volum 0..1 a percentatge i ho formata com "50%"
|
||||
static std::string volPct(float v) {
|
||||
int pct = static_cast<int>(v * 100.0F + 0.5F);
|
||||
if (pct < 0) pct = 0;
|
||||
if (pct > 100) pct = 100;
|
||||
static auto volPct(float v) -> std::string {
|
||||
int pct = static_cast<int>(std::lround(v * 100.0F));
|
||||
pct = std::max(pct, 0);
|
||||
pct = std::min(pct, 100);
|
||||
char buf[8];
|
||||
std::snprintf(buf, sizeof(buf), "%d%%", pct);
|
||||
return std::string(buf);
|
||||
return {buf};
|
||||
}
|
||||
|
||||
// Canvi +/- d'un volum en steps de 0.05 (5%) amb clamping
|
||||
static void stepVolume(float& v, int dir) {
|
||||
v += (dir >= 0 ? 0.05F : -0.05F);
|
||||
if (v < 0.0F) v = 0.0F;
|
||||
if (v > 1.0F) v = 1.0F;
|
||||
v = std::max(v, 0.0F);
|
||||
v = std::min(v, 1.0F);
|
||||
Options::applyAudio();
|
||||
}
|
||||
|
||||
static Page buildControls() {
|
||||
Page p{Locale::get("menu.titles.controls"), {}, 0};
|
||||
p.items.push_back({Locale::get("menu.items.move_up"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.up});
|
||||
p.items.push_back({Locale::get("menu.items.move_down"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.down});
|
||||
p.items.push_back({Locale::get("menu.items.move_left"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.left});
|
||||
p.items.push_back({Locale::get("menu.items.move_right"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.right});
|
||||
p.items.push_back({Locale::get("menu.items.menu_key"), ItemKind::KeyBind, nullptr, nullptr, nullptr, KeyConfig::scancodePtr("menu_toggle")});
|
||||
static auto buildControls() -> Page {
|
||||
Page p{.title = Locale::get("menu.titles.controls"), .items = {}, .cursor = 0, .subtitle = ""};
|
||||
p.items.push_back({Locale::get("menu.items.move_up"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.up});
|
||||
p.items.push_back({Locale::get("menu.items.move_down"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.down});
|
||||
p.items.push_back({Locale::get("menu.items.move_left"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.left});
|
||||
p.items.push_back({Locale::get("menu.items.move_right"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.right});
|
||||
p.items.push_back({Locale::get("menu.items.menu_key"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, KeyConfig::scancodePtr("menu_toggle")});
|
||||
return p;
|
||||
}
|
||||
|
||||
static Page buildAudio() {
|
||||
Page p{Locale::get("menu.titles.audio"), {}, 0};
|
||||
static auto buildAudio() -> Page {
|
||||
Page p{.title = Locale::get("menu.titles.audio"), .items = {}, .cursor = 0, .subtitle = ""};
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.master_enable"), ItemKind::Toggle, [] { return onOff(Options::audio.enabled); }, [](int) {
|
||||
p.items.push_back({Locale::get("menu.items.master_enable"), ItemKind::TOGGLE, [] { return onOff(Options::audio.enabled); }, [](int) {
|
||||
Options::audio.enabled = !Options::audio.enabled;
|
||||
Options::applyAudio(); }, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.master_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.volume); }, [](int dir) { stepVolume(Options::audio.volume, dir); }, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.master_volume"), ItemKind::INT_RANGE, [] { return volPct(Options::audio.volume); }, [](int dir) { stepVolume(Options::audio.volume, dir); }, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.music"), ItemKind::Toggle, [] { return onOff(Options::audio.music.enabled); }, [](int) {
|
||||
p.items.push_back({Locale::get("menu.items.music"), ItemKind::TOGGLE, [] { return onOff(Options::audio.music.enabled); }, [](int) {
|
||||
Options::audio.music.enabled = !Options::audio.music.enabled;
|
||||
Options::applyAudio(); }, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.music_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.music.volume); }, [](int dir) { stepVolume(Options::audio.music.volume, dir); }, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.music_volume"), ItemKind::INT_RANGE, [] { return volPct(Options::audio.music.volume); }, [](int dir) { stepVolume(Options::audio.music.volume, dir); }, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.sounds"), ItemKind::Toggle, [] { return onOff(Options::audio.sound.enabled); }, [](int) {
|
||||
p.items.push_back({Locale::get("menu.items.sounds"), ItemKind::TOGGLE, [] { return onOff(Options::audio.sound.enabled); }, [](int) {
|
||||
Options::audio.sound.enabled = !Options::audio.sound.enabled;
|
||||
Options::applyAudio(); }, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.sounds_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.sound.volume); }, [](int dir) { stepVolume(Options::audio.sound.volume, dir); }, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.sounds_volume"), ItemKind::INT_RANGE, [] { return volPct(Options::audio.sound.volume); }, [](int dir) { stepVolume(Options::audio.sound.volume, dir); }, nullptr});
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
static Page buildGame() {
|
||||
Page p{Locale::get("menu.titles.game"), {}, 0};
|
||||
static auto buildGame() -> Page {
|
||||
Page p{.title = Locale::get("menu.titles.game"), .items = {}, .cursor = 0, .subtitle = ""};
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.use_new_logo"), ItemKind::Toggle, [] { return yesNo(Options::game.use_new_logo); }, [](int) { Options::game.use_new_logo = !Options::game.use_new_logo; }, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.use_new_logo"), ItemKind::TOGGLE, [] { return yesNo(Options::game.use_new_logo); }, [](int) { Options::game.use_new_logo = !Options::game.use_new_logo; }, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.show_title_credits"), ItemKind::Toggle, [] { return yesNo(Options::game.show_title_credits); }, [](int) { Options::game.show_title_credits = !Options::game.show_title_credits; }, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.show_title_credits"), ItemKind::TOGGLE, [] { return yesNo(Options::game.show_title_credits); }, [](int) { Options::game.show_title_credits = !Options::game.show_title_credits; }, nullptr});
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.show_preload"), ItemKind::Toggle, [] { return yesNo(Options::game.show_preload); }, [](int) { Options::game.show_preload = !Options::game.show_preload; }, nullptr});
|
||||
p.items.push_back({Locale::get("menu.items.show_preload"), ItemKind::TOGGLE, [] { return yesNo(Options::game.show_preload); }, [](int) { Options::game.show_preload = !Options::game.show_preload; }, nullptr});
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
static Page buildSystem() {
|
||||
Page p{Locale::get("menu.titles.system"), {}, 0};
|
||||
static auto buildSystem() -> Page {
|
||||
Page p{.title = Locale::get("menu.titles.system"), .items = {}, .cursor = 0, .subtitle = ""};
|
||||
p.subtitle = std::string("v") + Texts::VERSION + " (" + Version::GIT_HASH + ")";
|
||||
|
||||
p.items.push_back({Locale::get("menu.items.restart"), ItemKind::Action, nullptr, nullptr, [] {
|
||||
if (Director::get()) Director::get()->requestRestart();
|
||||
p.items.push_back({Locale::get("menu.items.restart"), ItemKind::ACTION, nullptr, nullptr, [] {
|
||||
if (Director::get()) {
|
||||
Director::get()->requestRestart();
|
||||
}
|
||||
},
|
||||
nullptr,
|
||||
nullptr});
|
||||
|
||||
#ifndef __EMSCRIPTEN__
|
||||
p.items.push_back({Locale::get("menu.items.exit_game"), ItemKind::Action, nullptr, nullptr, [] {
|
||||
if (Director::get()) Director::get()->requestQuit();
|
||||
p.items.push_back({Locale::get("menu.items.exit_game"), ItemKind::ACTION, nullptr, nullptr, [] {
|
||||
if (Director::get()) {
|
||||
Director::get()->requestQuit();
|
||||
}
|
||||
},
|
||||
nullptr,
|
||||
nullptr});
|
||||
@@ -301,34 +309,42 @@ namespace Menu {
|
||||
|
||||
// Alpha blending per pixel sobre el buffer ARGB (ABGR en memòria)
|
||||
static void blendRect(Uint32* buf, int x, int y, int w, int h, Uint32 src_argb, Uint8 src_alpha) {
|
||||
const Uint8 sa = src_alpha;
|
||||
const Uint8 sr = src_argb & 0xFF;
|
||||
const Uint8 sg = (src_argb >> 8) & 0xFF;
|
||||
const Uint8 sb = (src_argb >> 16) & 0xFF;
|
||||
const Uint8 inv = 255 - sa;
|
||||
const Uint8 SA = src_alpha;
|
||||
const Uint8 SR = src_argb & 0xFF;
|
||||
const Uint8 SG = (src_argb >> 8) & 0xFF;
|
||||
const Uint8 SB = (src_argb >> 16) & 0xFF;
|
||||
const Uint8 INV = 255 - SA;
|
||||
for (int row = y; row < y + h; row++) {
|
||||
if (row < 0 || row >= SCREEN_H) continue;
|
||||
if (row < 0 || row >= SCREEN_H) {
|
||||
continue;
|
||||
}
|
||||
for (int col = x; col < x + w; col++) {
|
||||
if (col < 0 || col >= SCREEN_W) continue;
|
||||
Uint32* p = &buf[col + row * SCREEN_W];
|
||||
if (col < 0 || col >= SCREEN_W) {
|
||||
continue;
|
||||
}
|
||||
Uint32* p = &buf[col + (row * SCREEN_W)];
|
||||
Uint32 dst = *p;
|
||||
Uint8 dr = dst & 0xFF;
|
||||
Uint8 dg = (dst >> 8) & 0xFF;
|
||||
Uint8 db = (dst >> 16) & 0xFF;
|
||||
Uint8 r = (sr * sa + dr * inv) / 255;
|
||||
Uint8 g = (sg * sa + dg * inv) / 255;
|
||||
Uint8 b = (sb * sa + db * inv) / 255;
|
||||
*p = 0xFF000000u | (static_cast<Uint32>(b) << 16) | (static_cast<Uint32>(g) << 8) | r;
|
||||
Uint8 r = (SR * SA + dr * INV) / 255;
|
||||
Uint8 g = (SG * SA + dg * INV) / 255;
|
||||
Uint8 b = (SB * SA + db * INV) / 255;
|
||||
*p = 0xFF000000U | (static_cast<Uint32>(b) << 16) | (static_cast<Uint32>(g) << 8) | r;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void fillRect(Uint32* buf, int x, int y, int w, int h, Uint32 color) {
|
||||
for (int row = y; row < y + h; row++) {
|
||||
if (row < 0 || row >= SCREEN_H) continue;
|
||||
if (row < 0 || row >= SCREEN_H) {
|
||||
continue;
|
||||
}
|
||||
for (int col = x; col < x + w; col++) {
|
||||
if (col < 0 || col >= SCREEN_W) continue;
|
||||
buf[col + row * SCREEN_W] = color;
|
||||
if (col < 0 || col >= SCREEN_W) {
|
||||
continue;
|
||||
}
|
||||
buf[col + (row * SCREEN_W)] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,12 +359,9 @@ namespace Menu {
|
||||
// Mida final de la caixa segons el nombre d'items *visibles*.
|
||||
// body = (N-1) * ITEM_SPACING + charH — així BOTTOM_PAD és el buit real
|
||||
// sota el text del darrer ítem, no un buit extra per sobre d'un "slot" buit.
|
||||
static int boxHeight(const Page& page) {
|
||||
int n = 0;
|
||||
for (const auto& it : page.items) {
|
||||
if (isVisible(it)) ++n;
|
||||
}
|
||||
int body = (n == 0) ? 8 : (n - 1) * ITEM_SPACING + 8;
|
||||
static auto boxHeight(const Page& page) -> int {
|
||||
const int N = static_cast<int>(std::count_if(page.items.begin(), page.items.end(), [](const auto& it) { return isVisible(it); }));
|
||||
int body = (N == 0) ? 8 : ((N - 1) * ITEM_SPACING) + 8;
|
||||
int header = HEADER_H + (page.subtitle.empty() ? 0 : SUBTITLE_H);
|
||||
return header + body + BOTTOM_PAD;
|
||||
}
|
||||
@@ -356,94 +369,104 @@ namespace Menu {
|
||||
// --- API pública ---
|
||||
|
||||
void init() {
|
||||
font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
|
||||
stack_.clear();
|
||||
open_anim_ = 0.0F;
|
||||
closing_ = false;
|
||||
last_ticks_ = SDL_GetTicks();
|
||||
font = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
|
||||
stack.clear();
|
||||
open_anim = 0.0F;
|
||||
closing = false;
|
||||
last_ticks = SDL_GetTicks();
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
font_.reset();
|
||||
stack_.clear();
|
||||
closing_ = false;
|
||||
font.reset();
|
||||
stack.clear();
|
||||
closing = false;
|
||||
}
|
||||
|
||||
// "Actiu": accepta input. Durant l'animació de tancament la pila encara
|
||||
// té pàgines però ja no ha de processar tecles.
|
||||
auto isOpen() -> bool {
|
||||
return !stack_.empty() && !closing_;
|
||||
return !stack.empty() && !closing;
|
||||
}
|
||||
|
||||
// "Visible": encara hi ha caixa per pintar (incloent close animation).
|
||||
auto isVisible() -> bool {
|
||||
return !stack_.empty();
|
||||
return !stack.empty();
|
||||
}
|
||||
|
||||
void toggle() {
|
||||
if (closing_ && !stack_.empty()) {
|
||||
if (closing && !stack.empty()) {
|
||||
// Cancel·la el tancament en curs — continua l'animació cap a "obert"
|
||||
// des del valor actual d'open_anim_.
|
||||
closing_ = false;
|
||||
last_ticks_ = SDL_GetTicks();
|
||||
// des del valor actual d'open_anim.
|
||||
closing = false;
|
||||
last_ticks = SDL_GetTicks();
|
||||
return;
|
||||
}
|
||||
if (isOpen()) {
|
||||
close();
|
||||
} else {
|
||||
stack_.push_back(buildRoot());
|
||||
open_anim_ = 0.0F;
|
||||
closing_ = false;
|
||||
animated_h_ = static_cast<float>(boxHeight(stack_.back()));
|
||||
last_ticks_ = SDL_GetTicks();
|
||||
stack.push_back(buildRoot());
|
||||
open_anim = 0.0F;
|
||||
closing = false;
|
||||
animated_h = static_cast<float>(boxHeight(stack.back()));
|
||||
last_ticks = SDL_GetTicks();
|
||||
}
|
||||
}
|
||||
|
||||
// close() no buida la pila immediatament: marca closing_ i deixa que
|
||||
// render() faça decréixer open_anim_ fins a 0. En aquell moment es neteja
|
||||
// close() no buida la pila immediatament: marca closing i deixa que
|
||||
// render() faça decréixer open_anim fins a 0. En aquell moment es neteja
|
||||
// l'estat. Si es crida estant ja tancat o tancant-se, no-op.
|
||||
void close() {
|
||||
if (stack_.empty() || closing_) return;
|
||||
closing_ = true;
|
||||
capturing_ = nullptr;
|
||||
transition_active_ = false;
|
||||
transition_progress_ = 1.0F;
|
||||
last_ticks_ = SDL_GetTicks();
|
||||
if (stack.empty() || closing) {
|
||||
return;
|
||||
}
|
||||
closing = true;
|
||||
capturing = nullptr;
|
||||
transition_active = false;
|
||||
transition_progress = 1.0F;
|
||||
last_ticks = SDL_GetTicks();
|
||||
}
|
||||
|
||||
auto isCapturing() -> bool {
|
||||
return capturing_ != nullptr;
|
||||
return capturing != nullptr;
|
||||
}
|
||||
|
||||
void captureKey(SDL_Scancode sc) {
|
||||
if (!capturing_) return;
|
||||
if (capturing == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (sc == SDL_SCANCODE_ESCAPE) {
|
||||
// Cancel·la
|
||||
capturing_ = nullptr;
|
||||
capturing = nullptr;
|
||||
return;
|
||||
}
|
||||
*capturing_ = sc;
|
||||
capturing_ = nullptr;
|
||||
*capturing = sc;
|
||||
capturing = nullptr;
|
||||
}
|
||||
|
||||
void handleKey(SDL_Scancode sc) {
|
||||
if (!isOpen()) return;
|
||||
Page& page = stack_.back();
|
||||
if (page.items.empty()) {
|
||||
// Pàgina buida — només backspace surt
|
||||
if (sc == SDL_SCANCODE_BACKSPACE) {
|
||||
if (stack_.size() > 1)
|
||||
popPage();
|
||||
else
|
||||
close();
|
||||
static void backOrClose() {
|
||||
if (stack.size() > 1) {
|
||||
popPage();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
// Activació d'un ítem (RETURN/KP_ENTER): SUBMENU/ACTION criden enter,
|
||||
// KEY_BIND inicia captura, la resta avança change(+1).
|
||||
static void activateItem(Item& item) {
|
||||
if (item.kind == ItemKind::SUBMENU || item.kind == ItemKind::ACTION) {
|
||||
if (item.enter) {
|
||||
item.enter();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Si el cursor està sobre un ítem ocultat (p. ex. una acció anterior el va ocultar),
|
||||
// reubica'l al pròxim visible abans de processar l'entrada.
|
||||
if (!isVisible(page.items[page.cursor])) {
|
||||
page.cursor = nextVisibleCursor(page, page.cursor, +1);
|
||||
} else if (item.kind == ItemKind::KEY_BIND) {
|
||||
capturing = item.scancode;
|
||||
} else if (item.change) {
|
||||
item.change(+1);
|
||||
}
|
||||
}
|
||||
|
||||
static void applyKeyToItem(Page& page, SDL_Scancode sc) {
|
||||
Item& item = page.items[page.cursor];
|
||||
switch (sc) {
|
||||
case SDL_SCANCODE_UP:
|
||||
page.cursor = nextVisibleCursor(page, page.cursor, -1);
|
||||
@@ -452,59 +475,66 @@ namespace Menu {
|
||||
page.cursor = nextVisibleCursor(page, page.cursor, +1);
|
||||
break;
|
||||
case SDL_SCANCODE_LEFT:
|
||||
if (page.items[page.cursor].kind != ItemKind::Submenu &&
|
||||
page.items[page.cursor].change) {
|
||||
page.items[page.cursor].change(-1);
|
||||
if (item.kind != ItemKind::SUBMENU && item.change) {
|
||||
item.change(-1);
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_RIGHT:
|
||||
if (page.items[page.cursor].kind != ItemKind::Submenu &&
|
||||
page.items[page.cursor].change) {
|
||||
page.items[page.cursor].change(+1);
|
||||
if (item.kind != ItemKind::SUBMENU && item.change) {
|
||||
item.change(+1);
|
||||
}
|
||||
break;
|
||||
case SDL_SCANCODE_RETURN:
|
||||
case SDL_SCANCODE_KP_ENTER:
|
||||
if (page.items[page.cursor].kind == ItemKind::Submenu ||
|
||||
page.items[page.cursor].kind == ItemKind::Action) {
|
||||
if (page.items[page.cursor].enter) page.items[page.cursor].enter();
|
||||
} else if (page.items[page.cursor].kind == ItemKind::KeyBind) {
|
||||
capturing_ = page.items[page.cursor].scancode;
|
||||
} else if (page.items[page.cursor].change) {
|
||||
page.items[page.cursor].change(+1);
|
||||
}
|
||||
activateItem(item);
|
||||
break;
|
||||
case SDL_SCANCODE_BACKSPACE:
|
||||
if (stack_.size() > 1)
|
||||
popPage();
|
||||
else
|
||||
close();
|
||||
backOrClose();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Després de qualsevol acció, si el cursor quedara sobre un ítem ocult
|
||||
// (possible si una acció ha canviat la visibilitat pròpia de l'ítem actual,
|
||||
// edge case defensiu), salta al següent visible.
|
||||
if (!stack_.empty()) {
|
||||
Page& top = stack_.back();
|
||||
}
|
||||
|
||||
void handleKey(SDL_Scancode sc) {
|
||||
if (!isOpen()) {
|
||||
return;
|
||||
}
|
||||
Page& page = stack.back();
|
||||
if (page.items.empty()) {
|
||||
if (sc == SDL_SCANCODE_BACKSPACE) {
|
||||
backOrClose();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!isVisible(page.items[page.cursor])) {
|
||||
page.cursor = nextVisibleCursor(page, page.cursor, +1);
|
||||
}
|
||||
applyKeyToItem(page, sc);
|
||||
// Defensa: si una acció ha amagat l'ítem que tenim sota el cursor,
|
||||
// saltem al pròxim visible.
|
||||
if (!stack.empty()) {
|
||||
Page& top = stack.back();
|
||||
if (!top.items.empty() && !isVisible(top.items[top.cursor])) {
|
||||
top.cursor = nextVisibleCursor(top, top.cursor, +1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forward decl: renderOneItem viu sota renderPageContent però aquest l'ha de cridar.
|
||||
static void renderOneItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max);
|
||||
|
||||
// Dibuixa el contingut d'una pàgina amb un offset horitzontal i un rang de clip.
|
||||
// box_x/box_y són les coordenades de la caixa (per calcular posicions relatives);
|
||||
// clip_x_min/clip_x_max limiten on es dibuixa text i la línia separadora.
|
||||
static void renderPageContent(Uint32* pixel_data, const Page& page, int box_x, int box_y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
|
||||
// Títol
|
||||
int title_w = font_->width(page.title);
|
||||
int title_x = box_x + (BOX_W - title_w) / 2 + x_offset;
|
||||
font_->drawClipped(pixel_data, title_x, box_y + TITLE_PAD_Y, page.title, TITLE_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
int title_w = font->width(page.title);
|
||||
int title_x = box_x + ((BOX_W - title_w) / 2) + x_offset;
|
||||
font->drawClipped(pixel_data, title_x, box_y + TITLE_PAD_Y, page.title, TITLE_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
|
||||
// Línia sota el títol (també lliscada) — clippada manualment
|
||||
int title_line_y = box_y + TITLE_PAD_Y + font_->charHeight() + 2;
|
||||
int title_line_y = box_y + TITLE_PAD_Y + font->charHeight() + 2;
|
||||
if (title_line_y >= clip_y_min && title_line_y < clip_y_max) {
|
||||
int line_x = box_x + 4 + x_offset;
|
||||
int line_w = BOX_W - 8;
|
||||
@@ -518,151 +548,186 @@ namespace Menu {
|
||||
// Subtítol opcional (sota la línia del títol, abans dels items)
|
||||
int items_y = title_line_y + 4;
|
||||
if (!page.subtitle.empty()) {
|
||||
int sub_w = font_->width(page.subtitle.c_str());
|
||||
int sub_x = box_x + (BOX_W - sub_w) / 2 + x_offset;
|
||||
font_->drawClipped(pixel_data, sub_x, items_y, page.subtitle.c_str(), LABEL_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
int sub_w = font->width(page.subtitle.c_str());
|
||||
int sub_x = box_x + ((BOX_W - sub_w) / 2) + x_offset;
|
||||
font->drawClipped(pixel_data, sub_x, items_y, page.subtitle.c_str(), LABEL_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
items_y += SUBTITLE_H;
|
||||
}
|
||||
// Compta visibles — si cap, dibuixa placeholder (caixa totalment col·lapsada però oberta)
|
||||
int visible_count = 0;
|
||||
for (const auto& it : page.items)
|
||||
if (isVisible(it)) ++visible_count;
|
||||
if (visible_count == 0) {
|
||||
const int VISIBLE_COUNT = static_cast<int>(std::count_if(page.items.begin(), page.items.end(), [](const auto& it) { return isVisible(it); }));
|
||||
if (VISIBLE_COUNT == 0) {
|
||||
const char* empty_text = Locale::get("menu.values.empty");
|
||||
int ew = font_->width(empty_text);
|
||||
font_->drawClipped(pixel_data, box_x + (BOX_W - ew) / 2 + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
int ew = font->width(empty_text);
|
||||
font->drawClipped(pixel_data, box_x + ((BOX_W - ew) / 2) + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
return;
|
||||
}
|
||||
|
||||
int y_slot = 0; // índex de fila visible (independent de l'índex real de l'ítem)
|
||||
int y_slot = 0;
|
||||
for (size_t i = 0; i < page.items.size(); i++) {
|
||||
const Item& item = page.items[i];
|
||||
if (!isVisible(item)) continue;
|
||||
int y = items_y + y_slot * ITEM_SPACING;
|
||||
++y_slot;
|
||||
bool selected = (static_cast<int>(i) == page.cursor);
|
||||
Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR;
|
||||
|
||||
// Action: sense valor a la dreta — centrem el label amb el cursor just a l'esquerra.
|
||||
if (item.kind == ItemKind::Action) {
|
||||
int lw = font_->width(item.label);
|
||||
int lx = box_x + (BOX_W - lw) / 2 + x_offset;
|
||||
if (selected) {
|
||||
font_->drawClipped(pixel_data, lx - font_->width("> "), y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
font_->drawClipped(pixel_data, lx, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
if (!isVisible(item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (selected) {
|
||||
font_->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
|
||||
font_->drawClipped(pixel_data, box_x + ITEM_PAD_X + x_offset, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
|
||||
if (item.kind == ItemKind::Submenu) {
|
||||
const char* arrow = ">>";
|
||||
int aw = font_->width(arrow);
|
||||
Uint32 ac = selected ? CURSOR_COLOR : VALUE_COLOR;
|
||||
font_->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - aw + x_offset, y, arrow, ac, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
} else if (item.kind == ItemKind::KeyBind) {
|
||||
bool this_capturing = (capturing_ == item.scancode);
|
||||
const char* text = this_capturing ? Locale::get("menu.values.press_key") : (item.scancode ? SDL_GetScancodeName(*item.scancode) : "");
|
||||
if (!text || !*text) text = Locale::get("menu.values.unknown");
|
||||
int tw = font_->width(text);
|
||||
Uint32 tc = this_capturing ? 0xFF00FFFF : (selected ? CURSOR_COLOR : VALUE_COLOR);
|
||||
font_->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - tw + x_offset, y, text, tc, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
} else if (item.getValue) {
|
||||
std::string value = item.getValue();
|
||||
int value_w = font_->width(value.c_str());
|
||||
Uint32 value_color = selected ? CURSOR_COLOR : VALUE_COLOR;
|
||||
font_->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - value_w + x_offset, y, value.c_str(), value_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
const int Y = items_y + (y_slot * ITEM_SPACING);
|
||||
++y_slot;
|
||||
const bool SELECTED = (static_cast<int>(i) == page.cursor);
|
||||
renderOneItem(pixel_data, item, SELECTED, box_x, Y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
}
|
||||
|
||||
static auto keybindText(const Item& item, bool this_capturing) -> const char* {
|
||||
const char* text = nullptr;
|
||||
if (this_capturing) {
|
||||
text = Locale::get("menu.values.press_key");
|
||||
} else if (item.scancode != nullptr) {
|
||||
text = SDL_GetScancodeName(*item.scancode);
|
||||
} else {
|
||||
text = "";
|
||||
}
|
||||
if ((text == nullptr) || (*text == 0)) {
|
||||
text = Locale::get("menu.values.unknown");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
static void renderActionItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
|
||||
const Uint32 LABEL_COLOR_LOCAL = selected ? CURSOR_COLOR : LABEL_COLOR;
|
||||
const int LW = font->width(item.label);
|
||||
const int LX = box_x + ((BOX_W - LW) / 2) + x_offset;
|
||||
if (selected) {
|
||||
font->drawClipped(pixel_data, LX - font->width("> "), y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
font->drawClipped(pixel_data, LX, y, item.label, LABEL_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
|
||||
static auto keybindColor(bool this_capturing, bool selected) -> Uint32 {
|
||||
if (this_capturing) {
|
||||
return 0xFF00FFFF;
|
||||
}
|
||||
return selected ? CURSOR_COLOR : VALUE_COLOR;
|
||||
}
|
||||
|
||||
static void renderItemValue(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
|
||||
if (item.kind == ItemKind::SUBMENU) {
|
||||
const char* arrow = ">>";
|
||||
const int AW = font->width(arrow);
|
||||
const Uint32 AC = selected ? CURSOR_COLOR : VALUE_COLOR;
|
||||
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - AW + x_offset, y, arrow, AC, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
return;
|
||||
}
|
||||
if (item.kind == ItemKind::KEY_BIND) {
|
||||
const bool THIS_CAPTURING = (capturing == item.scancode);
|
||||
const char* text = keybindText(item, THIS_CAPTURING);
|
||||
const int TW = font->width(text);
|
||||
const Uint32 TC = keybindColor(THIS_CAPTURING, selected);
|
||||
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - TW + x_offset, y, text, TC, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
return;
|
||||
}
|
||||
if (item.get_value) {
|
||||
const std::string VALUE = item.get_value();
|
||||
const int VALUE_W = font->width(VALUE.c_str());
|
||||
const Uint32 VALUE_COLOR_LOCAL = selected ? CURSOR_COLOR : VALUE_COLOR;
|
||||
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - VALUE_W + x_offset, y, VALUE.c_str(), VALUE_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
}
|
||||
|
||||
static void renderOneItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
|
||||
if (item.kind == ItemKind::ACTION) {
|
||||
renderActionItem(pixel_data, item, selected, box_x, y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
return;
|
||||
}
|
||||
const Uint32 LABEL_COLOR_LOCAL = selected ? CURSOR_COLOR : LABEL_COLOR;
|
||||
if (selected) {
|
||||
font->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
font->drawClipped(pixel_data, box_x + ITEM_PAD_X + x_offset, y, item.label, LABEL_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
renderItemValue(pixel_data, item, selected, box_x, y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
}
|
||||
|
||||
void render(Uint32* pixel_data) {
|
||||
if (!isVisible() || !font_ || !pixel_data) return;
|
||||
if (!isVisible() || !font || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delta time
|
||||
Uint32 now = SDL_GetTicks();
|
||||
float dt = static_cast<float>(now - last_ticks_) / 1000.0F;
|
||||
last_ticks_ = now;
|
||||
if (closing_) {
|
||||
open_anim_ -= CLOSE_SPEED * dt;
|
||||
if (open_anim_ <= 0.0F) {
|
||||
float dt = static_cast<float>(now - last_ticks) / 1000.0F;
|
||||
last_ticks = now;
|
||||
if (closing) {
|
||||
open_anim -= CLOSE_SPEED * dt;
|
||||
if (open_anim <= 0.0F) {
|
||||
// Animació de tancament completada — buida l'estat de veritat.
|
||||
open_anim_ = 0.0F;
|
||||
stack_.clear();
|
||||
animated_h_ = 0.0F;
|
||||
closing_ = false;
|
||||
open_anim = 0.0F;
|
||||
stack.clear();
|
||||
animated_h = 0.0F;
|
||||
closing = false;
|
||||
return;
|
||||
}
|
||||
} else if (open_anim_ < 1.0F) {
|
||||
open_anim_ += OPEN_SPEED * dt;
|
||||
if (open_anim_ > 1.0F) open_anim_ = 1.0F;
|
||||
} else if (open_anim < 1.0F) {
|
||||
open_anim += OPEN_SPEED * dt;
|
||||
open_anim = std::min(open_anim, 1.0F);
|
||||
}
|
||||
|
||||
// Avança transició
|
||||
if (transition_active_) {
|
||||
transition_progress_ += TRANSITION_SPEED * dt;
|
||||
if (transition_progress_ >= 1.0F) {
|
||||
transition_progress_ = 1.0F;
|
||||
transition_active_ = false;
|
||||
if (transition_active) {
|
||||
transition_progress += TRANSITION_SPEED * dt;
|
||||
if (transition_progress >= 1.0F) {
|
||||
transition_progress = 1.0F;
|
||||
transition_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
const Page& page = stack_.back();
|
||||
const int current_h = boxHeight(page);
|
||||
const Page& page = stack.back();
|
||||
const int CURRENT_H = boxHeight(page);
|
||||
|
||||
// Smoothing exponencial de l'alçada cap al target (pàgina actual + ítems visibles).
|
||||
// Permet que el menú reaccione amb animació quan una opció canvia la visibilitat
|
||||
// d'altres ítems en calent (p. ex. shader=off → shader_type/preset/supersampling).
|
||||
if (animated_h_ <= 0.0F) {
|
||||
animated_h_ = static_cast<float>(current_h);
|
||||
if (animated_h <= 0.0F) {
|
||||
animated_h = static_cast<float>(CURRENT_H);
|
||||
} else {
|
||||
float diff = static_cast<float>(current_h) - animated_h_;
|
||||
float diff = static_cast<float>(CURRENT_H) - animated_h;
|
||||
if (std::fabs(diff) < 0.5F) {
|
||||
animated_h_ = static_cast<float>(current_h);
|
||||
animated_h = static_cast<float>(CURRENT_H);
|
||||
} else {
|
||||
float t = HEIGHT_RATE * dt;
|
||||
if (t > 1.0F) t = 1.0F;
|
||||
animated_h_ += diff * t;
|
||||
t = std::min(t, 1.0F);
|
||||
animated_h += diff * t;
|
||||
}
|
||||
}
|
||||
|
||||
float eased = Easing::outQuad(open_anim_);
|
||||
float eased = Easing::outQuad(open_anim);
|
||||
|
||||
// Calcula alçada (amb transició si escau)
|
||||
int target_h = static_cast<int>(animated_h_);
|
||||
if (transition_active_) {
|
||||
int outgoing_h = boxHeight(transition_outgoing_);
|
||||
float tp = Easing::outQuad(transition_progress_);
|
||||
target_h = Easing::lerpInt(outgoing_h, static_cast<int>(animated_h_), tp);
|
||||
int target_h = static_cast<int>(animated_h);
|
||||
if (transition_active) {
|
||||
int outgoing_h = boxHeight(transition_outgoing);
|
||||
float tp = Easing::outQuad(transition_progress);
|
||||
target_h = Easing::lerpInt(outgoing_h, static_cast<int>(animated_h), tp);
|
||||
}
|
||||
|
||||
// Caixa creix verticalment durant l'obertura
|
||||
int box_h = static_cast<int>(target_h * eased);
|
||||
if (box_h < 2) box_h = 2;
|
||||
box_h = std::max(box_h, 2);
|
||||
int box_x = (SCREEN_W - BOX_W) / 2;
|
||||
int box_y = (SCREEN_H - box_h) / 2;
|
||||
|
||||
// Fons semi-transparent (alpha escalat per l'animació d'obertura)
|
||||
Uint8 alpha = static_cast<Uint8>(BG_ALPHA * eased);
|
||||
auto alpha = static_cast<Uint8>(BG_ALPHA * eased);
|
||||
blendRect(pixel_data, box_x, box_y, BOX_W, box_h, BG_COLOR, alpha);
|
||||
|
||||
// El contingut només apareix quan la caixa és prou gran
|
||||
if (open_anim_ >= 0.9F) {
|
||||
if (open_anim >= 0.9F) {
|
||||
int clip_x_min = box_x + 1;
|
||||
int clip_x_max = box_x + BOX_W - 1;
|
||||
int clip_y_min = box_y + 1;
|
||||
int clip_y_max = box_y + box_h - 1;
|
||||
|
||||
if (transition_active_) {
|
||||
float tp = Easing::outQuad(transition_progress_);
|
||||
int out_offset = static_cast<int>(-transition_dir_ * BOX_W * tp);
|
||||
int new_offset = static_cast<int>(transition_dir_ * BOX_W * (1.0F - tp));
|
||||
renderPageContent(pixel_data, transition_outgoing_, box_x, box_y, out_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
if (transition_active) {
|
||||
float tp = Easing::outQuad(transition_progress);
|
||||
int out_offset = static_cast<int>(-transition_dir * BOX_W * tp);
|
||||
int new_offset = static_cast<int>(transition_dir * BOX_W * (1.0F - tp));
|
||||
renderPageContent(pixel_data, transition_outgoing, box_x, box_y, out_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
renderPageContent(pixel_data, page, box_x, box_y, new_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
} else {
|
||||
renderPageContent(pixel_data, page, box_x, box_y, 0, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||
|
||||
+270
-281
@@ -14,7 +14,7 @@
|
||||
|
||||
namespace Overlay {
|
||||
|
||||
static std::unique_ptr<Text> font_;
|
||||
static std::unique_ptr<Text> font;
|
||||
|
||||
// --- Aspecte de la notificació ---
|
||||
static constexpr Uint32 NOTIF_BG_COLOR = 0xFF2E1A1A; // Fons blau fosc (ABGR)
|
||||
@@ -33,7 +33,7 @@ namespace Overlay {
|
||||
|
||||
// --- Estat de les notificacions ---
|
||||
|
||||
enum class Status { RISING,
|
||||
enum class Status : std::uint8_t { RISING,
|
||||
STAY,
|
||||
VANISHING,
|
||||
FINISHED };
|
||||
@@ -52,12 +52,12 @@ namespace Overlay {
|
||||
int box_h{0}; // Alçada de la caixa (calculat al crear)
|
||||
};
|
||||
|
||||
static std::vector<Notification> notifications_;
|
||||
static Uint32 last_ticks_ = 0;
|
||||
static std::vector<Notification> notifications;
|
||||
static Uint32 last_ticks = 0;
|
||||
|
||||
// --- Render info ---
|
||||
static Options::RenderInfoPosition info_visible_pos_ = Options::RenderInfoPosition::OFF;
|
||||
static float info_anim_ = 0.0F; // 0 = fora de pantalla, 1 = posició final
|
||||
static Options::RenderInfoPosition info_visible_pos = Options::RenderInfoPosition::OFF;
|
||||
static float info_anim = 0.0F; // 0 = fora de pantalla, 1 = posició final
|
||||
static constexpr float INFO_SLIDE_SPEED = 5.0F;
|
||||
|
||||
// Segments del render info — cadascú amb la seva pròpia visibilitat animada
|
||||
@@ -69,17 +69,17 @@ namespace Overlay {
|
||||
bool visible{false};
|
||||
bool mono_digits{false}; // si true, dígits amb amplada fixa (la resta natural)
|
||||
};
|
||||
static InfoSegment info_segments_[INFO_SEGMENT_COUNT];
|
||||
static InfoSegment info_segments[INFO_SEGMENT_COUNT];
|
||||
|
||||
// --- Crèdits cinematogràfics ---
|
||||
// Usen el sistema de notificacions en posició TOP_CENTER_DROP.
|
||||
enum class CreditsPhase { IDLE,
|
||||
enum class CreditsPhase : std::uint8_t { IDLE,
|
||||
DELAY,
|
||||
PLAYING_1,
|
||||
GAP,
|
||||
PLAYING_2 };
|
||||
static CreditsPhase credits_phase_ = CreditsPhase::IDLE;
|
||||
static float credits_timer_ = 0.0F; // segons dins la phase actual
|
||||
static CreditsPhase credits_phase = CreditsPhase::IDLE;
|
||||
static float credits_timer = 0.0F; // segons dins la phase actual
|
||||
static constexpr float CREDITS_DELAY = 2.0F;
|
||||
static constexpr float CREDITS_GAP = 0.4F;
|
||||
static constexpr float CREDITS_HOLD = 7.5F;
|
||||
@@ -87,281 +87,268 @@ namespace Overlay {
|
||||
static constexpr Uint32 CREDITS_FG = NOTIF_TEXT_COLOR; // mateix cian
|
||||
|
||||
// --- Doble ESC per a eixir ---
|
||||
static bool esc_waiting_ = false;
|
||||
static bool esc_waiting = false;
|
||||
|
||||
void init() {
|
||||
font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
|
||||
last_ticks_ = SDL_GetTicks();
|
||||
font = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
|
||||
last_ticks = SDL_GetTicks();
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
font_.reset();
|
||||
notifications_.clear();
|
||||
font.reset();
|
||||
notifications.clear();
|
||||
}
|
||||
|
||||
// Pinta un rectangle sòlid dins els límits de la pantalla
|
||||
static void drawRect(Uint32* pixel_data, int rx, int ry, int rw, int rh, Uint32 color) {
|
||||
for (int row = ry; row < ry + rh; row++) {
|
||||
if (row < 0 || row >= SCREEN_H) continue;
|
||||
if (row < 0 || row >= SCREEN_H) {
|
||||
continue;
|
||||
}
|
||||
for (int col = rx; col < rx + rw; col++) {
|
||||
if (col < 0 || col >= SCREEN_W) continue;
|
||||
pixel_data[col + row * SCREEN_W] = color;
|
||||
if (col < 0 || col >= SCREEN_W) {
|
||||
continue;
|
||||
}
|
||||
pixel_data[col + (row * SCREEN_W)] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void updateNotifFsm(Notification& notif, float dt) {
|
||||
switch (notif.status) {
|
||||
case Status::RISING:
|
||||
notif.anim += SLIDE_SPEED * dt;
|
||||
if (notif.anim >= 1.0F) {
|
||||
notif.anim = 1.0F;
|
||||
notif.status = Status::STAY;
|
||||
notif.timer = 0.0F;
|
||||
}
|
||||
break;
|
||||
case Status::STAY:
|
||||
notif.timer += dt;
|
||||
if (notif.timer >= notif.duration) {
|
||||
notif.status = Status::VANISHING;
|
||||
}
|
||||
break;
|
||||
case Status::VANISHING:
|
||||
notif.anim -= SLIDE_SPEED * dt;
|
||||
if (notif.anim <= 0.0F) {
|
||||
notif.status = Status::FINISHED;
|
||||
}
|
||||
break;
|
||||
case Status::FINISHED:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void computeNotifBoxPos(const Notification& notif, int& box_x, int& box_y) {
|
||||
switch (notif.pos) {
|
||||
case NotifPosition::TOP_LEFT_SLIDE:
|
||||
box_x = NOTIF_MARGIN_X - static_cast<int>((1.0F - notif.anim) * (notif.box_w + NOTIF_MARGIN_X));
|
||||
box_y = NOTIF_MARGIN_Y;
|
||||
break;
|
||||
case NotifPosition::TOP_CENTER_DROP:
|
||||
box_x = (SCREEN_W - notif.box_w) / 2;
|
||||
box_y = NOTIF_MARGIN_Y - static_cast<int>((1.0F - notif.anim) * (notif.box_h + NOTIF_MARGIN_Y));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void drawNotifTextLine(Uint32* pixel_data, const std::string& line, int line_x, int line_y, const Notification& notif) {
|
||||
if (notif.style == NotifStyle::SHADOW) {
|
||||
font->draw(pixel_data, line_x + 1, line_y + 1, line.c_str(), notif.accent_color);
|
||||
} else if (notif.style == NotifStyle::OUTLINE) {
|
||||
font->draw(pixel_data, line_x, line_y - 1, line.c_str(), notif.accent_color);
|
||||
font->draw(pixel_data, line_x, line_y + 1, line.c_str(), notif.accent_color);
|
||||
font->draw(pixel_data, line_x - 1, line_y, line.c_str(), notif.accent_color);
|
||||
font->draw(pixel_data, line_x + 1, line_y, line.c_str(), notif.accent_color);
|
||||
}
|
||||
font->draw(pixel_data, line_x, line_y, line.c_str(), notif.text_color);
|
||||
}
|
||||
|
||||
static void renderOneNotification(Uint32* pixel_data, const Notification& notif) {
|
||||
int box_x = 0;
|
||||
int box_y = 0;
|
||||
computeNotifBoxPos(notif, box_x, box_y);
|
||||
if (notif.style == NotifStyle::BOX) {
|
||||
drawRect(pixel_data, box_x, box_y, notif.box_w, notif.box_h, notif.accent_color);
|
||||
}
|
||||
const int LINE_H = font->charHeight();
|
||||
int line_y = box_y + NOTIF_PADDING_V;
|
||||
for (const auto& line : notif.lines) {
|
||||
const int LINE_W = font->width(line.c_str());
|
||||
const int LINE_X = box_x + ((notif.box_w - LINE_W) / 2);
|
||||
drawNotifTextLine(pixel_data, line, LINE_X, line_y, notif);
|
||||
line_y += LINE_H + 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void updateRenderInfoFsm(float dt) {
|
||||
const auto DESIRED = Options::render_info.position;
|
||||
if (DESIRED == info_visible_pos) {
|
||||
if (info_anim < 1.0F) {
|
||||
info_anim = std::min(info_anim + (INFO_SLIDE_SPEED * dt), 1.0F);
|
||||
}
|
||||
} else if (info_visible_pos == Options::RenderInfoPosition::OFF) {
|
||||
info_visible_pos = DESIRED;
|
||||
info_anim = 0.0F;
|
||||
} else {
|
||||
info_anim -= INFO_SLIDE_SPEED * dt;
|
||||
if (info_anim <= 0.0F) {
|
||||
info_anim = 0.0F;
|
||||
info_visible_pos = DESIRED;
|
||||
}
|
||||
}
|
||||
for (auto& seg : info_segments) {
|
||||
const float TARGET = seg.visible ? 1.0F : 0.0F;
|
||||
if (seg.anim < TARGET) {
|
||||
seg.anim = std::min(seg.anim + (SEG_SPEED * dt), TARGET);
|
||||
} else if (seg.anim > TARGET) {
|
||||
seg.anim = std::max(seg.anim - (SEG_SPEED * dt), TARGET);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static auto computeInfoTotalWidth(int digit_cell) -> float {
|
||||
float total_w = 0.0F;
|
||||
for (const auto& seg : info_segments) {
|
||||
if (seg.anim > 0.0F && !seg.text.empty()) {
|
||||
const int W = seg.mono_digits
|
||||
? font->widthMonoDigits(seg.text.c_str(), digit_cell)
|
||||
: font->width(seg.text.c_str());
|
||||
total_w += static_cast<float>(W) * Easing::outQuad(seg.anim);
|
||||
}
|
||||
}
|
||||
return total_w;
|
||||
}
|
||||
|
||||
static void drawInfoSegment(Uint32* pixel_data, const InfoSegment& seg, int xi, int info_y, int digit_cell) {
|
||||
if (seg.mono_digits) {
|
||||
font->drawMonoDigits(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color, digit_cell);
|
||||
font->drawMonoDigits(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color, digit_cell);
|
||||
} else {
|
||||
font->draw(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color);
|
||||
font->draw(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color);
|
||||
}
|
||||
}
|
||||
|
||||
static void renderRenderInfo(Uint32* pixel_data) {
|
||||
if (info_visible_pos == Options::RenderInfoPosition::OFF || info_anim <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
const int DIGIT_CELL = font->charBoxWidth() - 1;
|
||||
const float TOTAL_W = computeInfoTotalWidth(DIGIT_CELL);
|
||||
if (TOTAL_W <= 0.0F) {
|
||||
return;
|
||||
}
|
||||
const float EASED_Y = Easing::outQuad(info_anim);
|
||||
const int CH = font->charHeight();
|
||||
const int FINAL_Y = (info_visible_pos == Options::RenderInfoPosition::TOP) ? 1 : SCREEN_H - CH - 1;
|
||||
const int START_Y = (info_visible_pos == Options::RenderInfoPosition::TOP) ? -CH - 1 : SCREEN_H;
|
||||
const int INFO_Y = START_Y + static_cast<int>(static_cast<float>(FINAL_Y - START_Y) * EASED_Y);
|
||||
float cur_x = (SCREEN_W - TOTAL_W) / 2.0F;
|
||||
for (const auto& seg : info_segments) {
|
||||
if (seg.anim <= 0.01F || seg.text.empty()) {
|
||||
continue;
|
||||
}
|
||||
const int XI = static_cast<int>(cur_x);
|
||||
const int SEG_W = seg.mono_digits
|
||||
? font->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
|
||||
: font->width(seg.text.c_str());
|
||||
drawInfoSegment(pixel_data, seg, XI, INFO_Y, DIGIT_CELL);
|
||||
cur_x += static_cast<float>(SEG_W) * Easing::outQuad(seg.anim);
|
||||
}
|
||||
}
|
||||
|
||||
static void renderPauseIndicator(Uint32* pixel_data) {
|
||||
if ((Director::get() == nullptr) || !Director::get()->isPaused()) {
|
||||
return;
|
||||
}
|
||||
const char* pause_text = Locale::get("notifications.pause");
|
||||
const int W = font->width(pause_text);
|
||||
const int X = SCREEN_W - W - 4;
|
||||
const int Y = 4;
|
||||
font->draw(pixel_data, X, Y - 1, pause_text, 0xFFFFFFFF);
|
||||
font->draw(pixel_data, X, Y + 1, pause_text, 0xFFFFFFFF);
|
||||
font->draw(pixel_data, X - 1, Y, pause_text, 0xFFFFFFFF);
|
||||
font->draw(pixel_data, X + 1, Y, pause_text, 0xFFFFFFFF);
|
||||
font->draw(pixel_data, X, Y, pause_text, 0xFF0000FF);
|
||||
}
|
||||
|
||||
static void emitCreditsLines(const char* role_key, const char* name_key) {
|
||||
showNotification(
|
||||
{std::string(Locale::get(role_key)), std::string(Locale::get(name_key))},
|
||||
CREDITS_HOLD,
|
||||
NotifPosition::TOP_CENTER_DROP,
|
||||
NotifStyle::OUTLINE,
|
||||
CREDITS_BG,
|
||||
CREDITS_FG);
|
||||
}
|
||||
|
||||
static void advanceCredits(float dt) {
|
||||
if (credits_phase == CreditsPhase::IDLE) {
|
||||
return;
|
||||
}
|
||||
credits_timer += dt;
|
||||
switch (credits_phase) {
|
||||
case CreditsPhase::DELAY:
|
||||
if (credits_timer >= CREDITS_DELAY) {
|
||||
emitCreditsLines("credits.port_role", "credits.port_name");
|
||||
credits_phase = CreditsPhase::PLAYING_1;
|
||||
credits_timer = 0.0F;
|
||||
}
|
||||
break;
|
||||
case CreditsPhase::PLAYING_1:
|
||||
if (notifications.empty()) {
|
||||
credits_phase = CreditsPhase::GAP;
|
||||
credits_timer = 0.0F;
|
||||
}
|
||||
break;
|
||||
case CreditsPhase::GAP:
|
||||
if (credits_timer >= CREDITS_GAP) {
|
||||
emitCreditsLines("credits.modern_role", "credits.modern_name");
|
||||
credits_phase = CreditsPhase::PLAYING_2;
|
||||
credits_timer = 0.0F;
|
||||
}
|
||||
break;
|
||||
case CreditsPhase::PLAYING_2:
|
||||
if (notifications.empty()) {
|
||||
credits_phase = CreditsPhase::IDLE;
|
||||
credits_timer = 0.0F;
|
||||
}
|
||||
break;
|
||||
case CreditsPhase::IDLE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void render(Uint32* pixel_data) {
|
||||
if (!font_ || !pixel_data) return;
|
||||
if (!font || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
const Uint32 NOW = SDL_GetTicks();
|
||||
const float DT = static_cast<float>(NOW - last_ticks) / 1000.0F;
|
||||
last_ticks = NOW;
|
||||
|
||||
// Calcula delta time
|
||||
Uint32 now = SDL_GetTicks();
|
||||
float dt = static_cast<float>(now - last_ticks_) / 1000.0F;
|
||||
last_ticks_ = now;
|
||||
|
||||
// Actualitza i pinta cada notificació
|
||||
for (auto& notif : notifications_) {
|
||||
switch (notif.status) {
|
||||
case Status::RISING:
|
||||
notif.anim += SLIDE_SPEED * dt;
|
||||
if (notif.anim >= 1.0F) {
|
||||
notif.anim = 1.0F;
|
||||
notif.status = Status::STAY;
|
||||
notif.timer = 0.0F;
|
||||
}
|
||||
break;
|
||||
|
||||
case Status::STAY:
|
||||
notif.timer += dt;
|
||||
if (notif.timer >= notif.duration) {
|
||||
notif.status = Status::VANISHING;
|
||||
}
|
||||
break;
|
||||
|
||||
case Status::VANISHING:
|
||||
notif.anim -= SLIDE_SPEED * dt;
|
||||
if (notif.anim <= 0.0F) {
|
||||
notif.status = Status::FINISHED;
|
||||
}
|
||||
break;
|
||||
|
||||
case Status::FINISHED:
|
||||
break;
|
||||
}
|
||||
|
||||
if (notif.status == Status::FINISHED) continue;
|
||||
|
||||
// Posició segons el tipus
|
||||
int box_x = 0;
|
||||
int box_y = 0;
|
||||
switch (notif.pos) {
|
||||
case NotifPosition::TOP_LEFT_SLIDE: {
|
||||
int target_x = NOTIF_MARGIN_X;
|
||||
int target_y = NOTIF_MARGIN_Y;
|
||||
box_x = target_x - static_cast<int>((1.0F - notif.anim) * (notif.box_w + NOTIF_MARGIN_X));
|
||||
box_y = target_y;
|
||||
break;
|
||||
}
|
||||
case NotifPosition::TOP_CENTER_DROP: {
|
||||
int target_y = NOTIF_MARGIN_Y;
|
||||
box_x = (SCREEN_W - notif.box_w) / 2;
|
||||
// Baixa des de sobre de la pantalla fins a target_y
|
||||
box_y = target_y - static_cast<int>((1.0F - notif.anim) * (notif.box_h + NOTIF_MARGIN_Y));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Pinta fons (si BOX)
|
||||
if (notif.style == NotifStyle::BOX) {
|
||||
drawRect(pixel_data, box_x, box_y, notif.box_w, notif.box_h, notif.accent_color);
|
||||
}
|
||||
|
||||
// Pinta el text línia a línia (amb ombra o contorn segons style)
|
||||
int line_h = font_->charHeight();
|
||||
int line_y = box_y + NOTIF_PADDING_V;
|
||||
for (const auto& line : notif.lines) {
|
||||
int line_w = font_->width(line.c_str());
|
||||
int line_x = box_x + (notif.box_w - line_w) / 2; // centrat dins la caixa
|
||||
if (notif.style == NotifStyle::SHADOW) {
|
||||
font_->draw(pixel_data, line_x + 1, line_y + 1, line.c_str(), notif.accent_color);
|
||||
} else if (notif.style == NotifStyle::OUTLINE) {
|
||||
// Contorn 4-direccional (N, S, E, W)
|
||||
font_->draw(pixel_data, line_x, line_y - 1, line.c_str(), notif.accent_color);
|
||||
font_->draw(pixel_data, line_x, line_y + 1, line.c_str(), notif.accent_color);
|
||||
font_->draw(pixel_data, line_x - 1, line_y, line.c_str(), notif.accent_color);
|
||||
font_->draw(pixel_data, line_x + 1, line_y, line.c_str(), notif.accent_color);
|
||||
}
|
||||
font_->draw(pixel_data, line_x, line_y, line.c_str(), notif.text_color);
|
||||
line_y += line_h + 1;
|
||||
for (auto& notif : notifications) {
|
||||
updateNotifFsm(notif, DT);
|
||||
if (notif.status != Status::FINISHED) {
|
||||
renderOneNotification(pixel_data, notif);
|
||||
}
|
||||
}
|
||||
|
||||
// Render info (FPS, driver, shader) — animat amb slide vertical
|
||||
// State machine: visible_pos s'actualitza cap a desired quan anim arriba a 0
|
||||
{
|
||||
const auto desired = Options::render_info.position;
|
||||
if (desired == info_visible_pos_) {
|
||||
// Mateix lloc: entra fins a 1
|
||||
if (info_anim_ < 1.0F) {
|
||||
info_anim_ += INFO_SLIDE_SPEED * dt;
|
||||
if (info_anim_ > 1.0F) info_anim_ = 1.0F;
|
||||
}
|
||||
} else {
|
||||
// Canvi: si visible_pos està OFF, commuta directament
|
||||
if (info_visible_pos_ == Options::RenderInfoPosition::OFF) {
|
||||
info_visible_pos_ = desired;
|
||||
info_anim_ = 0.0F;
|
||||
} else {
|
||||
// Ix del lloc actual
|
||||
info_anim_ -= INFO_SLIDE_SPEED * dt;
|
||||
if (info_anim_ <= 0.0F) {
|
||||
info_anim_ = 0.0F;
|
||||
info_visible_pos_ = desired;
|
||||
}
|
||||
}
|
||||
}
|
||||
updateRenderInfoFsm(DT);
|
||||
renderRenderInfo(pixel_data);
|
||||
|
||||
// Actualitza animacions individuals dels segments
|
||||
for (auto& seg : info_segments_) {
|
||||
float target = seg.visible ? 1.0F : 0.0F;
|
||||
if (seg.anim < target) {
|
||||
seg.anim += SEG_SPEED * dt;
|
||||
if (seg.anim > target) seg.anim = target;
|
||||
} else if (seg.anim > target) {
|
||||
seg.anim -= SEG_SPEED * dt;
|
||||
if (seg.anim < target) seg.anim = target;
|
||||
}
|
||||
}
|
||||
|
||||
// Render si hi ha alguna cosa visible
|
||||
if (info_visible_pos_ != Options::RenderInfoPosition::OFF && info_anim_ > 0.0F) {
|
||||
const int DIGIT_CELL = font_->charBoxWidth() - 1; // amplada uniforme per dígit
|
||||
|
||||
// Calcula amplada total interpolant cada segment per la seva anim
|
||||
float total_w = 0.0F;
|
||||
for (auto& seg : info_segments_) {
|
||||
if (seg.anim > 0.0F && !seg.text.empty()) {
|
||||
int w = seg.mono_digits
|
||||
? font_->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
|
||||
: font_->width(seg.text.c_str());
|
||||
total_w += w * Easing::outQuad(seg.anim);
|
||||
}
|
||||
}
|
||||
if (total_w > 0.0F) {
|
||||
float eased_y = Easing::outQuad(info_anim_);
|
||||
int ch = font_->charHeight();
|
||||
int final_y;
|
||||
int start_y;
|
||||
if (info_visible_pos_ == Options::RenderInfoPosition::TOP) {
|
||||
final_y = 1;
|
||||
start_y = -ch - 1;
|
||||
} else {
|
||||
final_y = SCREEN_H - ch - 1;
|
||||
start_y = SCREEN_H;
|
||||
}
|
||||
int info_y = start_y + static_cast<int>((final_y - start_y) * eased_y);
|
||||
|
||||
// Dibuixa cada segment en la seva posició x acumulada
|
||||
float cur_x = (SCREEN_W - total_w) / 2.0F;
|
||||
for (auto& seg : info_segments_) {
|
||||
if (seg.anim > 0.01F && !seg.text.empty()) {
|
||||
int xi = static_cast<int>(cur_x);
|
||||
int seg_w = seg.mono_digits
|
||||
? font_->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
|
||||
: font_->width(seg.text.c_str());
|
||||
if (seg.mono_digits) {
|
||||
font_->drawMonoDigits(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color, DIGIT_CELL);
|
||||
font_->drawMonoDigits(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color, DIGIT_CELL);
|
||||
} else {
|
||||
font_->draw(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color);
|
||||
font_->draw(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color);
|
||||
}
|
||||
cur_x += seg_w * Easing::outQuad(seg.anim);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::erase_if(notifications, [](const Notification& n) { return n.status == Status::FINISHED; });
|
||||
if (esc_waiting && notifications.empty()) {
|
||||
esc_waiting = false;
|
||||
}
|
||||
|
||||
// Elimina les acabades
|
||||
notifications_.erase(
|
||||
std::remove_if(notifications_.begin(), notifications_.end(), [](const Notification& n) { return n.status == Status::FINISHED; }),
|
||||
notifications_.end());
|
||||
renderPauseIndicator(pixel_data);
|
||||
advanceCredits(DT);
|
||||
|
||||
// Si la notificació d'ESC ha desaparegut, reseteja l'estat
|
||||
if (esc_waiting_ && notifications_.empty()) {
|
||||
esc_waiting_ = false;
|
||||
}
|
||||
|
||||
// Indicador de pausa persistent (cantó superior dret)
|
||||
if (Director::get() && Director::get()->isPaused()) {
|
||||
const char* pause_text = Locale::get("notifications.pause");
|
||||
int w = font_->width(pause_text);
|
||||
int x = SCREEN_W - w - 4;
|
||||
int y = 4;
|
||||
// Contorn blanc 4-direccional
|
||||
font_->draw(pixel_data, x, y - 1, pause_text, 0xFFFFFFFF);
|
||||
font_->draw(pixel_data, x, y + 1, pause_text, 0xFFFFFFFF);
|
||||
font_->draw(pixel_data, x - 1, y, pause_text, 0xFFFFFFFF);
|
||||
font_->draw(pixel_data, x + 1, y, pause_text, 0xFFFFFFFF);
|
||||
// Text en roig
|
||||
font_->draw(pixel_data, x, y, pause_text, 0xFF0000FF);
|
||||
}
|
||||
|
||||
// Crèdits seqüencials — dispara notificacions TOP_CENTER_DROP una darrere l'altra.
|
||||
if (credits_phase_ != CreditsPhase::IDLE) {
|
||||
credits_timer_ += dt;
|
||||
switch (credits_phase_) {
|
||||
case CreditsPhase::DELAY:
|
||||
if (credits_timer_ >= CREDITS_DELAY) {
|
||||
showNotification(
|
||||
{std::string(Locale::get("credits.port_role")),
|
||||
std::string(Locale::get("credits.port_name"))},
|
||||
CREDITS_HOLD,
|
||||
NotifPosition::TOP_CENTER_DROP,
|
||||
NotifStyle::OUTLINE,
|
||||
CREDITS_BG,
|
||||
CREDITS_FG);
|
||||
credits_phase_ = CreditsPhase::PLAYING_1;
|
||||
credits_timer_ = 0.0F;
|
||||
}
|
||||
break;
|
||||
case CreditsPhase::PLAYING_1:
|
||||
if (notifications_.empty()) {
|
||||
credits_phase_ = CreditsPhase::GAP;
|
||||
credits_timer_ = 0.0F;
|
||||
}
|
||||
break;
|
||||
case CreditsPhase::GAP:
|
||||
if (credits_timer_ >= CREDITS_GAP) {
|
||||
showNotification(
|
||||
{std::string(Locale::get("credits.modern_role")),
|
||||
std::string(Locale::get("credits.modern_name"))},
|
||||
CREDITS_HOLD,
|
||||
NotifPosition::TOP_CENTER_DROP,
|
||||
NotifStyle::OUTLINE,
|
||||
CREDITS_BG,
|
||||
CREDITS_FG);
|
||||
credits_phase_ = CreditsPhase::PLAYING_2;
|
||||
credits_timer_ = 0.0F;
|
||||
}
|
||||
break;
|
||||
case CreditsPhase::PLAYING_2:
|
||||
if (notifications_.empty()) {
|
||||
credits_phase_ = CreditsPhase::IDLE;
|
||||
credits_timer_ = 0.0F;
|
||||
}
|
||||
break;
|
||||
case CreditsPhase::IDLE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Neteja notificacions finalitzades
|
||||
notifications_.erase(
|
||||
std::remove_if(notifications_.begin(), notifications_.end(), [](const Notification& n) { return n.status == Status::FINISHED; }),
|
||||
notifications_.end());
|
||||
|
||||
// Menú flotant per damunt de tot (isVisible inclou l'animació de tancament)
|
||||
std::erase_if(notifications, [](const Notification& n) { return n.status == Status::FINISHED; });
|
||||
if (Menu::isVisible()) {
|
||||
Menu::render(pixel_data);
|
||||
}
|
||||
@@ -378,7 +365,7 @@ namespace Overlay {
|
||||
Uint32 accent_color,
|
||||
Uint32 text_color) {
|
||||
// Reemplaça la notificació anterior
|
||||
notifications_.clear();
|
||||
notifications.clear();
|
||||
|
||||
Notification notif;
|
||||
notif.lines = lines;
|
||||
@@ -391,15 +378,15 @@ namespace Overlay {
|
||||
// Calcula l'amplada màxima de les línies
|
||||
int max_w = 0;
|
||||
for (const auto& line : lines) {
|
||||
int w = font_->width(line.c_str());
|
||||
if (w > max_w) max_w = w;
|
||||
int w = font->width(line.c_str());
|
||||
max_w = std::max(w, max_w);
|
||||
}
|
||||
notif.box_w = max_w + NOTIF_PADDING_H * 2;
|
||||
int line_h = font_->charHeight();
|
||||
int line_h = font->charHeight();
|
||||
int line_count = static_cast<int>(lines.size());
|
||||
notif.box_h = line_count * line_h + (line_count - 1) * 1 + NOTIF_PADDING_V * 2;
|
||||
|
||||
notifications_.push_back(notif);
|
||||
notifications.push_back(notif);
|
||||
}
|
||||
|
||||
void toggleRenderInfo() { cycleRenderInfo(+1); }
|
||||
@@ -414,45 +401,47 @@ namespace Overlay {
|
||||
void setRenderInfoSegments(const char* s0, const char* s1, const char* s2, const char* s3, unsigned int mono_mask) {
|
||||
const char* segs[INFO_SEGMENT_COUNT] = {s0, s1, s2, s3};
|
||||
for (int i = 0; i < INFO_SEGMENT_COUNT; i++) {
|
||||
info_segments_[i].mono_digits = (mono_mask >> i) & 1u;
|
||||
info_segments[i].mono_digits = (((mono_mask >> i) & 1U) != 0U);
|
||||
if (segs[i] != nullptr && *segs[i] != '\0') {
|
||||
info_segments_[i].text = segs[i];
|
||||
info_segments_[i].visible = true;
|
||||
info_segments[i].text = segs[i];
|
||||
info_segments[i].visible = true;
|
||||
} else {
|
||||
info_segments_[i].visible = false;
|
||||
info_segments[i].visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void startCredits() {
|
||||
if (credits_phase_ != CreditsPhase::IDLE) return;
|
||||
credits_phase_ = CreditsPhase::DELAY;
|
||||
credits_timer_ = 0.0F;
|
||||
if (credits_phase != CreditsPhase::IDLE) {
|
||||
return;
|
||||
}
|
||||
credits_phase = CreditsPhase::DELAY;
|
||||
credits_timer = 0.0F;
|
||||
}
|
||||
|
||||
void cancelCredits() {
|
||||
credits_phase_ = CreditsPhase::IDLE;
|
||||
credits_timer_ = 0.0F;
|
||||
notifications_.clear();
|
||||
credits_phase = CreditsPhase::IDLE;
|
||||
credits_timer = 0.0F;
|
||||
notifications.clear();
|
||||
}
|
||||
|
||||
auto creditsActive() -> bool {
|
||||
return credits_phase_ != CreditsPhase::IDLE;
|
||||
return credits_phase != CreditsPhase::IDLE;
|
||||
}
|
||||
|
||||
auto isEscConsumed() -> bool {
|
||||
return esc_waiting_;
|
||||
return esc_waiting;
|
||||
}
|
||||
|
||||
auto handleEscape() -> bool {
|
||||
if (!esc_waiting_) {
|
||||
if (!esc_waiting) {
|
||||
// Primera pulsació: mostra avís i consumeix
|
||||
esc_waiting_ = true;
|
||||
esc_waiting = true;
|
||||
showNotification(Locale::get("notifications.exit_double_esc"), 2.0F);
|
||||
return true; // Consumit
|
||||
}
|
||||
// Segona pulsació: deixa passar
|
||||
esc_waiting_ = false;
|
||||
esc_waiting = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -13,13 +14,13 @@ namespace Overlay {
|
||||
void render(Uint32* pixel_data);
|
||||
|
||||
// Posició + animació d'una notificació
|
||||
enum class NotifPosition {
|
||||
enum class NotifPosition : std::uint8_t {
|
||||
TOP_LEFT_SLIDE, // Cantó superior esquerra, slide horizontal des de fora
|
||||
TOP_CENTER_DROP, // Centrat horitzontal, baixa des de sobre
|
||||
};
|
||||
|
||||
// Estil de la notificació: caixa de fons, ombra o contorn del text
|
||||
enum class NotifStyle {
|
||||
enum class NotifStyle : std::uint8_t {
|
||||
BOX, // Rectangle de fons amb accent_color
|
||||
SHADOW, // Sense fons, text amb ombra (offset +1,+1) en accent_color
|
||||
OUTLINE, // Sense fons, text amb contorn 4-direccional en accent_color
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "core/rendering/screen.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
|
||||
@@ -55,18 +56,18 @@ namespace {
|
||||
} // namespace
|
||||
#endif // __EMSCRIPTEN__
|
||||
|
||||
std::unique_ptr<Screen> Screen::instance_;
|
||||
std::unique_ptr<Screen> Screen::instance;
|
||||
|
||||
void Screen::init() {
|
||||
instance_ = std::unique_ptr<Screen>(new Screen());
|
||||
instance = std::unique_ptr<Screen>(new Screen());
|
||||
}
|
||||
|
||||
void Screen::destroy() {
|
||||
instance_.reset();
|
||||
instance.reset();
|
||||
}
|
||||
|
||||
auto Screen::get() -> Screen* {
|
||||
return instance_.get();
|
||||
return instance.get();
|
||||
}
|
||||
|
||||
Screen::Screen() {
|
||||
@@ -76,15 +77,15 @@ Screen::Screen() {
|
||||
|
||||
calculateMaxZoom();
|
||||
|
||||
if (zoom_ < 1) zoom_ = 1;
|
||||
if (zoom_ > max_zoom_) zoom_ = max_zoom_;
|
||||
zoom_ = std::max(zoom_, 1);
|
||||
zoom_ = std::min(zoom_, max_zoom_);
|
||||
|
||||
// Clamp de la resolució interna a [1, max_zoom_]. Llegir del YAML i
|
||||
// ajustar aquí és l'únic moment en què es fa — el menú re-clampa cada
|
||||
// canvi. Si la pantalla és més petita que el valor desat (p.ex. canvi
|
||||
// de monitor), baixem al màxim suportat.
|
||||
if (Options::video.internal_resolution < 1) Options::video.internal_resolution = 1;
|
||||
if (Options::video.internal_resolution > max_zoom_) Options::video.internal_resolution = max_zoom_;
|
||||
Options::video.internal_resolution = std::max(Options::video.internal_resolution, 1);
|
||||
Options::video.internal_resolution = std::min(Options::video.internal_resolution, max_zoom_);
|
||||
|
||||
int w = GAME_WIDTH * zoom_;
|
||||
int h = Options::video.aspect_ratio_4_3 ? static_cast<int>(GAME_HEIGHT * 1.2F) * zoom_ : GAME_HEIGHT * zoom_;
|
||||
@@ -124,15 +125,25 @@ Screen::~Screen() {
|
||||
if (shader_backend_) {
|
||||
#ifndef NO_SHADERS
|
||||
auto* gpu = dynamic_cast<Rendering::SDL3GPUShader*>(shader_backend_.get());
|
||||
if (gpu) gpu->destroy();
|
||||
if (gpu != nullptr) {
|
||||
gpu->destroy();
|
||||
}
|
||||
#endif
|
||||
shader_backend_.reset();
|
||||
}
|
||||
|
||||
if (internal_texture_sdl_) SDL_DestroyTexture(internal_texture_sdl_);
|
||||
if (texture_) SDL_DestroyTexture(texture_);
|
||||
if (renderer_) SDL_DestroyRenderer(renderer_);
|
||||
if (window_) SDL_DestroyWindow(window_);
|
||||
if (internal_texture_sdl_ != nullptr) {
|
||||
SDL_DestroyTexture(internal_texture_sdl_);
|
||||
}
|
||||
if (texture_ != nullptr) {
|
||||
SDL_DestroyTexture(texture_);
|
||||
}
|
||||
if (renderer_ != nullptr) {
|
||||
SDL_DestroyRenderer(renderer_);
|
||||
}
|
||||
if (window_ != nullptr) {
|
||||
SDL_DestroyWindow(window_);
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::initShaders() {
|
||||
@@ -143,7 +154,9 @@ void Screen::initShaders() {
|
||||
// curtcircuiten cap al fallback SDL_Renderer.
|
||||
return;
|
||||
#else
|
||||
if (!Options::video.gpu_acceleration) return;
|
||||
if (!Options::video.gpu_acceleration) {
|
||||
return;
|
||||
}
|
||||
|
||||
shader_backend_ = std::make_unique<Rendering::SDL3GPUShader>();
|
||||
|
||||
@@ -165,13 +178,7 @@ void Screen::initShaders() {
|
||||
shader_backend_->setScalingMode(Options::video.scaling_mode);
|
||||
shader_backend_->setVSync(Options::video.vsync);
|
||||
shader_backend_->setTextureFilter(Options::video.texture_filter);
|
||||
shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3);
|
||||
shader_backend_->setDownscaleAlgo(Options::video.downscale_algo);
|
||||
|
||||
if (Options::video.supersampling) {
|
||||
shader_backend_->setOversample(3);
|
||||
}
|
||||
|
||||
shader_backend_->setStretch43(Options::video.aspect_ratio_4_3);
|
||||
shader_backend_->setInternalResolution(Options::video.internal_resolution);
|
||||
|
||||
// Resol el shader actiu des del config
|
||||
@@ -218,28 +225,28 @@ void Screen::present(Uint32* pixel_data) {
|
||||
// no trencar la selecció de l'usuari.
|
||||
Rendering::PostFXParams clean{};
|
||||
shader_backend_->setPostFXParams(clean);
|
||||
const auto prev_shader = shader_backend_->getActiveShader();
|
||||
if (prev_shader != Rendering::ShaderType::POSTFX) {
|
||||
const auto PREV_SHADER = shader_backend_->getActiveShader();
|
||||
if (PREV_SHADER != Rendering::ShaderType::POSTFX) {
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::POSTFX);
|
||||
}
|
||||
shader_backend_->uploadPixels(pixel_data, GAME_WIDTH, GAME_HEIGHT);
|
||||
shader_backend_->render();
|
||||
if (prev_shader != Rendering::ShaderType::POSTFX) {
|
||||
shader_backend_->setActiveShader(prev_shader);
|
||||
if (PREV_SHADER != Rendering::ShaderType::POSTFX) {
|
||||
shader_backend_->setActiveShader(PREV_SHADER);
|
||||
}
|
||||
} else {
|
||||
// Fallback SDL_Renderer. A mult=1, flux directe original: logical
|
||||
// Fallback SDL_Renderer. A MULT=1, flux directe original: logical
|
||||
// presentation (setada per applyFallbackPresentation) + scale mode de
|
||||
// texture_ segons l'opció. A mult>1, la còpia intermèdia crea la
|
||||
// texture_ segons l'opció. A MULT>1, la còpia intermèdia crea la
|
||||
// font ampliada (NN via GPU), i es presenta via logical presentation
|
||||
// a la mida de la font intermèdia.
|
||||
SDL_UpdateTexture(texture_, nullptr, pixel_data, GAME_WIDTH * sizeof(Uint32));
|
||||
|
||||
const int mult = Options::video.internal_resolution;
|
||||
if (mult > 1) {
|
||||
const int MULT = Options::video.internal_resolution;
|
||||
if (MULT > 1) {
|
||||
ensureFallbackInternalTexture();
|
||||
if (internal_texture_sdl_ != nullptr) {
|
||||
// Còpia NN a la textura intermèdia (mult·game). Sampler NN
|
||||
// Còpia NN a la textura intermèdia (MULT·game). Sampler NN
|
||||
// per construcció: volem píxels grans i nets.
|
||||
SDL_SetTextureScaleMode(texture_, SDL_SCALEMODE_NEAREST);
|
||||
SDL_SetRenderTarget(renderer_, internal_texture_sdl_);
|
||||
@@ -248,7 +255,7 @@ void Screen::present(Uint32* pixel_data) {
|
||||
SDL_SetRenderTarget(renderer_, nullptr);
|
||||
|
||||
// Filtre global al pas final → finestra (via logical presentation
|
||||
// que applyFallbackPresentation ja configura amb mida game·mult).
|
||||
// que applyFallbackPresentation ja configura amb mida game·MULT).
|
||||
SDL_ScaleMode final_scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
|
||||
? SDL_SCALEMODE_LINEAR
|
||||
: SDL_SCALEMODE_NEAREST;
|
||||
@@ -260,9 +267,9 @@ void Screen::present(Uint32* pixel_data) {
|
||||
}
|
||||
// Si la creació de la textura intermèdia ha fallat, caiem al path normal.
|
||||
}
|
||||
// mult=1 (o fallback-del-fallback): texture_ directament. El scale mode
|
||||
// MULT=1 (o fallback-del-fallback): texture_ directament. El scale mode
|
||||
// el manté applyFallbackPresentation — però el re-apliquem per si la
|
||||
// ruta mult>1 el va sobreescriure anteriorment.
|
||||
// ruta MULT>1 el va sobreescriure anteriorment.
|
||||
SDL_ScaleMode direct_scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
|
||||
? SDL_SCALEMODE_LINEAR
|
||||
: SDL_SCALEMODE_NEAREST;
|
||||
@@ -282,19 +289,25 @@ void Screen::toggleFullscreen() {
|
||||
}
|
||||
|
||||
void Screen::incZoom() {
|
||||
if (fullscreen_ || zoom_ >= max_zoom_) return;
|
||||
if (fullscreen_ || zoom_ >= max_zoom_) {
|
||||
return;
|
||||
}
|
||||
zoom_++;
|
||||
adjustWindowSize();
|
||||
}
|
||||
|
||||
void Screen::decZoom() {
|
||||
if (fullscreen_ || zoom_ <= 1) return;
|
||||
if (fullscreen_ || zoom_ <= 1) {
|
||||
return;
|
||||
}
|
||||
zoom_--;
|
||||
adjustWindowSize();
|
||||
}
|
||||
|
||||
void Screen::setZoom(int zoom) {
|
||||
if (zoom < 1 || zoom > max_zoom_ || fullscreen_) return;
|
||||
if (zoom < 1 || zoom > max_zoom_ || fullscreen_) {
|
||||
return;
|
||||
}
|
||||
zoom_ = zoom;
|
||||
adjustWindowSize();
|
||||
}
|
||||
@@ -306,22 +319,10 @@ void Screen::toggleShaders() {
|
||||
}
|
||||
}
|
||||
|
||||
auto Screen::toggleSupersampling() -> bool {
|
||||
// SS només té sentit amb shaders on i pipeline PostFX (el Lanczos downscale
|
||||
// i el camí SS s'apliquen al pas de PostFX; CRTPI fa el seu propi
|
||||
// submostreig intern i no usa aquesta via).
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
|
||||
if (!Options::video.shader_enabled) return false;
|
||||
if (shader_backend_->getActiveShader() != Rendering::ShaderType::POSTFX) return false;
|
||||
Options::video.supersampling = !Options::video.supersampling;
|
||||
shader_backend_->setOversample(Options::video.supersampling ? 3 : 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Screen::toggleAspectRatio() {
|
||||
Options::video.aspect_ratio_4_3 = !Options::video.aspect_ratio_4_3;
|
||||
if (shader_backend_) {
|
||||
shader_backend_->setStretch4_3(Options::video.aspect_ratio_4_3);
|
||||
shader_backend_->setStretch43(Options::video.aspect_ratio_4_3);
|
||||
} else {
|
||||
applyFallbackPresentation();
|
||||
}
|
||||
@@ -366,9 +367,11 @@ void Screen::cycleTextureFilter(int dir) {
|
||||
|
||||
void Screen::changeInternalResolution(int dir) {
|
||||
int next = Options::video.internal_resolution + (dir >= 0 ? 1 : -1);
|
||||
if (next < 1) next = 1;
|
||||
if (next > max_zoom_) next = max_zoom_;
|
||||
if (next == Options::video.internal_resolution) return;
|
||||
next = std::max(next, 1);
|
||||
next = std::min(next, max_zoom_);
|
||||
if (next == Options::video.internal_resolution) {
|
||||
return;
|
||||
}
|
||||
Options::video.internal_resolution = next;
|
||||
|
||||
// Propaga al backend actiu. Al fallback path, la textura es recrea al
|
||||
@@ -381,8 +384,12 @@ void Screen::changeInternalResolution(int dir) {
|
||||
}
|
||||
|
||||
auto Screen::nextShaderType() -> bool {
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
|
||||
if (!Options::video.shader_enabled) return false;
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
|
||||
return false;
|
||||
}
|
||||
if (!Options::video.shader_enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
|
||||
shader_backend_->setActiveShader(Rendering::ShaderType::CRTPI);
|
||||
@@ -397,16 +404,24 @@ auto Screen::nextShaderType() -> bool {
|
||||
}
|
||||
|
||||
auto Screen::nextPreset() -> bool {
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
|
||||
if (!Options::video.shader_enabled) return false;
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
|
||||
return false;
|
||||
}
|
||||
if (!Options::video.shader_enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
|
||||
if (Options::postfx_presets.empty()) return false;
|
||||
if (Options::postfx_presets.empty()) {
|
||||
return false;
|
||||
}
|
||||
Options::current_postfx_preset = (Options::current_postfx_preset + 1) % static_cast<int>(Options::postfx_presets.size());
|
||||
Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
|
||||
applyCurrentPostFXPreset();
|
||||
} else {
|
||||
if (Options::crtpi_presets.empty()) return false;
|
||||
if (Options::crtpi_presets.empty()) {
|
||||
return false;
|
||||
}
|
||||
Options::current_crtpi_preset = (Options::current_crtpi_preset + 1) % static_cast<int>(Options::crtpi_presets.size());
|
||||
Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
|
||||
applyCurrentCrtPiPreset();
|
||||
@@ -420,17 +435,25 @@ auto Screen::prevShaderType() -> bool {
|
||||
}
|
||||
|
||||
auto Screen::prevPreset() -> bool {
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return false;
|
||||
if (!Options::video.shader_enabled) return false;
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
|
||||
return false;
|
||||
}
|
||||
if (!Options::video.shader_enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
|
||||
if (Options::postfx_presets.empty()) return false;
|
||||
if (Options::postfx_presets.empty()) {
|
||||
return false;
|
||||
}
|
||||
int n = static_cast<int>(Options::postfx_presets.size());
|
||||
Options::current_postfx_preset = (Options::current_postfx_preset - 1 + n) % n;
|
||||
Options::video.current_postfx_preset = Options::postfx_presets[Options::current_postfx_preset].name;
|
||||
applyCurrentPostFXPreset();
|
||||
} else {
|
||||
if (Options::crtpi_presets.empty()) return false;
|
||||
if (Options::crtpi_presets.empty()) {
|
||||
return false;
|
||||
}
|
||||
int n = static_cast<int>(Options::crtpi_presets.size());
|
||||
Options::current_crtpi_preset = (Options::current_crtpi_preset - 1 + n) % n;
|
||||
Options::video.current_crtpi_preset = Options::crtpi_presets[Options::current_crtpi_preset].name;
|
||||
@@ -440,13 +463,17 @@ auto Screen::prevPreset() -> bool {
|
||||
}
|
||||
|
||||
auto Screen::getCurrentPresetName() const -> const char* {
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return "---";
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
|
||||
return "---";
|
||||
}
|
||||
if (shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX) {
|
||||
if (Options::current_postfx_preset < static_cast<int>(Options::postfx_presets.size()))
|
||||
if (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 < static_cast<int>(Options::crtpi_presets.size()))
|
||||
if (Options::current_crtpi_preset < static_cast<int>(Options::crtpi_presets.size())) {
|
||||
return Options::crtpi_presets[Options::current_crtpi_preset].name.c_str();
|
||||
}
|
||||
}
|
||||
return "---";
|
||||
}
|
||||
@@ -458,22 +485,30 @@ void Screen::setActiveShader(Rendering::ShaderType type) {
|
||||
}
|
||||
|
||||
void Screen::applyCurrentPostFXPreset() {
|
||||
if (!shader_backend_ || Options::postfx_presets.empty()) return;
|
||||
if (!shader_backend_ || Options::postfx_presets.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto& preset = Options::postfx_presets[Options::current_postfx_preset];
|
||||
Rendering::PostFXParams p;
|
||||
p.vignette = preset.vignette;
|
||||
p.scanlines = preset.scanlines;
|
||||
p.chroma = preset.chroma;
|
||||
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);
|
||||
}
|
||||
|
||||
void Screen::applyCurrentCrtPiPreset() {
|
||||
if (!shader_backend_ || Options::crtpi_presets.empty()) return;
|
||||
if (!shader_backend_ || Options::crtpi_presets.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto& preset = Options::crtpi_presets[Options::current_crtpi_preset];
|
||||
Rendering::CrtPiParams p;
|
||||
p.scanline_weight = preset.scanline_weight;
|
||||
@@ -498,12 +533,14 @@ auto Screen::isHardwareAccelerated() const -> bool {
|
||||
}
|
||||
|
||||
auto Screen::getActiveShaderName() const -> const char* {
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) return "SENSE GPU";
|
||||
if (!shader_backend_ || !shader_backend_->isHardwareAccelerated()) {
|
||||
return "SENSE GPU";
|
||||
}
|
||||
return shader_backend_->getActiveShader() == Rendering::ShaderType::POSTFX ? "POSTFX" : "CRT-PI";
|
||||
}
|
||||
|
||||
void Screen::updateRenderInfo() {
|
||||
static const Uint32 start_ticks = SDL_GetTicks();
|
||||
static const Uint32 START_TICKS = SDL_GetTicks();
|
||||
std::string driver = gpu_driver_.empty() ? "sdl" : toLower(gpu_driver_);
|
||||
|
||||
// Segment 0: FPS + driver (sempre visible)
|
||||
@@ -515,27 +552,24 @@ void Screen::updateRenderInfo() {
|
||||
shader_seg = " - " + toLower(getActiveShaderName()) + " " + toLower(getCurrentPresetName());
|
||||
}
|
||||
|
||||
// Segment 2: supersampling indicator
|
||||
const char* ss_seg = (Options::video.shader_enabled && Options::video.supersampling) ? " (ss)" : nullptr;
|
||||
|
||||
// Segment 3: hora (només si show_time)
|
||||
// Segment 2: hora (només si show_time)
|
||||
char time_buf[32] = {0};
|
||||
if (Options::render_info.show_time) {
|
||||
Uint32 elapsed = SDL_GetTicks() - start_ticks;
|
||||
Uint32 elapsed = SDL_GetTicks() - START_TICKS;
|
||||
int minutes = elapsed / 60000;
|
||||
int seconds = (elapsed / 1000) % 60;
|
||||
int centis = (elapsed / 10) % 100;
|
||||
snprintf(time_buf, sizeof(time_buf), " - %d:%02d.%02d", minutes, seconds, centis);
|
||||
}
|
||||
|
||||
// Dígits en mono a FPS (segment 0) i TEMPS (segment 3): els dígits canvien
|
||||
// Dígits en mono a FPS (segment 0) i TEMPS (segment 2): els dígits canvien
|
||||
// contínuament mentre els símbols del voltant ("fps", ":", ".", " - ") no
|
||||
Overlay::setRenderInfoSegments(
|
||||
fps_driver.c_str(),
|
||||
shader_seg.empty() ? nullptr : shader_seg.c_str(),
|
||||
ss_seg,
|
||||
time_buf[0] ? time_buf : nullptr,
|
||||
0b1001);
|
||||
(time_buf[0] != 0) ? time_buf : nullptr,
|
||||
nullptr,
|
||||
0b0101);
|
||||
}
|
||||
|
||||
void Screen::applyFallbackPresentation() {
|
||||
@@ -544,7 +578,9 @@ void Screen::applyFallbackPresentation() {
|
||||
SDL_ScaleMode scale = (Options::video.texture_filter == Options::TextureFilter::LINEAR)
|
||||
? SDL_SCALEMODE_LINEAR
|
||||
: SDL_SCALEMODE_NEAREST;
|
||||
if (texture_) SDL_SetTextureScaleMode(texture_, scale);
|
||||
if (texture_ != nullptr) {
|
||||
SDL_SetTextureScaleMode(texture_, scale);
|
||||
}
|
||||
|
||||
// Si 4:3 actiu, la finestra ja té aspect 4:3 (alçada × 1.2); STRETCH és
|
||||
// l'única opció viable al path fallback (el GPU path fa l'upscale 4:3 abans
|
||||
@@ -573,14 +609,16 @@ void Screen::applyFallbackPresentation() {
|
||||
}
|
||||
// Amb resolució interna N > 1, la mida lògica creix proporcionalment
|
||||
// perquè SDL scale des de 320·N × 200·N a la finestra — menys aggressive linear.
|
||||
const int mult = Options::video.internal_resolution < 1 ? 1 : Options::video.internal_resolution;
|
||||
SDL_SetRenderLogicalPresentation(renderer_, GAME_WIDTH * mult, GAME_HEIGHT * mult, mode);
|
||||
const int MULT = Options::video.internal_resolution < 1 ? 1 : Options::video.internal_resolution;
|
||||
SDL_SetRenderLogicalPresentation(renderer_, GAME_WIDTH * MULT, GAME_HEIGHT * MULT, mode);
|
||||
}
|
||||
|
||||
void Screen::ensureFallbackInternalTexture() {
|
||||
if (renderer_ == nullptr) return;
|
||||
const int mult = Options::video.internal_resolution;
|
||||
if (mult <= 1) {
|
||||
if (renderer_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
const int MULT = Options::video.internal_resolution;
|
||||
if (MULT <= 1) {
|
||||
// No cal textura intermèdia — recicla si la teníem.
|
||||
if (internal_texture_sdl_ != nullptr) {
|
||||
SDL_DestroyTexture(internal_texture_sdl_);
|
||||
@@ -589,7 +627,9 @@ void Screen::ensureFallbackInternalTexture() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (internal_texture_sdl_ != nullptr && internal_texture_mult_ == mult) return;
|
||||
if (internal_texture_sdl_ != nullptr && internal_texture_mult_ == MULT) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (internal_texture_sdl_ != nullptr) {
|
||||
SDL_DestroyTexture(internal_texture_sdl_);
|
||||
@@ -598,15 +638,15 @@ void Screen::ensureFallbackInternalTexture() {
|
||||
internal_texture_sdl_ = SDL_CreateTexture(renderer_,
|
||||
SDL_PIXELFORMAT_ABGR8888,
|
||||
SDL_TEXTUREACCESS_TARGET,
|
||||
GAME_WIDTH * mult,
|
||||
GAME_HEIGHT * mult);
|
||||
GAME_WIDTH * MULT,
|
||||
GAME_HEIGHT * MULT);
|
||||
if (internal_texture_sdl_ == nullptr) {
|
||||
std::cerr << "Screen: failed to create fallback internal texture (×" << mult << "): "
|
||||
std::cerr << "Screen: failed to create fallback internal texture (×" << MULT << "): "
|
||||
<< SDL_GetError() << '\n';
|
||||
internal_texture_mult_ = 0;
|
||||
return;
|
||||
}
|
||||
internal_texture_mult_ = mult;
|
||||
internal_texture_mult_ = MULT;
|
||||
}
|
||||
|
||||
void Screen::adjustWindowSize() {
|
||||
@@ -620,11 +660,11 @@ void Screen::adjustWindowSize() {
|
||||
void Screen::calculateMaxZoom() {
|
||||
SDL_DisplayID display = SDL_GetPrimaryDisplay();
|
||||
const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(display);
|
||||
if (mode) {
|
||||
if (mode != nullptr) {
|
||||
int max_w = mode->w / GAME_WIDTH;
|
||||
int max_h = mode->h / GAME_HEIGHT;
|
||||
max_zoom_ = (max_w < max_h) ? max_w : max_h;
|
||||
if (max_zoom_ < 1) max_zoom_ = 1;
|
||||
max_zoom_ = std::max(max_zoom_, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ class Screen {
|
||||
// no es complia. Els callers (F-keys, menú) poden suprimir notificacions
|
||||
// o feedback quan la crida no ha tingut efecte.
|
||||
void toggleShaders();
|
||||
auto toggleSupersampling() -> bool; // false si GPU off / shaders off / actiu != POSTFX
|
||||
void toggleAspectRatio();
|
||||
void cycleScalingMode(int dir); // Cicla DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER
|
||||
void toggleVSync();
|
||||
@@ -71,7 +70,7 @@ class Screen {
|
||||
void applyFallbackPresentation(); // Logical presentation + scale mode per al path SDL_Renderer
|
||||
void ensureFallbackInternalTexture(); // Recrea internal_texture_sdl_ si cal (fallback path)
|
||||
|
||||
static std::unique_ptr<Screen> instance_;
|
||||
static std::unique_ptr<Screen> instance;
|
||||
|
||||
SDL_Window* window_{nullptr};
|
||||
SDL_Renderer* renderer_{nullptr};
|
||||
|
||||
@@ -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,23 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __APPLE__
|
||||
|
||||
// Fragment shader d'upscale (mostreig directe). S'usa per al pas de resolució
|
||||
// interna (scene_texture → internal_texture) quan internal_resolution > 1.
|
||||
namespace Rendering::Msl {
|
||||
|
||||
inline constexpr const char* kUpscaleFrag = R"(
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
struct VertOut { float4 pos [[position]]; float2 uv; };
|
||||
fragment float4 upscale_fs(VertOut in [[stage_in]],
|
||||
texture2d<float> scene [[texture(0)]],
|
||||
sampler smp [[sampler(0)]])
|
||||
{
|
||||
return scene.sample(smp, in.uv);
|
||||
}
|
||||
)";
|
||||
|
||||
} // namespace Rendering::Msl
|
||||
|
||||
#endif // __APPLE__
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,20 +7,28 @@
|
||||
|
||||
// PostFX uniforms pushed to fragment stage each frame.
|
||||
// Must match the MSL struct and GLSL uniform block layout.
|
||||
// 12 floats = 48 bytes — meets Metal/Vulkan 16-byte alignment requirement.
|
||||
// 16 floats = 64 bytes (4 × vec4) — meets Metal/Vulkan 16-byte alignment.
|
||||
struct PostFXUniforms {
|
||||
// vec4 #0
|
||||
float vignette_strength; // 0 = none, ~0.8 = subtle
|
||||
float chroma_strength; // 0 = off, ~0.2 = subtle chromatic aberration
|
||||
float chroma_min; // aberració cromàtica mínima (sempre present)
|
||||
float scanline_strength; // 0 = off, 1 = full
|
||||
float screen_height; // logical height in pixels (used by bleeding effect)
|
||||
float mask_strength; // 0 = off, 1 = full phosphor dot mask
|
||||
float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction
|
||||
float curvature; // 0 = flat, 1 = max barrel distortion
|
||||
float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding
|
||||
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||||
float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f)
|
||||
float oversample; // supersampling factor (1.0 = off, 3.0 = 3×SS)
|
||||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz — keep struct at 48 bytes (3 × 16)
|
||||
// vec4 #1
|
||||
float mask_strength; // 0 = off, 1 = full phosphor dot mask
|
||||
float gamma_strength; // 0 = off, 1 = full gamma 2.4/2.2 correction
|
||||
float curvature; // 0 = flat, 1 = max barrel distortion
|
||||
float bleeding; // 0 = off, 1 = max NTSC chrominance bleeding
|
||||
// vec4 #2
|
||||
float pixel_scale; // physical pixels per logical pixel (vh / tex_height_)
|
||||
float time; // seconds since SDL init (SDL_GetTicks() / 1000.0f)
|
||||
float flicker; // 0 = off, 1 = phosphor flicker ~50 Hz
|
||||
float chroma_max; // si == chroma_min queda estàtic; si != pulsa sinusoidalment
|
||||
// vec4 #3 — paràmetres de forma de les scanlines (exposats per preset)
|
||||
float scan_dark_ratio; // fracció de subfila fosca (1/3 = 0.333 per defecte)
|
||||
float scan_dark_floor; // brillantor de la subfila fosca (0.42 per defecte)
|
||||
float scan_edge_soft; // suavitzat de la transició (0 = step dur, 1 = 1px físic)
|
||||
float pad3;
|
||||
};
|
||||
|
||||
// CrtPi uniforms pushed to fragment stage each frame.
|
||||
@@ -49,15 +57,6 @@ struct CrtPiUniforms {
|
||||
float texture_height; // Alto del canvas en píxeles (inyectado en render)
|
||||
};
|
||||
|
||||
// Downscale uniforms pushed to the Lanczos downscale fragment stage.
|
||||
// 1 int + 3 floats = 16 bytes — meets Metal/Vulkan alignment.
|
||||
struct DownscaleUniforms {
|
||||
int algorithm; // 0 = Lanczos2 (ventana 2), 1 = Lanczos3 (ventana 3)
|
||||
float pad0;
|
||||
float pad1;
|
||||
float pad2;
|
||||
};
|
||||
|
||||
namespace Rendering {
|
||||
|
||||
/**
|
||||
@@ -78,7 +77,7 @@ namespace Rendering {
|
||||
const std::string& fragment_source) -> bool override;
|
||||
|
||||
void render() override;
|
||||
void setTextureSize(float width, float height) override {}
|
||||
void setTextureSize(float /*width*/, float /*height*/) override {}
|
||||
void cleanup() final; // Libera pipeline/texturas pero mantiene el device vivo
|
||||
void destroy(); // Limpieza completa (device + swapchain); llamar solo al cerrar
|
||||
[[nodiscard]] auto isHardwareAccelerated() const -> bool override { return is_initialized_; }
|
||||
@@ -99,15 +98,6 @@ namespace Rendering {
|
||||
// Selecciona el mode de presentació lògica (DISABLED/STRETCH/LETTERBOX/OVERSCAN/INTEGER)
|
||||
void setScalingMode(Options::ScalingMode mode) override;
|
||||
|
||||
// Establece factor de supersampling (1 = off, 3 = 3×SS)
|
||||
void setOversample(int factor) override;
|
||||
|
||||
// Selecciona algoritmo de downscale: 0=bilinear legacy, 1=Lanczos2, 2=Lanczos3
|
||||
void setDownscaleAlgo(int algo) override;
|
||||
|
||||
// Devuelve las dimensiones de la textura de supersampling (0,0 si SS desactivado)
|
||||
[[nodiscard]] auto getSsTextureSize() const -> std::pair<int, int> override;
|
||||
|
||||
// Selecciona el shader de post-procesado activo (POSTFX o CRTPI)
|
||||
void setActiveShader(ShaderType type) override;
|
||||
|
||||
@@ -118,8 +108,8 @@ namespace Rendering {
|
||||
[[nodiscard]] auto getActiveShader() const -> ShaderType override { return active_shader_; }
|
||||
|
||||
// Estirament vertical 4:3 (fusionat amb l'upscale pass)
|
||||
void setStretch4_3(bool enabled) override;
|
||||
[[nodiscard]] auto isStretch4_3() const -> bool override { return stretch_4_3_; }
|
||||
void setStretch43(bool enabled) override;
|
||||
[[nodiscard]] auto isStretch43() const -> bool override { return stretch_4_3_; }
|
||||
|
||||
// Filtre de textura global (sempre aplicat, independent de 4:3)
|
||||
void setTextureFilter(Options::TextureFilter filter) override {
|
||||
@@ -146,39 +136,62 @@ namespace Rendering {
|
||||
Uint32 num_uniform_buffers) -> SDL_GPUShader*;
|
||||
|
||||
auto createPipeline() -> bool;
|
||||
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
|
||||
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
|
||||
auto recreateScaledTexture(int factor) -> bool; // Recrea scaled_texture_ para factor dado
|
||||
auto recreateInternalTexture() -> bool; // Recrea internal_texture_ (res interna × N)
|
||||
static auto calcSsFactor(float zoom) -> int; // Primer múltiplo de 3 >= zoom (mín 3)
|
||||
auto createCrtPiPipeline() -> bool; // Pipeline dedicado para el shader CrtPi
|
||||
auto reinitTexturesAndBuffer() -> bool; // Recrea scene_texture_ y upload_buffer_
|
||||
auto recreateInternalTexture() -> bool; // Recrea internal_texture_ (res interna × N)
|
||||
// Devuelve el mejor present mode disponible: IMMEDIATE > MAILBOX > VSYNC
|
||||
[[nodiscard]] auto bestPresentMode(bool vsync) const -> SDL_GPUPresentMode;
|
||||
|
||||
SDL_Window* window_ = nullptr;
|
||||
SDL_GPUDevice* device_ = nullptr;
|
||||
SDL_GPUGraphicsPipeline* pipeline_ = nullptr; // PostFX pass (→ swapchain o → postfx_texture_)
|
||||
SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr; // CrtPi pass (→ swapchain directo, sin SS)
|
||||
SDL_GPUGraphicsPipeline* postfx_offscreen_pipeline_ = nullptr; // PostFX → postfx_texture_ (B8G8R8A8, solo con Lanczos)
|
||||
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale pass (solo con SS)
|
||||
SDL_GPUGraphicsPipeline* downscale_pipeline_ = nullptr; // Lanczos downscale (solo con SS + algo > 0)
|
||||
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del joc (game_width_ × game_height_)
|
||||
SDL_GPUTexture* internal_texture_ = nullptr; // Resolució interna ampliada (game·N × game·N), si N>1
|
||||
SDL_GPUTexture* scaled_texture_ = nullptr; // Upscale target (game×factor, amb 4:3 si actiu)
|
||||
SDL_GPUTexture* postfx_texture_ = nullptr; // PostFX output a resolució escalada, sols amb Lanczos
|
||||
SDL_GPUGraphicsPipeline* pipeline_ = nullptr; // PostFX pass → swapchain
|
||||
SDL_GPUGraphicsPipeline* crtpi_pipeline_ = nullptr; // CrtPi pass → swapchain
|
||||
SDL_GPUGraphicsPipeline* upscale_pipeline_ = nullptr; // Upscale per al pas de resolució interna
|
||||
SDL_GPUTexture* scene_texture_ = nullptr; // Canvas del joc (game_width_ × game_height_)
|
||||
SDL_GPUTexture* internal_texture_ = nullptr; // Resolució interna ampliada (game·N × game·N), si N>1
|
||||
SDL_GPUTransferBuffer* upload_buffer_ = nullptr;
|
||||
SDL_GPUSampler* sampler_ = nullptr; // NEAREST
|
||||
SDL_GPUSampler* linear_sampler_ = nullptr; // LINEAR
|
||||
SDL_GPUSampler* linear_sampler_ = nullptr; // LINEAR (per texture_filter_linear_)
|
||||
|
||||
PostFXUniforms uniforms_{.vignette_strength = 0.6F, .chroma_strength = 0.15F, .scanline_strength = 0.7F, .screen_height = 200.0F, .pixel_scale = 1.0F, .oversample = 1.0F};
|
||||
CrtPiUniforms crtpi_uniforms_{.scanline_weight = 6.0F, .scanline_gap_brightness = 0.12F, .bloom_factor = 3.5F, .input_gamma = 2.4F, .output_gamma = 2.2F, .mask_brightness = 0.80F, .curvature_x = 0.05F, .curvature_y = 0.10F, .mask_type = 2, .enable_scanlines = 1, .enable_multisample = 1, .enable_gamma = 1};
|
||||
PostFXUniforms uniforms_{
|
||||
.vignette_strength = 0.6F,
|
||||
.chroma_min = 0.15F,
|
||||
.scanline_strength = 0.7F,
|
||||
.screen_height = 200.0F,
|
||||
.mask_strength = 0.0F,
|
||||
.gamma_strength = 0.0F,
|
||||
.curvature = 0.0F,
|
||||
.bleeding = 0.0F,
|
||||
.pixel_scale = 1.0F,
|
||||
.time = 0.0F,
|
||||
.flicker = 0.0F,
|
||||
.chroma_max = 0.15F,
|
||||
.scan_dark_ratio = 0.333F,
|
||||
.scan_dark_floor = 0.42F,
|
||||
.scan_edge_soft = 1.0F,
|
||||
.pad3 = 0.0F};
|
||||
CrtPiUniforms crtpi_uniforms_{
|
||||
.scanline_weight = 6.0F,
|
||||
.scanline_gap_brightness = 0.12F,
|
||||
.bloom_factor = 3.5F,
|
||||
.input_gamma = 2.4F,
|
||||
.output_gamma = 2.2F,
|
||||
.mask_brightness = 0.80F,
|
||||
.curvature_x = 0.05F,
|
||||
.curvature_y = 0.10F,
|
||||
.mask_type = 2,
|
||||
.enable_scanlines = 1,
|
||||
.enable_multisample = 1,
|
||||
.enable_gamma = 1,
|
||||
.enable_curvature = 0,
|
||||
.enable_sharper = 0,
|
||||
.texture_width = 0.0F,
|
||||
.texture_height = 0.0F};
|
||||
ShaderType active_shader_ = ShaderType::POSTFX; // Shader de post-procesado activo
|
||||
|
||||
int game_width_ = 0; // Dimensions originals del canvas
|
||||
int game_height_ = 0;
|
||||
int ss_factor_ = 0; // Factor SS activo (3, 6, 9...) o 0 si SS desactivado
|
||||
int oversample_ = 1; // SS on/off (1 = off, >1 = on)
|
||||
int downscale_algo_ = 1; // 0 = bilinear legacy, 1 = Lanczos2, 2 = Lanczos3
|
||||
int internal_res_ = 1; // Multiplicador de resolució interna (1 = off)
|
||||
int internal_res_ = 1; // Multiplicador de resolució interna (1 = off)
|
||||
std::string driver_name_;
|
||||
std::string preferred_driver_; // Driver preferido; vacío = auto (SDL elige)
|
||||
bool is_initialized_ = false;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
@@ -10,7 +11,7 @@
|
||||
namespace Rendering {
|
||||
|
||||
/** @brief Identificador del shader de post-procesado activo */
|
||||
enum class ShaderType { POSTFX,
|
||||
enum class ShaderType : std::uint8_t { POSTFX,
|
||||
CRTPI };
|
||||
|
||||
/**
|
||||
@@ -20,12 +21,19 @@ namespace Rendering {
|
||||
struct PostFXParams {
|
||||
float vignette = 0.0F; // Intensidad de la viñeta
|
||||
float scanlines = 0.0F; // Intensidad de las scanlines
|
||||
float chroma = 0.0F; // Aberración cromática
|
||||
// Aberració cromàtica — varia entre min i max via sinusoidal; si coincideixen
|
||||
// queda estàtica. min > 0 garanteix que la imatge mai sigui lliure de chroma.
|
||||
float chroma_min = 0.0F;
|
||||
float chroma_max = 0.0F;
|
||||
float mask = 0.0F; // Máscara de fósforo RGB
|
||||
float gamma = 0.0F; // Corrección gamma (blend 0=off, 1=full)
|
||||
float curvature = 0.0F; // Curvatura barrel CRT
|
||||
float bleeding = 0.0F; // Sangrado de color NTSC
|
||||
float flicker = 0.0F; // Parpadeo de fósforo CRT ~50 Hz
|
||||
// Forma de les scanlines — 3 subpíxels per fila lògica per defecte.
|
||||
float scan_dark_ratio = 0.333F; // fracció obscura (1/3)
|
||||
float scan_dark_floor = 0.42F; // brillantor subfila fosca
|
||||
float scan_edge_soft = 1.0F; // 0 = step dur; 1 = suavitzat 1 px físic
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -111,27 +119,6 @@ namespace Rendering {
|
||||
*/
|
||||
virtual void setScalingMode(Options::ScalingMode /*mode*/) {}
|
||||
|
||||
/**
|
||||
* @brief Establece el factor de supersampling (1 = off, 3 = 3× SS)
|
||||
* Con factor > 1, la textura GPU se crea a game×factor resolución y
|
||||
* las scanlines se hornean en CPU (uploadPixels). El sampler usa LINEAR.
|
||||
*/
|
||||
virtual void setOversample(int /*factor*/) {}
|
||||
|
||||
/**
|
||||
* @brief Selecciona el algoritmo de downscale tras el PostFX (SS activo).
|
||||
* 0 = bilinear legacy (comportamiento actual, sin textura intermedia),
|
||||
* 1 = Lanczos2 (ventana 2, ~25 muestras), 2 = Lanczos3 (ventana 3, ~49 muestras).
|
||||
*/
|
||||
virtual void setDownscaleAlgo(int /*algo*/) {}
|
||||
[[nodiscard]] virtual auto getDownscaleAlgo() const -> int { return 0; }
|
||||
|
||||
/**
|
||||
* @brief Devuelve las dimensiones de la textura de supersampling.
|
||||
* @return Par (ancho, alto) en píxeles; (0, 0) si SS está desactivado.
|
||||
*/
|
||||
[[nodiscard]] virtual auto getSsTextureSize() const -> std::pair<int, int> { return {0, 0}; }
|
||||
|
||||
/**
|
||||
* @brief Verifica si el backend está usando aceleración por hardware
|
||||
* @return true si usa aceleración (OpenGL/Metal/Vulkan)
|
||||
@@ -170,8 +157,8 @@ namespace Rendering {
|
||||
* @brief Activa/desactiva estirament vertical 4:3 (200→240 línies efectives).
|
||||
* Només afecta el viewport, no les textures ni els shaders.
|
||||
*/
|
||||
virtual void setStretch4_3(bool /*enabled*/) {}
|
||||
[[nodiscard]] virtual auto isStretch4_3() const -> bool { return false; }
|
||||
virtual void setStretch43(bool /*enabled*/) {}
|
||||
[[nodiscard]] virtual auto isStretch43() const -> bool { return false; }
|
||||
|
||||
/**
|
||||
* @brief Filtre de textura global per a l'upscale final (sempre aplicat).
|
||||
|
||||
+134
-175
@@ -1,5 +1,6 @@
|
||||
#include "core/rendering/text.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -12,7 +13,8 @@
|
||||
|
||||
// Forward declarations de gif.h (inclòs des de jdraw8.cpp, no es pot incloure dos vegades)
|
||||
struct rgb;
|
||||
extern unsigned char* LoadGif(unsigned char* data, unsigned short* w, unsigned short* h);
|
||||
// NOLINTNEXTLINE(readability-identifier-naming) — exportat per external/gif.h, no controlem el nom.
|
||||
extern auto LoadGif(unsigned char* data, unsigned short* w, unsigned short* h) -> unsigned char*;
|
||||
|
||||
Text::Text(const char* fnt_file, const char* gif_file) {
|
||||
loadBitmap(gif_file);
|
||||
@@ -23,7 +25,9 @@ Text::Text(const char* fnt_file, const char* gif_file) {
|
||||
|
||||
auto Text::nextCodepoint(const char*& ptr) -> uint32_t {
|
||||
auto byte = static_cast<uint8_t>(*ptr);
|
||||
if (byte == 0) return 0;
|
||||
if (byte == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t cp = 0;
|
||||
int extra = 0;
|
||||
@@ -47,7 +51,9 @@ auto Text::nextCodepoint(const char*& ptr) -> uint32_t {
|
||||
ptr++;
|
||||
for (int i = 0; i < extra; i++) {
|
||||
auto cont = static_cast<uint8_t>(*ptr);
|
||||
if ((cont & 0xC0) != 0x80) return 0xFFFD;
|
||||
if ((cont & 0xC0) != 0x80) {
|
||||
return 0xFFFD;
|
||||
}
|
||||
cp = (cp << 6) | (cont & 0x3F);
|
||||
ptr++;
|
||||
}
|
||||
@@ -71,7 +77,9 @@ void Text::loadFont(const char* fnt_file) {
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
// Ignora comentaris i línies buides
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
if (line.empty() || line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Elimina comentaris inline
|
||||
auto comment_pos = line.find('#');
|
||||
@@ -133,9 +141,10 @@ void Text::loadBitmap(const char* gif_file) {
|
||||
int w = raw[6] | (raw[7] << 8);
|
||||
int h = raw[8] | (raw[9] << 8);
|
||||
|
||||
unsigned short gw = 0, gh = 0;
|
||||
unsigned short gw = 0;
|
||||
unsigned short gh = 0;
|
||||
Uint8* pixels = LoadGif(raw, &gw, &gh);
|
||||
if (!pixels) {
|
||||
if (pixels == nullptr) {
|
||||
std::cerr << "Text: unable to decode GIF: " << gif_file << '\n';
|
||||
return;
|
||||
}
|
||||
@@ -150,50 +159,57 @@ void Text::loadBitmap(const char* gif_file) {
|
||||
|
||||
// --- Renderitzat ---
|
||||
|
||||
void Text::draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const {
|
||||
if (bitmap_.empty() || !pixel_data) return;
|
||||
auto Text::resolveGlyph(uint32_t cp) const -> const GlyphInfo* {
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it != glyphs_.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
it = glyphs_.find('?');
|
||||
return (it != glyphs_.end()) ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
|
||||
while (*ptr) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) break;
|
||||
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) {
|
||||
it = glyphs_.find('?');
|
||||
if (it == glyphs_.end()) {
|
||||
cursor_x += box_width_;
|
||||
void Text::blitGlyph(Uint32* pixel_data, int dst_x, int dst_y, const GlyphInfo& glyph, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const {
|
||||
const int GY_START = std::max(0, clip_y_min - dst_y);
|
||||
const int GY_END = std::min(box_height_, clip_y_max - dst_y);
|
||||
const int GX_START = std::max(0, clip_x_min - dst_x);
|
||||
const int GX_END = std::min(glyph.w, clip_x_max - dst_x);
|
||||
for (int gy = GY_START; gy < GY_END; gy++) {
|
||||
const int SRC_Y = glyph.y + gy;
|
||||
if (SRC_Y >= bitmap_height_) {
|
||||
continue;
|
||||
}
|
||||
const int DST_ROW = dst_y + gy;
|
||||
for (int gx = GX_START; gx < GX_END; gx++) {
|
||||
const int SRC_X = glyph.x + gx;
|
||||
if (SRC_X >= bitmap_width_) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const auto& glyph = it->second;
|
||||
|
||||
// Pinta glifo pixel a pixel
|
||||
for (int gy = 0; gy < box_height_; gy++) {
|
||||
int dst_y = y + gy;
|
||||
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) continue;
|
||||
|
||||
for (int gx = 0; gx < glyph.w; gx++) {
|
||||
int dst_x = cursor_x + gx;
|
||||
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) continue;
|
||||
|
||||
int src_x = glyph.x + gx;
|
||||
int src_y = glyph.y + gy;
|
||||
|
||||
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) continue;
|
||||
|
||||
Uint8 pixel = bitmap_[src_x + src_y * bitmap_width_];
|
||||
// Píxel no transparent (índex 0 és fons típicament)
|
||||
if (pixel != 0) {
|
||||
pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color;
|
||||
}
|
||||
const Uint8 PIXEL = bitmap_[SRC_X + (SRC_Y * bitmap_width_)];
|
||||
if (PIXEL != 0) {
|
||||
pixel_data[(dst_x + gx) + (DST_ROW * SCREEN_WIDTH)] = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cursor_x += glyph.w + 1; // +1 kerning
|
||||
void Text::draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const {
|
||||
if (bitmap_.empty() || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
const GlyphInfo* glyph = resolveGlyph(cp);
|
||||
if (glyph == nullptr) {
|
||||
cursor_x += box_width_;
|
||||
continue;
|
||||
}
|
||||
blitGlyph(pixel_data, cursor_x, y, *glyph, color, 0, SCREEN_WIDTH, 0, SCREEN_HEIGHT);
|
||||
cursor_x += glyph->w + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,152 +220,82 @@ void Text::drawCentered(Uint32* pixel_data, int y, const char* text, Uint32 colo
|
||||
}
|
||||
|
||||
void Text::drawClipped(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const {
|
||||
if (bitmap_.empty() || !pixel_data) return;
|
||||
|
||||
// Descart ràpid si el glifo sencer cau fora verticalment
|
||||
if (y + box_height_ <= clip_y_min || y >= clip_y_max) return;
|
||||
|
||||
if (bitmap_.empty() || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
if (y + box_height_ <= clip_y_min || y >= clip_y_max) {
|
||||
return;
|
||||
}
|
||||
const int X_MIN = std::max(0, clip_x_min);
|
||||
const int X_MAX = std::min(SCREEN_WIDTH, clip_x_max);
|
||||
const int Y_MIN = std::max(0, clip_y_min);
|
||||
const int Y_MAX = std::min(SCREEN_HEIGHT, clip_y_max);
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
|
||||
while (*ptr) {
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) break;
|
||||
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) {
|
||||
it = glyphs_.find('?');
|
||||
if (it == glyphs_.end()) {
|
||||
cursor_x += box_width_;
|
||||
continue;
|
||||
}
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto& glyph = it->second;
|
||||
|
||||
// Si el glifo està completament fora del clip horitzontal, salta
|
||||
if (cursor_x + glyph.w <= clip_x_min || cursor_x >= clip_x_max) {
|
||||
cursor_x += glyph.w + 1;
|
||||
const GlyphInfo* glyph = resolveGlyph(cp);
|
||||
if (glyph == nullptr) {
|
||||
cursor_x += box_width_;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int gy = 0; gy < box_height_; gy++) {
|
||||
int dst_y = y + gy;
|
||||
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) continue;
|
||||
if (dst_y < clip_y_min || dst_y >= clip_y_max) continue;
|
||||
|
||||
for (int gx = 0; gx < glyph.w; gx++) {
|
||||
int dst_x = cursor_x + gx;
|
||||
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) continue;
|
||||
if (dst_x < clip_x_min || dst_x >= clip_x_max) continue;
|
||||
|
||||
int src_x = glyph.x + gx;
|
||||
int src_y = glyph.y + gy;
|
||||
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) continue;
|
||||
|
||||
Uint8 pixel = bitmap_[src_x + src_y * bitmap_width_];
|
||||
if (pixel != 0) {
|
||||
pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color;
|
||||
}
|
||||
}
|
||||
if (cursor_x + glyph->w > X_MIN && cursor_x < X_MAX) {
|
||||
blitGlyph(pixel_data, cursor_x, y, *glyph, color, X_MIN, X_MAX, Y_MIN, Y_MAX);
|
||||
}
|
||||
|
||||
cursor_x += glyph.w + 1;
|
||||
cursor_x += glyph->w + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void Text::drawMono(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int cell_w) const {
|
||||
if (bitmap_.empty() || !pixel_data) return;
|
||||
|
||||
if (bitmap_.empty() || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
|
||||
while (*ptr) {
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) break;
|
||||
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) {
|
||||
it = glyphs_.find('?');
|
||||
if (it == glyphs_.end()) {
|
||||
cursor_x += cell_w;
|
||||
continue;
|
||||
}
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto& glyph = it->second;
|
||||
// Centra el glif dins la cel·la
|
||||
int glyph_x = cursor_x + (cell_w - glyph.w) / 2;
|
||||
|
||||
for (int gy = 0; gy < box_height_; gy++) {
|
||||
int dst_y = y + gy;
|
||||
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) continue;
|
||||
|
||||
for (int gx = 0; gx < glyph.w; gx++) {
|
||||
int dst_x = glyph_x + gx;
|
||||
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) continue;
|
||||
|
||||
int src_x = glyph.x + gx;
|
||||
int src_y = glyph.y + gy;
|
||||
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) continue;
|
||||
|
||||
Uint8 pixel = bitmap_[src_x + src_y * bitmap_width_];
|
||||
if (pixel != 0) {
|
||||
pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color;
|
||||
}
|
||||
}
|
||||
const GlyphInfo* glyph = resolveGlyph(cp);
|
||||
if (glyph == nullptr) {
|
||||
cursor_x += cell_w;
|
||||
continue;
|
||||
}
|
||||
|
||||
const int GLYPH_X = cursor_x + ((cell_w - glyph->w) / 2);
|
||||
blitGlyph(pixel_data, GLYPH_X, y, *glyph, color, 0, SCREEN_WIDTH, 0, SCREEN_HEIGHT);
|
||||
cursor_x += cell_w;
|
||||
}
|
||||
}
|
||||
|
||||
void Text::drawMonoDigits(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int digit_cell_w) const {
|
||||
if (bitmap_.empty() || !pixel_data) return;
|
||||
|
||||
if (bitmap_.empty() || (pixel_data == nullptr)) {
|
||||
return;
|
||||
}
|
||||
const char* ptr = text;
|
||||
int cursor_x = x;
|
||||
bool first = true;
|
||||
|
||||
while (*ptr) {
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) break;
|
||||
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) {
|
||||
it = glyphs_.find('?');
|
||||
if (it == glyphs_.end()) {
|
||||
if (!first) cursor_x += 1;
|
||||
cursor_x += box_width_;
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto& glyph = it->second;
|
||||
bool is_digit = (cp >= '0' && cp <= '9');
|
||||
|
||||
if (!first) cursor_x += 1; // kerning
|
||||
|
||||
int glyph_x = is_digit ? cursor_x + (digit_cell_w - glyph.w) / 2 : cursor_x;
|
||||
|
||||
for (int gy = 0; gy < box_height_; gy++) {
|
||||
int dst_y = y + gy;
|
||||
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) continue;
|
||||
for (int gx = 0; gx < glyph.w; gx++) {
|
||||
int dst_x = glyph_x + gx;
|
||||
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) continue;
|
||||
int src_x = glyph.x + gx;
|
||||
int src_y = glyph.y + gy;
|
||||
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) continue;
|
||||
Uint8 pixel = bitmap_[src_x + src_y * bitmap_width_];
|
||||
if (pixel != 0) {
|
||||
pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color;
|
||||
}
|
||||
}
|
||||
if (!first) {
|
||||
cursor_x += 1; // kerning
|
||||
}
|
||||
|
||||
cursor_x += is_digit ? digit_cell_w : glyph.w;
|
||||
const GlyphInfo* glyph = resolveGlyph(cp);
|
||||
if (glyph == nullptr) {
|
||||
cursor_x += box_width_;
|
||||
first = false;
|
||||
continue;
|
||||
}
|
||||
const bool IS_DIGIT = (cp >= '0' && cp <= '9');
|
||||
const int GLYPH_X = IS_DIGIT ? cursor_x + ((digit_cell_w - glyph->w) / 2) : cursor_x;
|
||||
blitGlyph(pixel_data, GLYPH_X, y, *glyph, color, 0, SCREEN_WIDTH, 0, SCREEN_HEIGHT);
|
||||
cursor_x += IS_DIGIT ? digit_cell_w : glyph->w;
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
@@ -358,32 +304,41 @@ auto Text::widthMonoDigits(const char* text, int digit_cell_w) const -> int {
|
||||
const char* ptr = text;
|
||||
int w = 0;
|
||||
bool first = true;
|
||||
while (*ptr) {
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) break;
|
||||
if (!first) w += 1; // kerning
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
if (!first) {
|
||||
w += 1; // kerning
|
||||
}
|
||||
first = false;
|
||||
bool is_digit = (cp >= '0' && cp <= '9');
|
||||
if (is_digit) {
|
||||
w += digit_cell_w;
|
||||
} else {
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) it = glyphs_.find('?');
|
||||
if (it != glyphs_.end())
|
||||
if (it == glyphs_.end()) {
|
||||
it = glyphs_.find('?');
|
||||
}
|
||||
if (it != glyphs_.end()) {
|
||||
w += it->second.w;
|
||||
else
|
||||
} else {
|
||||
w += box_width_;
|
||||
}
|
||||
}
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
auto Text::widthMono(const char* text, int cell_w) const -> int {
|
||||
auto Text::widthMono(const char* text, int cell_w) -> int {
|
||||
const char* ptr = text;
|
||||
int count = 0;
|
||||
while (*ptr) {
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) break;
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return count * cell_w;
|
||||
@@ -394,16 +349,20 @@ auto Text::width(const char* text) const -> int {
|
||||
int w = 0;
|
||||
bool first = true;
|
||||
|
||||
while (*ptr) {
|
||||
while (*ptr != 0) {
|
||||
uint32_t cp = nextCodepoint(ptr);
|
||||
if (cp == 0) break;
|
||||
if (cp == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto it = glyphs_.find(cp);
|
||||
if (it == glyphs_.end()) {
|
||||
it = glyphs_.find('?');
|
||||
}
|
||||
|
||||
if (!first) w += 1; // kerning
|
||||
if (!first) {
|
||||
w += 1; // kerning
|
||||
}
|
||||
first = false;
|
||||
|
||||
if (it != glyphs_.end()) {
|
||||
|
||||
@@ -28,7 +28,7 @@ class Text {
|
||||
// Calcula ancho en píxeles d'un text
|
||||
[[nodiscard]] auto width(const char* text) const -> int;
|
||||
// Amplada mono: nombre de codepoints × cell_w
|
||||
[[nodiscard]] auto widthMono(const char* text, int cell_w) const -> int;
|
||||
[[nodiscard]] static auto widthMono(const char* text, int cell_w) -> int;
|
||||
// Amplada mono-dígits: amplada natural, però substituint els dígits per digit_cell_w
|
||||
[[nodiscard]] auto widthMonoDigits(const char* text, int digit_cell_w) const -> int;
|
||||
[[nodiscard]] auto charHeight() const -> int { return box_height_; }
|
||||
@@ -57,6 +57,13 @@ class Text {
|
||||
void loadFont(const char* fnt_file);
|
||||
void loadBitmap(const char* gif_file);
|
||||
|
||||
// Resolt un codepoint al GlyphInfo corresponent o al fallback '?'.
|
||||
// Retorna nullptr si ni el codepoint ni el fallback existeixen.
|
||||
[[nodiscard]] auto resolveGlyph(uint32_t cp) const -> const GlyphInfo*;
|
||||
// Pinta un glif a (dst_x, dst_y) amb clipping per finestra.
|
||||
// Si la finestra és tota la pantalla, passar clip_x_min=0, clip_x_max=SCREEN_WIDTH, idem y.
|
||||
void blitGlyph(Uint32* pixel_data, int dst_x, int dst_y, const GlyphInfo& glyph, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const;
|
||||
|
||||
static constexpr int SCREEN_WIDTH = 320;
|
||||
static constexpr int SCREEN_HEIGHT = 200;
|
||||
};
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
// ni inline, així que no podem tornar-lo a incloure aquí. Ens fiem de les
|
||||
// declaracions extern dels símbols que ens calen (linkatge C++ normal,
|
||||
// igual que fa text.cpp).
|
||||
extern unsigned char* LoadGif(unsigned char* data, unsigned short* w, unsigned short* h);
|
||||
extern unsigned char* LoadPalette(unsigned char* data);
|
||||
// NOLINTBEGIN(readability-identifier-naming) — símbols externs de gif.h.
|
||||
extern auto LoadGif(unsigned char* data, unsigned short* w, unsigned short* h) -> unsigned char*;
|
||||
extern auto LoadPalette(unsigned char* data) -> unsigned char*;
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
|
||||
namespace Resource {
|
||||
|
||||
@@ -33,7 +35,7 @@ namespace Resource {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
auto Cache::getMusic(const std::string& name) -> JA_Music_t* {
|
||||
auto Cache::getMusic(const std::string& name) -> Ja::Music* {
|
||||
auto it = std::ranges::find_if(musics_, [&](const auto& m) { return m.name == name; });
|
||||
if (it != musics_.end()) {
|
||||
return it->music.get();
|
||||
@@ -42,7 +44,7 @@ namespace Resource {
|
||||
throw std::runtime_error("Music not found: " + name);
|
||||
}
|
||||
|
||||
auto Cache::getSound(const std::string& name) -> JA_Sound_t* {
|
||||
auto Cache::getSound(const std::string& name) -> Ja::Sound* {
|
||||
auto it = std::ranges::find_if(sounds_, [&](const auto& s) { return s.name == name; });
|
||||
if (it != sounds_.end()) {
|
||||
return it->sound.get();
|
||||
@@ -79,7 +81,7 @@ namespace Resource {
|
||||
}
|
||||
|
||||
void Cache::calculateTotal() {
|
||||
auto* list = List::get();
|
||||
const auto* list = List::get();
|
||||
total_count_ = static_cast<int>(
|
||||
list->getListByType(List::Type::MUSIC).size() +
|
||||
list->getListByType(List::Type::SOUND).size() +
|
||||
@@ -90,7 +92,9 @@ namespace Resource {
|
||||
}
|
||||
|
||||
auto Cache::getProgress() const -> float {
|
||||
if (total_count_ == 0) return 1.0F;
|
||||
if (total_count_ == 0) {
|
||||
return 1.0F;
|
||||
}
|
||||
return static_cast<float>(loaded_count_) / static_cast<float>(total_count_);
|
||||
}
|
||||
|
||||
@@ -101,67 +105,64 @@ namespace Resource {
|
||||
std::cout << "Resource::Cache: precarregant " << total_count_ << " assets\n";
|
||||
}
|
||||
|
||||
auto Cache::loadStep(int budget_ms) -> bool {
|
||||
if (stage_ == LoadStage::DONE) return true;
|
||||
void Cache::stepEachInList(List::Type type, const std::function<void()>& clear_fn, LoadStage next, const std::function<void(size_t)>& load_fn) {
|
||||
auto items = List::get()->getListByType(type);
|
||||
if (stage_index_ == 0) {
|
||||
clear_fn();
|
||||
}
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = next;
|
||||
stage_index_ = 0;
|
||||
return;
|
||||
}
|
||||
load_fn(stage_index_++);
|
||||
}
|
||||
|
||||
const Uint64 start_ns = SDL_GetTicksNS();
|
||||
const Uint64 budget_ns = static_cast<Uint64>(budget_ms) * 1'000'000ULL;
|
||||
auto* list = List::get();
|
||||
void Cache::stepTextFiles() {
|
||||
auto data_items = List::get()->getListByType(List::Type::DATA);
|
||||
auto font_items = List::get()->getListByType(List::Type::FONT);
|
||||
auto items = data_items;
|
||||
items.insert(items.end(), font_items.begin(), font_items.end());
|
||||
if (stage_index_ == 0) {
|
||||
text_files_.clear();
|
||||
}
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::DONE;
|
||||
stage_index_ = 0;
|
||||
std::cout << "Resource::Cache: precarrega completada (" << loaded_count_ << "/" << total_count_ << ")\n";
|
||||
return;
|
||||
}
|
||||
loadOneTextFile(stage_index_++);
|
||||
}
|
||||
|
||||
auto Cache::loadStep(int budget_ms) -> bool {
|
||||
if (stage_ == LoadStage::DONE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const Uint64 START_NS = SDL_GetTicksNS();
|
||||
const Uint64 BUDGET_NS = static_cast<Uint64>(budget_ms) * 1'000'000ULL;
|
||||
|
||||
while (stage_ != LoadStage::DONE) {
|
||||
switch (stage_) {
|
||||
case LoadStage::MUSICS: {
|
||||
auto items = list->getListByType(List::Type::MUSIC);
|
||||
if (stage_index_ == 0) musics_.clear();
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::SOUNDS;
|
||||
stage_index_ = 0;
|
||||
break;
|
||||
}
|
||||
loadOneMusic(stage_index_++);
|
||||
case LoadStage::MUSICS:
|
||||
stepEachInList(List::Type::MUSIC, [this] { musics_.clear(); }, LoadStage::SOUNDS, [this](size_t i) { loadOneMusic(i); });
|
||||
break;
|
||||
}
|
||||
case LoadStage::SOUNDS: {
|
||||
auto items = list->getListByType(List::Type::SOUND);
|
||||
if (stage_index_ == 0) sounds_.clear();
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::BITMAPS;
|
||||
stage_index_ = 0;
|
||||
break;
|
||||
}
|
||||
loadOneSound(stage_index_++);
|
||||
case LoadStage::SOUNDS:
|
||||
stepEachInList(List::Type::SOUND, [this] { sounds_.clear(); }, LoadStage::BITMAPS, [this](size_t i) { loadOneSound(i); });
|
||||
break;
|
||||
}
|
||||
case LoadStage::BITMAPS: {
|
||||
auto items = list->getListByType(List::Type::BITMAP);
|
||||
if (stage_index_ == 0) surfaces_.clear();
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::TEXT_FILES;
|
||||
stage_index_ = 0;
|
||||
break;
|
||||
}
|
||||
loadOneBitmap(stage_index_++);
|
||||
case LoadStage::BITMAPS:
|
||||
stepEachInList(List::Type::BITMAP, [this] { surfaces_.clear(); }, LoadStage::TEXT_FILES, [this](size_t i) { loadOneBitmap(i); });
|
||||
break;
|
||||
}
|
||||
case LoadStage::TEXT_FILES: {
|
||||
auto data_items = list->getListByType(List::Type::DATA);
|
||||
auto font_items = list->getListByType(List::Type::FONT);
|
||||
auto items = data_items;
|
||||
items.insert(items.end(), font_items.begin(), font_items.end());
|
||||
if (stage_index_ == 0) text_files_.clear();
|
||||
if (stage_index_ >= items.size()) {
|
||||
stage_ = LoadStage::DONE;
|
||||
stage_index_ = 0;
|
||||
std::cout << "Resource::Cache: precarrega completada (" << loaded_count_ << "/" << total_count_ << ")\n";
|
||||
break;
|
||||
}
|
||||
loadOneTextFile(stage_index_++);
|
||||
case LoadStage::TEXT_FILES:
|
||||
stepTextFiles();
|
||||
break;
|
||||
}
|
||||
case LoadStage::DONE:
|
||||
break;
|
||||
}
|
||||
if ((SDL_GetTicksNS() - start_ns) >= budget_ns) break;
|
||||
if ((SDL_GetTicksNS() - START_NS) >= BUDGET_NS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return stage_ == LoadStage::DONE;
|
||||
@@ -178,12 +179,12 @@ namespace Resource {
|
||||
std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n';
|
||||
return;
|
||||
}
|
||||
JA_Music_t* music = JA_LoadMusic(bytes.data(), static_cast<Uint32>(bytes.size()), path.c_str());
|
||||
Ja::Music* music = Ja::loadMusic(bytes.data(), static_cast<Uint32>(bytes.size()), path.c_str());
|
||||
if (music == nullptr) {
|
||||
std::cerr << "Resource::Cache: JA_LoadMusic ha fallat per " << path << '\n';
|
||||
std::cerr << "Resource::Cache: Ja::loadMusic ha fallat per " << path << '\n';
|
||||
return;
|
||||
}
|
||||
musics_.push_back(MusicResource{.name = name, .music = std::unique_ptr<JA_Music_t, MusicDeleter>(music)});
|
||||
musics_.push_back(MusicResource{.name = name, .music = std::unique_ptr<Ja::Music, MusicDeleter>(music)});
|
||||
++loaded_count_;
|
||||
std::cout << " [music ] " << name << '\n';
|
||||
}
|
||||
@@ -199,12 +200,12 @@ namespace Resource {
|
||||
std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n';
|
||||
return;
|
||||
}
|
||||
JA_Sound_t* sound = JA_LoadSound(bytes.data(), static_cast<uint32_t>(bytes.size()));
|
||||
Ja::Sound* sound = Ja::loadSound(bytes.data(), static_cast<uint32_t>(bytes.size()));
|
||||
if (sound == nullptr) {
|
||||
std::cerr << "Resource::Cache: JA_LoadSound ha fallat per " << path << '\n';
|
||||
std::cerr << "Resource::Cache: Ja::loadSound ha fallat per " << path << '\n';
|
||||
return;
|
||||
}
|
||||
sounds_.push_back(SoundResource{.name = name, .sound = std::unique_ptr<JA_Sound_t, SoundDeleter>(sound)});
|
||||
sounds_.push_back(SoundResource{.name = name, .sound = std::unique_ptr<Ja::Sound, SoundDeleter>(sound)});
|
||||
++loaded_count_;
|
||||
std::cout << " [sound ] " << name << '\n';
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/resources/resource_list.hpp"
|
||||
#include "core/resources/resource_types.hpp"
|
||||
|
||||
namespace Resource {
|
||||
@@ -26,8 +29,8 @@ namespace Resource {
|
||||
auto operator=(const Cache&) -> Cache& = delete;
|
||||
|
||||
// Getters: throw runtime_error si el nom no existeix al cache.
|
||||
auto getMusic(const std::string& name) -> JA_Music_t*;
|
||||
auto getSound(const std::string& name) -> JA_Sound_t*;
|
||||
auto getMusic(const std::string& name) -> Ja::Music*;
|
||||
auto getSound(const std::string& name) -> Ja::Sound*;
|
||||
auto getSurfacePixels(const std::string& name) -> const std::vector<Uint8>&;
|
||||
auto getPaletteBytes(const std::string& name) -> const std::vector<Uint8>&;
|
||||
auto getTextFile(const std::string& name) -> const std::vector<uint8_t>&;
|
||||
@@ -42,7 +45,7 @@ namespace Resource {
|
||||
private:
|
||||
Cache() = default;
|
||||
|
||||
enum class LoadStage {
|
||||
enum class LoadStage : std::uint8_t {
|
||||
MUSICS,
|
||||
SOUNDS,
|
||||
BITMAPS,
|
||||
@@ -55,6 +58,8 @@ namespace Resource {
|
||||
void loadOneSound(size_t index);
|
||||
void loadOneBitmap(size_t index);
|
||||
void loadOneTextFile(size_t index);
|
||||
void stepEachInList(List::Type type, const std::function<void()>& clear_fn, LoadStage next, const std::function<void(size_t)>& load_fn);
|
||||
void stepTextFiles();
|
||||
|
||||
std::vector<MusicResource> musics_;
|
||||
std::vector<SoundResource> sounds_;
|
||||
|
||||
@@ -9,30 +9,34 @@
|
||||
namespace ResourceHelper {
|
||||
|
||||
namespace {
|
||||
ResourcePack pack_;
|
||||
bool pack_loaded_ = false;
|
||||
bool fallback_enabled_ = true;
|
||||
ResourcePack pack_obj;
|
||||
bool pack_loaded = false;
|
||||
bool fallback_enabled = true;
|
||||
|
||||
auto readFromDisk(const std::string& relative_path) -> std::vector<uint8_t> {
|
||||
const std::string full = std::string(file_getresourcefolder()) + relative_path;
|
||||
std::ifstream file(full, std::ios::binary | std::ios::ate);
|
||||
if (!file) return {};
|
||||
const std::string FULL = std::string(Jf::getResourceFolder()) + relative_path;
|
||||
std::ifstream file(FULL, std::ios::binary | std::ios::ate);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::streamsize size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
std::vector<uint8_t> data(size);
|
||||
if (!file.read(reinterpret_cast<char*>(data.data()), size)) return {};
|
||||
if (!file.read(reinterpret_cast<char*>(data.data()), size)) {
|
||||
return {};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
auto initializeResourceSystem(const std::string& pack_file, bool enable_fallback) -> bool {
|
||||
fallback_enabled_ = enable_fallback;
|
||||
pack_loaded_ = pack_.loadPack(pack_file);
|
||||
fallback_enabled = enable_fallback;
|
||||
pack_loaded = pack_obj.loadPack(pack_file);
|
||||
|
||||
if (pack_loaded_) {
|
||||
std::cout << "ResourceHelper: pack loaded (" << pack_.getResourceCount()
|
||||
if (pack_loaded) {
|
||||
std::cout << "ResourceHelper: pack loaded (" << pack_obj.getResourceCount()
|
||||
<< " entries) from " << pack_file << '\n';
|
||||
} else if (enable_fallback) {
|
||||
std::cout << "ResourceHelper: no pack at " << pack_file
|
||||
@@ -46,22 +50,22 @@ namespace ResourceHelper {
|
||||
}
|
||||
|
||||
void shutdownResourceSystem() {
|
||||
pack_.clear();
|
||||
pack_loaded_ = false;
|
||||
pack_obj.clear();
|
||||
pack_loaded = false;
|
||||
}
|
||||
|
||||
auto loadFile(const std::string& relative_path) -> std::vector<uint8_t> {
|
||||
if (pack_loaded_ && pack_.hasResource(relative_path)) {
|
||||
return pack_.getResource(relative_path);
|
||||
if (pack_loaded && pack_obj.hasResource(relative_path)) {
|
||||
return pack_obj.getResource(relative_path);
|
||||
}
|
||||
if (fallback_enabled_) {
|
||||
if (fallback_enabled) {
|
||||
return readFromDisk(relative_path);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
auto hasPack() -> bool {
|
||||
return pack_loaded_;
|
||||
return pack_loaded;
|
||||
}
|
||||
|
||||
} // namespace ResourceHelper
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include <vector>
|
||||
|
||||
// API d'alt nivell per a llegir recursos. Prova primer el pack (si està
|
||||
// carregat), després cau al fitxer solt dins `file_getresourcefolder()`
|
||||
// carregat), després cau al fitxer solt dins `Jf::getResourceFolder()`
|
||||
// si el fallback està activat.
|
||||
namespace ResourceHelper {
|
||||
|
||||
|
||||
@@ -95,11 +95,21 @@ namespace Resource {
|
||||
}
|
||||
|
||||
auto List::parseAssetType(const std::string& type_str) -> Type {
|
||||
if (type_str == "DATA") return Type::DATA;
|
||||
if (type_str == "BITMAP") return Type::BITMAP;
|
||||
if (type_str == "MUSIC") return Type::MUSIC;
|
||||
if (type_str == "SOUND") return Type::SOUND;
|
||||
if (type_str == "FONT") return Type::FONT;
|
||||
if (type_str == "DATA") {
|
||||
return Type::DATA;
|
||||
}
|
||||
if (type_str == "BITMAP") {
|
||||
return Type::BITMAP;
|
||||
}
|
||||
if (type_str == "MUSIC") {
|
||||
return Type::MUSIC;
|
||||
}
|
||||
if (type_str == "SOUND") {
|
||||
return Type::SOUND;
|
||||
}
|
||||
if (type_str == "FONT") {
|
||||
return Type::FONT;
|
||||
}
|
||||
throw std::runtime_error("Unknown asset type: " + type_str);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -13,7 +14,7 @@ namespace Resource {
|
||||
// iterar per categoria a l'hora de carregar.
|
||||
class List {
|
||||
public:
|
||||
enum class Type : int {
|
||||
enum class Type : std::uint8_t {
|
||||
DATA,
|
||||
BITMAP,
|
||||
MUSIC,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <numeric>
|
||||
|
||||
const std::string ResourcePack::DEFAULT_ENCRYPT_KEY = "AEE_RESOURCES__2026";
|
||||
|
||||
@@ -21,15 +22,13 @@ ResourcePack::~ResourcePack() {
|
||||
|
||||
auto ResourcePack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
|
||||
// djb2-like hash, seed 0x12345678 (idèntic a CCAE).
|
||||
uint32_t checksum = 0x12345678;
|
||||
for (unsigned char b : data) {
|
||||
checksum = ((checksum << 5) + checksum) + b;
|
||||
}
|
||||
return checksum;
|
||||
return std::accumulate(data.begin(), data.end(), uint32_t{0x12345678}, [](uint32_t acc, unsigned char b) { return ((acc << 5) + acc) + b; });
|
||||
}
|
||||
|
||||
void ResourcePack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
|
||||
if (key.empty()) return;
|
||||
if (key.empty()) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < data.size(); ++i) {
|
||||
data[i] ^= static_cast<uint8_t>(key[i % key.length()]);
|
||||
}
|
||||
@@ -161,23 +160,25 @@ auto ResourcePack::addDirectory(const std::string& directory) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(directory)) {
|
||||
if (!entry.is_regular_file()) continue;
|
||||
|
||||
std::string filepath = entry.path().string();
|
||||
std::string filename = std::filesystem::relative(entry.path(), directory).string();
|
||||
std::ranges::replace(filename, '\\', '/');
|
||||
|
||||
if (!addFile(filename, filepath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
namespace fs = std::filesystem;
|
||||
return std::all_of(fs::recursive_directory_iterator(directory),
|
||||
fs::recursive_directory_iterator{},
|
||||
[&](const fs::directory_entry& entry) {
|
||||
if (!entry.is_regular_file()) {
|
||||
return true;
|
||||
}
|
||||
std::string filepath = entry.path().string();
|
||||
std::string filename = fs::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()) return {};
|
||||
if (it == resources_.end()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const ResourceEntry& entry = it->second;
|
||||
if (entry.offset + entry.size > data_.size()) {
|
||||
|
||||
@@ -8,38 +8,39 @@
|
||||
#include <vector>
|
||||
|
||||
// Forward declarations to keep this header light.
|
||||
struct JA_Music_t;
|
||||
struct JA_Sound_t;
|
||||
|
||||
void JA_DeleteMusic(JA_Music_t* music);
|
||||
void JA_DeleteSound(JA_Sound_t* sound);
|
||||
namespace Ja {
|
||||
struct Music;
|
||||
struct Sound;
|
||||
void deleteMusic(Music* music);
|
||||
void deleteSound(Sound* sound);
|
||||
} // namespace Ja
|
||||
|
||||
namespace Resource {
|
||||
|
||||
struct MusicDeleter {
|
||||
void operator()(JA_Music_t* music) const noexcept {
|
||||
void operator()(Ja::Music* music) const noexcept {
|
||||
if (music != nullptr) {
|
||||
JA_DeleteMusic(music);
|
||||
Ja::deleteMusic(music);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct SoundDeleter {
|
||||
void operator()(JA_Sound_t* sound) const noexcept {
|
||||
void operator()(Ja::Sound* sound) const noexcept {
|
||||
if (sound != nullptr) {
|
||||
JA_DeleteSound(sound);
|
||||
Ja::deleteSound(sound);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct MusicResource {
|
||||
std::string name;
|
||||
std::unique_ptr<JA_Music_t, MusicDeleter> music;
|
||||
std::unique_ptr<Ja::Music, MusicDeleter> music;
|
||||
};
|
||||
|
||||
struct SoundResource {
|
||||
std::string name;
|
||||
std::unique_ptr<JA_Sound_t, SoundDeleter> sound;
|
||||
std::unique_ptr<Ja::Sound, SoundDeleter> sound;
|
||||
};
|
||||
|
||||
// Una entrada BITMAP descodifica un GIF i emmagatzema els seus
|
||||
|
||||
+196
-225
@@ -20,107 +20,104 @@
|
||||
#include "game/info.hpp"
|
||||
#include "game/modulegame.hpp"
|
||||
#include "game/options.hpp"
|
||||
#include "scenes/banner_scene.hpp"
|
||||
#include "scenes/boot_loader_scene.hpp"
|
||||
#include "scenes/credits_scene.hpp"
|
||||
#include "scenes/intro_new_logo_scene.hpp"
|
||||
#include "scenes/intro_scene.hpp"
|
||||
#include "scenes/menu_scene.hpp"
|
||||
#include "scenes/mort_scene.hpp"
|
||||
#include "scenes/scene.hpp"
|
||||
#include "scenes/scene_registry.hpp"
|
||||
#include "scenes/secreta_scene.hpp"
|
||||
#include "scenes/slides_scene.hpp"
|
||||
#include "game/scenes/banner_scene.hpp"
|
||||
#include "game/scenes/boot_loader_scene.hpp"
|
||||
#include "game/scenes/credits_scene.hpp"
|
||||
#include "game/scenes/intro_new_logo_scene.hpp"
|
||||
#include "game/scenes/intro_scene.hpp"
|
||||
#include "game/scenes/menu_scene.hpp"
|
||||
#include "game/scenes/mort_scene.hpp"
|
||||
#include "game/scenes/scene.hpp"
|
||||
#include "game/scenes/scene_registry.hpp"
|
||||
#include "game/scenes/secreta_scene.hpp"
|
||||
#include "game/scenes/slides_scene.hpp"
|
||||
|
||||
// Cheats del joc original — declarats a jinput.cpp
|
||||
extern void JI_moveCheats(Uint8 new_key);
|
||||
|
||||
std::unique_ptr<Director> Director::instance_;
|
||||
std::unique_ptr<Director> Director::instance;
|
||||
|
||||
Director::~Director() = default;
|
||||
|
||||
void Director::initGameContext() {
|
||||
info::ctx.num_habitacio = Options::game.habitacio_inicial;
|
||||
info::ctx.num_piramide = Options::game.piramide_inicial;
|
||||
info::ctx.diners = Options::game.diners_inicial;
|
||||
info::ctx.diamants = Options::game.diamants_inicial;
|
||||
info::ctx.vida = Options::game.vides;
|
||||
info::ctx.momies = 0;
|
||||
info::ctx.nou_personatge = false;
|
||||
info::ctx.pepe_activat = false;
|
||||
Info::ctx.num_habitacio = Options::game.habitacio_inicial;
|
||||
Info::ctx.num_piramide = Options::game.piramide_inicial;
|
||||
Info::ctx.diners = Options::game.diners_inicial;
|
||||
Info::ctx.diamants = Options::game.diamants_inicial;
|
||||
Info::ctx.vida = Options::game.vides;
|
||||
Info::ctx.momies = 0;
|
||||
Info::ctx.nou_personatge = false;
|
||||
Info::ctx.pepe_activat = false;
|
||||
|
||||
FILE* ini = fopen("trick.ini", "rb");
|
||||
if (ini != nullptr) {
|
||||
info::ctx.nou_personatge = true;
|
||||
Info::ctx.nou_personatge = true;
|
||||
fclose(ini);
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<scenes::Scene> Director::createNextScene() {
|
||||
auto Director::createNextScene() const -> std::unique_ptr<Scenes::Scene> {
|
||||
// Mentre el Resource::Cache no haja acabat de precarregar, executem
|
||||
// el BootLoaderScene — pinta una barra de progrés i avança la
|
||||
// càrrega per pressupost de temps. Quan acaba, retorna i tornem ací
|
||||
// amb el cache plenament disponible per a la resta d'escenes.
|
||||
if (Resource::Cache::get() != nullptr && !Resource::Cache::get()->isLoadDone()) {
|
||||
return std::make_unique<scenes::BootLoaderScene>();
|
||||
return std::make_unique<Scenes::BootLoaderScene>();
|
||||
}
|
||||
if (game_state_ == 0) {
|
||||
// Gameplay. ModuleGame és una scenes::Scene des de la Phase A.
|
||||
// Gameplay. ModuleGame és una Scenes::Scene des de la Phase A.
|
||||
return std::make_unique<ModuleGame>();
|
||||
}
|
||||
// game_state_ == 1: dispatch al registry per num_piramide. Replica
|
||||
// del redirect que el vell ModuleSequence::Go() feia: si el jugador
|
||||
// arriba a la Secreta (6) sense prou diners, salta als slides de
|
||||
// fracàs (7) abans de buscar l'escena al registry.
|
||||
if (info::ctx.num_piramide == 6 && info::ctx.diners < 200) {
|
||||
info::ctx.num_piramide = 7;
|
||||
if (Info::ctx.num_piramide == 6 && Info::ctx.diners < 200) {
|
||||
Info::ctx.num_piramide = 7;
|
||||
}
|
||||
return scenes::SceneRegistry::instance().tryCreate(info::ctx.num_piramide);
|
||||
return Scenes::SceneRegistry::instance().tryCreate(Info::ctx.num_piramide);
|
||||
}
|
||||
|
||||
void Director::init() {
|
||||
instance_ = std::unique_ptr<Director>(new Director());
|
||||
instance = std::unique_ptr<Director>(new Director());
|
||||
Gamepad::init();
|
||||
|
||||
// Registre d'escenes. Cada entrada = un state_key (`num_piramide`)
|
||||
// amb una factory de `scenes::Scene`. iterate() consulta aquest
|
||||
// amb una factory de `Scenes::Scene`. iterate() consulta aquest
|
||||
// registry per a tots els states de seqüència (game_state_ == 1); si
|
||||
// una clau no apareix ací, Director surt ordenadament.
|
||||
auto& registry = scenes::SceneRegistry::instance();
|
||||
registry.registerScene(0, [] { return std::make_unique<scenes::MenuScene>(); });
|
||||
registry.registerScene(100, [] { return std::make_unique<scenes::MortScene>(); });
|
||||
auto& registry = Scenes::SceneRegistry::instance();
|
||||
registry.registerScene(0, [] { return std::make_unique<Scenes::MenuScene>(); });
|
||||
registry.registerScene(100, [] { return std::make_unique<Scenes::MortScene>(); });
|
||||
// BannerScene cobreix les piràmides 2..5 (el vell doBanner decideix
|
||||
// pel switch intern llegint info::ctx.num_piramide).
|
||||
// pel switch intern llegint Info::ctx.num_piramide).
|
||||
for (int p = 2; p <= 5; ++p) {
|
||||
registry.registerScene(p, [] { return std::make_unique<scenes::BannerScene>(); });
|
||||
registry.registerScene(p, [] { return std::make_unique<Scenes::BannerScene>(); });
|
||||
}
|
||||
// SlidesScene cobreix els dos states on el vell `doSlides` s'invocava:
|
||||
// - num_piramide == 1: slides narratius inicials (entrada al joc)
|
||||
// - num_piramide == 7: slides de fracàs (ve del redirect 6→7 quan
|
||||
// l'usuari no té prou diners per a la Secreta)
|
||||
registry.registerScene(1, [] { return std::make_unique<scenes::SlidesScene>(); });
|
||||
registry.registerScene(7, [] { return std::make_unique<scenes::SlidesScene>(); });
|
||||
registry.registerScene(6, [] { return std::make_unique<scenes::SecretaScene>(); });
|
||||
registry.registerScene(8, [] { return std::make_unique<scenes::CreditsScene>(); });
|
||||
registry.registerScene(1, [] { return std::make_unique<Scenes::SlidesScene>(); });
|
||||
registry.registerScene(7, [] { return std::make_unique<Scenes::SlidesScene>(); });
|
||||
registry.registerScene(6, [] { return std::make_unique<Scenes::SecretaScene>(); });
|
||||
registry.registerScene(8, [] { return std::make_unique<Scenes::CreditsScene>(); });
|
||||
// State 255 (intro): dues variants segons `Options::game.use_new_logo`.
|
||||
// La factory tria a runtime — així es pot togglar des del menú sense
|
||||
// re-registrar. Les dues escenes construeixen una IntroSpritesScene
|
||||
// com a sub-escena per a la part d'animacions de sprites.
|
||||
registry.registerScene(255, []() -> std::unique_ptr<scenes::Scene> {
|
||||
registry.registerScene(255, []() -> std::unique_ptr<Scenes::Scene> {
|
||||
if (Options::game.use_new_logo) {
|
||||
return std::make_unique<scenes::IntroNewLogoScene>();
|
||||
return std::make_unique<Scenes::IntroNewLogoScene>();
|
||||
}
|
||||
return std::make_unique<scenes::IntroScene>();
|
||||
return std::make_unique<Scenes::IntroScene>();
|
||||
});
|
||||
}
|
||||
|
||||
void Director::destroy() {
|
||||
Gamepad::destroy();
|
||||
instance_.reset();
|
||||
instance.reset();
|
||||
}
|
||||
|
||||
auto Director::get() -> Director* {
|
||||
return instance_.get();
|
||||
return instance.get();
|
||||
}
|
||||
|
||||
void Director::togglePause() {
|
||||
@@ -139,120 +136,104 @@ void Director::setup() {
|
||||
has_frame_ = false;
|
||||
}
|
||||
|
||||
bool Director::iterate() {
|
||||
void Director::applyRestart() {
|
||||
restart_requested_ = false;
|
||||
Audio::get()->stopMusic();
|
||||
Audio::get()->stopAllSounds();
|
||||
initGameContext();
|
||||
Info::ctx.num_piramide = 255;
|
||||
current_scene_.reset();
|
||||
game_state_ = 1;
|
||||
has_frame_ = false;
|
||||
Menu::close();
|
||||
Ji::setInputBlocked(false);
|
||||
}
|
||||
|
||||
void Director::maybeStartTitleCredits() {
|
||||
static bool credits_triggered_ = false;
|
||||
if (credits_triggered_ || Info::ctx.num_piramide != 0) {
|
||||
return;
|
||||
}
|
||||
if (Options::game.show_title_credits) {
|
||||
Overlay::startCredits();
|
||||
}
|
||||
credits_triggered_ = true;
|
||||
}
|
||||
|
||||
auto Director::tickActiveScene() -> bool {
|
||||
if (current_scene_ && (current_scene_->done() || Jg::quitting())) {
|
||||
game_state_ = current_scene_->nextState();
|
||||
current_scene_.reset();
|
||||
}
|
||||
if (!current_scene_) {
|
||||
if (game_state_ == -1 || Jg::quitting()) {
|
||||
return false;
|
||||
}
|
||||
current_scene_ = createNextScene();
|
||||
if (!current_scene_) {
|
||||
return false;
|
||||
}
|
||||
current_scene_->onEnter();
|
||||
last_tick_ms_ = SDL_GetTicks();
|
||||
}
|
||||
|
||||
Ji::update();
|
||||
const Uint32 NOW = SDL_GetTicks();
|
||||
const int DELTA_MS = static_cast<int>(NOW - last_tick_ms_);
|
||||
last_tick_ms_ = NOW;
|
||||
current_scene_->tick(DELTA_MS);
|
||||
|
||||
Jd8::flip();
|
||||
std::memcpy(game_frame_, Jd8::getFramebuffer(), sizeof(game_frame_));
|
||||
has_frame_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto Director::iterate() -> bool {
|
||||
if (quit_requested_) {
|
||||
JG_QuitSignal();
|
||||
current_scene_.reset(); // destrueix l'escena actual ordenadament
|
||||
Jg::quitSignal();
|
||||
current_scene_.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reinici "suau": processat al començament del frame per no manipular
|
||||
// l'escena des d'una lambda del menú mentre encara s'està executant.
|
||||
if (restart_requested_) {
|
||||
restart_requested_ = false;
|
||||
Audio::get()->stopMusic();
|
||||
Audio::get()->stopAllSounds();
|
||||
// Reinicialitza info::ctx des d'Options (vides, diners, diamants...)
|
||||
// en lloc de ctx.reset() pla que deixaria vida=0 → jugador mort.
|
||||
initGameContext();
|
||||
// Força l'intro independentment de `piramide_inicial` (que pot estar
|
||||
// configurat a una piràmide intermèdia per a proves ràpides).
|
||||
info::ctx.num_piramide = 255;
|
||||
current_scene_.reset();
|
||||
game_state_ = 1; // 1 = dispatch via SceneRegistry per num_piramide
|
||||
has_frame_ = false;
|
||||
Menu::close();
|
||||
JI_SetInputBlocked(false); // el menú ho havia bloquejat — cal desfer-ho
|
||||
applyRestart();
|
||||
}
|
||||
|
||||
if (!context_initialized_) {
|
||||
initGameContext();
|
||||
context_initialized_ = true;
|
||||
}
|
||||
|
||||
constexpr Uint32 FRAME_MS_VSYNC = 16; // ~60 FPS amb VSync
|
||||
constexpr Uint32 FRAME_MS_NO_VSYNC = 4; // ~250 FPS sense VSync (límit superior)
|
||||
|
||||
const Uint32 frame_start = SDL_GetTicks();
|
||||
constexpr Uint32 FRAME_MS_VSYNC = 16;
|
||||
constexpr Uint32 FRAME_MS_NO_VSYNC = 4;
|
||||
const Uint32 FRAME_START = SDL_GetTicks();
|
||||
|
||||
Gamepad::update();
|
||||
KeyRemap::update();
|
||||
GlobalInputs::handle();
|
||||
Mouse::updateCursorVisibility();
|
||||
|
||||
// Bombeig de l'àudio: reomple l'stream de música i para els canals
|
||||
// drenats. Substituïx el callback de SDL_AddTimer de la versió
|
||||
// antiga — imprescindible per al port a emscripten.
|
||||
Audio::update();
|
||||
|
||||
// Dispara els crèdits cinematogràfics la primera vegada que el joc
|
||||
// arriba al menú del títol (info::ctx.num_piramide == 0).
|
||||
static bool credits_triggered = false;
|
||||
if (!credits_triggered && info::ctx.num_piramide == 0) {
|
||||
if (Options::game.show_title_credits) {
|
||||
Overlay::startCredits();
|
||||
}
|
||||
credits_triggered = true;
|
||||
}
|
||||
maybeStartTitleCredits();
|
||||
|
||||
// Si l'overlay ja no bloqueja ESC (timeout), desbloquegem
|
||||
if (esc_blocked_ && !Overlay::isEscConsumed()) {
|
||||
esc_blocked_ = false;
|
||||
}
|
||||
|
||||
// Avança l'escena (si no estem pausats). En pausa, es manté l'escena
|
||||
// congelada i re-presentem l'últim frame amb l'overlay fresc per
|
||||
// damunt.
|
||||
if (!paused_) {
|
||||
// Transicions: si l'escena actual ha acabat (o s'ha senyalat
|
||||
// quit), llegim el seu next state i la destruïm per crear la
|
||||
// següent a continuació.
|
||||
if (current_scene_ && (current_scene_->done() || JG_Quitting())) {
|
||||
game_state_ = current_scene_->nextState();
|
||||
current_scene_.reset();
|
||||
if (!tickActiveScene()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Si no hi ha escena activa, construeix la pròxima segons
|
||||
// game_state_ i info::ctx. Si és impossible (game_state_ == -1,
|
||||
// quit, o state no registrat), eixim del loop.
|
||||
if (!current_scene_) {
|
||||
if (game_state_ == -1 || JG_Quitting()) return false;
|
||||
current_scene_ = createNextScene();
|
||||
if (!current_scene_) return false;
|
||||
current_scene_->onEnter();
|
||||
last_tick_ms_ = SDL_GetTicks();
|
||||
}
|
||||
|
||||
// Tick de l'escena. JI_Update refresca key_pressed/any_key; el
|
||||
// delta_ms és el temps real transcorregut des de l'últim tick.
|
||||
JI_Update();
|
||||
const Uint32 now = SDL_GetTicks();
|
||||
const int delta_ms = static_cast<int>(now - last_tick_ms_);
|
||||
last_tick_ms_ = now;
|
||||
current_scene_->tick(delta_ms);
|
||||
|
||||
// Converteix `screen` indexat → `pixel_data` ARGB amb la paleta
|
||||
// actual. JD8_Flip ja no fa yield (Phase B.2 eliminà els fibers);
|
||||
// ara només omple el framebuffer perquè el Director l'aprofite.
|
||||
JD8_Flip();
|
||||
std::memcpy(game_frame_, JD8_GetFramebuffer(), sizeof(game_frame_));
|
||||
has_frame_ = true;
|
||||
}
|
||||
|
||||
// Presenta sempre: parteix del frame net del joc, afegeix overlay i envia
|
||||
if (has_frame_) {
|
||||
std::memcpy(presentation_buffer_, game_frame_, sizeof(presentation_buffer_));
|
||||
Screen::get()->present(presentation_buffer_);
|
||||
}
|
||||
|
||||
// Límit de framerate segons VSync.
|
||||
// Nota: quan el runtime posseïx el main loop (SDL_AppIterate /
|
||||
// emscripten), aquest SDL_Delay no és ideal. Fase 7 afegirà un mode
|
||||
// que es basa en el timing intern de SDL en lloc del delay explícit.
|
||||
const Uint32 target_ms = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC;
|
||||
const Uint32 elapsed = SDL_GetTicks() - frame_start;
|
||||
if (elapsed < target_ms) {
|
||||
SDL_Delay(target_ms - elapsed);
|
||||
const Uint32 TARGET_MS = Options::video.vsync ? FRAME_MS_VSYNC : FRAME_MS_NO_VSYNC;
|
||||
const Uint32 ELAPSED = SDL_GetTicks() - FRAME_START;
|
||||
if (ELAPSED < TARGET_MS) {
|
||||
SDL_Delay(TARGET_MS - ELAPSED);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -262,7 +243,7 @@ void Director::teardown() {
|
||||
// Senyal de quit i descàrrega ordenada de l'escena en curs. Els
|
||||
// destructors de cada escena són no-bloquejants — ja no fan fades
|
||||
// bloquejants. La resta de cleanup la gestiona `destroy()`.
|
||||
JG_QuitSignal();
|
||||
Jg::quitSignal();
|
||||
current_scene_.reset();
|
||||
}
|
||||
|
||||
@@ -270,7 +251,9 @@ void Director::run() {
|
||||
setup();
|
||||
while (true) {
|
||||
pollAllEvents();
|
||||
if (!iterate()) break;
|
||||
if (!iterate()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
teardown();
|
||||
}
|
||||
@@ -282,113 +265,101 @@ void Director::pollAllEvents() {
|
||||
}
|
||||
}
|
||||
|
||||
auto Director::handleMenuEvent(const SDL_Event& event) -> bool {
|
||||
// Empassar-se el KEY_UP d'una tecla que el menú va consumir en KEY_DOWN.
|
||||
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode >= 0 &&
|
||||
event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) {
|
||||
menu_keys_held_[event.key.scancode] = false;
|
||||
return true;
|
||||
}
|
||||
const bool KEY_DOWN = event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat;
|
||||
// Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot.
|
||||
if (Menu::isCapturing() && KEY_DOWN) {
|
||||
Menu::captureKey(event.key.scancode);
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return true;
|
||||
}
|
||||
// Pausa / menú toggle.
|
||||
if (KEY_DOWN && event.key.scancode == KeyConfig::scancode("pause_toggle")) {
|
||||
togglePause();
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return true;
|
||||
}
|
||||
if (KEY_DOWN && event.key.scancode == KeyConfig::scancode("menu_toggle")) {
|
||||
Menu::toggle();
|
||||
Ji::setInputBlocked(Menu::isOpen());
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return true;
|
||||
}
|
||||
// Si el menú està obert, consumeix tot l'input de teclat.
|
||||
if (Menu::isOpen() && KEY_DOWN) {
|
||||
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||
Menu::close();
|
||||
Ji::setInputBlocked(false);
|
||||
esc_swallow_until_release_ = true;
|
||||
} else {
|
||||
Menu::handleKey(event.key.scancode);
|
||||
if (!Menu::isOpen()) {
|
||||
Ji::setInputBlocked(false);
|
||||
}
|
||||
}
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return true;
|
||||
}
|
||||
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto Director::handleEscapeEvent(const SDL_Event& event) -> bool {
|
||||
// Salta els crèdits amb qualsevol tecla que arribe al joc.
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) {
|
||||
Overlay::cancelCredits();
|
||||
return true;
|
||||
}
|
||||
// Allibera el bloqueig d'ESC quan l'usuari la deixa anar.
|
||||
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode == SDL_SCANCODE_ESCAPE && esc_swallow_until_release_) {
|
||||
esc_swallow_until_release_ = false;
|
||||
return true;
|
||||
}
|
||||
// ESC KEY_DOWN: bloqueja per polling i decideix notificació vs eixida.
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) {
|
||||
esc_blocked_ = true;
|
||||
if (!Overlay::isEscConsumed()) {
|
||||
Overlay::handleEscape();
|
||||
} else {
|
||||
esc_blocked_ = false;
|
||||
key_pressed_ = true;
|
||||
Jg::quitSignal();
|
||||
paused_ = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Director::handleEvent(const SDL_Event& event) {
|
||||
if (event.type == SDL_EVENT_QUIT) {
|
||||
JG_QuitSignal();
|
||||
Jg::quitSignal();
|
||||
requestQuit();
|
||||
}
|
||||
// Hot-plug de gamepad (a Emscripten els dispositius web entren com
|
||||
// JOYSTICK_ADDED/REMOVED perquè SDL no reconeix el GUID)
|
||||
if (event.type == SDL_EVENT_GAMEPAD_ADDED || event.type == SDL_EVENT_GAMEPAD_REMOVED ||
|
||||
event.type == SDL_EVENT_JOYSTICK_ADDED || event.type == SDL_EVENT_JOYSTICK_REMOVED) {
|
||||
Gamepad::handleEvent(event);
|
||||
return;
|
||||
}
|
||||
// Empassar-se el KEY_UP de qualsevol tecla que el menú va consumir en KEY_DOWN
|
||||
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode >= 0 &&
|
||||
event.key.scancode < SDL_SCANCODE_COUNT && menu_keys_held_[event.key.scancode]) {
|
||||
menu_keys_held_[event.key.scancode] = false;
|
||||
if (handleMenuEvent(event)) {
|
||||
return;
|
||||
}
|
||||
// Captura de tecla (remapeig al menú): intercepta KEY_DOWN abans de tot
|
||||
if (Menu::isCapturing() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
|
||||
Menu::captureKey(event.key.scancode);
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
if (handleEscapeEvent(event)) {
|
||||
return;
|
||||
}
|
||||
// Pausa: F11 (o tecla configurada) pausa/reprén la simulació.
|
||||
// No mostrem notificació — l'indicador persistent "Pausa" a la cantonada
|
||||
// superior dreta (pintat per Overlay) ja comunica l'estat.
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
|
||||
event.key.scancode == KeyConfig::scancode("pause_toggle")) {
|
||||
togglePause();
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return;
|
||||
}
|
||||
// Menú: F12 (o tecla configurada) obre/tanca el menú flotant
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
|
||||
event.key.scancode == KeyConfig::scancode("menu_toggle")) {
|
||||
Menu::toggle();
|
||||
JI_SetInputBlocked(Menu::isOpen());
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return;
|
||||
}
|
||||
// Si el menú està obert, consumeix tot l'input de teclat
|
||||
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat) {
|
||||
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||
Menu::close();
|
||||
JI_SetInputBlocked(false);
|
||||
// Empassa l'ESC fins al release perquè el joc no la veja per polling
|
||||
esc_swallow_until_release_ = true;
|
||||
} else {
|
||||
Menu::handleKey(event.key.scancode);
|
||||
// El menú pot haver-se tancat (p.ex. Backspace al nivell arrel)
|
||||
if (!Menu::isOpen()) {
|
||||
JI_SetInputBlocked(false);
|
||||
}
|
||||
}
|
||||
menu_keys_held_[event.key.scancode] = true;
|
||||
return;
|
||||
}
|
||||
if (Menu::isOpen() && event.type == SDL_EVENT_KEY_UP) {
|
||||
return; // no deixem passar KEY_UP al joc tampoc
|
||||
}
|
||||
// Salta els crèdits amb qualsevol tecla que arribe al joc. Es fa DESPRÉS
|
||||
// del toggle del menú/pausa i del handling del menú obert — així F12 i
|
||||
// SELECT (gamepad) obrin el menú sense cancel·lar els crèdits, i la
|
||||
// navegació per dins del menú tampoc els anul·la.
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat && Overlay::creditsActive()) {
|
||||
Overlay::cancelCredits();
|
||||
return;
|
||||
}
|
||||
// Allibera el bloqueig d'ESC quan l'usuari la deixa anar
|
||||
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode == SDL_SCANCODE_ESCAPE && esc_swallow_until_release_) {
|
||||
esc_swallow_until_release_ = false;
|
||||
return;
|
||||
}
|
||||
// ESC: interceptem KEY_DOWN per bloquejar-la ABANS que el joc la veja per polling
|
||||
if (event.type == SDL_EVENT_KEY_DOWN && event.key.scancode == SDL_SCANCODE_ESCAPE && !event.key.repeat) {
|
||||
esc_blocked_ = true; // Bloqueja ESC per polling immediatament
|
||||
if (!Overlay::isEscConsumed()) {
|
||||
// Primera pulsació: mostra notificació
|
||||
Overlay::handleEscape();
|
||||
} else {
|
||||
// Segona pulsació: senyal d'eixida al joc
|
||||
esc_blocked_ = false;
|
||||
if (event.type == SDL_EVENT_KEY_UP && event.key.scancode != SDL_SCANCODE_ESCAPE) {
|
||||
const auto SC = event.key.scancode;
|
||||
if (!KeyConfig::isGuiKey(SC)) {
|
||||
key_pressed_ = true;
|
||||
JG_QuitSignal();
|
||||
// Si estem en pausa, la desactivem: el fiber del joc està
|
||||
// congelat i necessita ser reprès per veure la senyal de
|
||||
// quit i poder tornar de forma natural.
|
||||
paused_ = false;
|
||||
}
|
||||
return; // no processa més aquest event
|
||||
}
|
||||
if (event.type == SDL_EVENT_KEY_UP) {
|
||||
if (event.key.scancode == SDL_SCANCODE_ESCAPE) {
|
||||
// Ja processat a KEY_DOWN, només deixem netejar el bloqueig
|
||||
// quan l'overlay faça timeout
|
||||
return;
|
||||
} else {
|
||||
// Comprova si és una tecla d'UI registrada (no passa al joc).
|
||||
// KeyConfig::isGuiKey cobreix totes les tecles GUI a la vegada,
|
||||
// incloent pause_toggle i menu_toggle (defensa en profunditat:
|
||||
// aquestes ja s'haurien hagut de menjar al swallow d'amunt).
|
||||
const auto sc = event.key.scancode;
|
||||
if (!KeyConfig::isGuiKey(sc)) {
|
||||
key_pressed_ = true;
|
||||
JI_moveCheats(sc);
|
||||
}
|
||||
Ji::moveCheats(SC);
|
||||
}
|
||||
}
|
||||
Mouse::handleEvent(event);
|
||||
@@ -396,7 +367,7 @@ void Director::handleEvent(const SDL_Event& event) {
|
||||
|
||||
void Director::requestQuit() {
|
||||
quit_requested_ = true;
|
||||
JG_QuitSignal();
|
||||
Jg::quitSignal();
|
||||
}
|
||||
|
||||
void Director::requestRestart() {
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "scenes/scene.hpp"
|
||||
#include "game/scenes/scene.hpp"
|
||||
|
||||
// El Director és l'únic thread del runtime. Cada iterate() fa input →
|
||||
// tick de l'escena actual → JD8_Flip → overlay → present → sleep al frame
|
||||
// target. Totes les escenes (`scenes::Scene` i `ModuleGame`) són
|
||||
// tick de l'escena actual → Jd8::flip → overlay → present → sleep al frame
|
||||
// target. Totes les escenes (`Scenes::Scene` i `ModuleGame`) són
|
||||
// tick-based i no bloquegen — no hi ha fibers, mutex ni condition_variable.
|
||||
// Compatible amb SDL_AppIterate i amb el futur port a emscripten.
|
||||
class Director {
|
||||
@@ -28,46 +28,53 @@ class Director {
|
||||
// per l'event loop de SDL3 en lloc d'un bucle propi — imprescindible
|
||||
// per al port a emscripten, on el runtime posseïx el main loop.
|
||||
void setup();
|
||||
bool iterate(); // torna false quan el joc vol eixir
|
||||
auto iterate() -> bool; // torna false quan el joc vol eixir
|
||||
void teardown();
|
||||
void handleEvent(const SDL_Event& event);
|
||||
|
||||
// Demana l'eixida (ex: segona pulsació d'ESC o SDL_QUIT)
|
||||
void requestQuit();
|
||||
auto isQuitRequested() const -> bool { return quit_requested_; }
|
||||
[[nodiscard]] auto isQuitRequested() const -> bool { return quit_requested_; }
|
||||
|
||||
// Demana un reinici "suau": para música i sons, reseteja info::ctx i
|
||||
// Demana un reinici "suau": para música i sons, reseteja Info::ctx i
|
||||
// torna a l'intro (state 255). Es processa al començament del pròxim
|
||||
// iterate() per evitar manipular l'escena des d'una lambda del menú.
|
||||
void requestRestart();
|
||||
|
||||
// Consumeix el flag de "tecla polsada" (com l'antic JI_AnyKey)
|
||||
// Consumeix el flag de "tecla polsada" (com l'antic Ji::anyKey)
|
||||
auto consumeKeyPressed() -> bool;
|
||||
|
||||
// Indica si ESC està bloquejada (el joc no l'ha de veure)
|
||||
auto isEscBlocked() const -> bool { return esc_blocked_ || esc_swallow_until_release_; }
|
||||
[[nodiscard]] auto isEscBlocked() const -> bool { return esc_blocked_ || esc_swallow_until_release_; }
|
||||
|
||||
// Pausa: mentre està activa, iterate() no avança l'escena — es
|
||||
// continua presentant el darrer frame amb overlay fresc.
|
||||
void togglePause();
|
||||
auto isPaused() const -> bool { return paused_; }
|
||||
[[nodiscard]] auto isPaused() const -> bool { return paused_; }
|
||||
|
||||
public:
|
||||
~Director();
|
||||
|
||||
private:
|
||||
Director() = default;
|
||||
|
||||
static std::unique_ptr<Director> instance_;
|
||||
static std::unique_ptr<Director> instance;
|
||||
|
||||
void pollAllEvents(); // drenatge amb SDL_PollEvent, només per al bucle natiu
|
||||
|
||||
// Inicialitza info::ctx a partir de Options::game.* i comprova trick.ini.
|
||||
// Inicialitza Info::ctx a partir de Options::game.* i comprova trick.ini.
|
||||
// Es crida una sola vegada des d'iterate() a la primera invocació.
|
||||
void initGameContext();
|
||||
// Construeix l'escena apropiada segons game_state_ i info::ctx.
|
||||
static void initGameContext();
|
||||
// Construeix l'escena apropiada segons game_state_ i Info::ctx.
|
||||
// Retorna nullptr si l'state actual no té escena registrada (bug).
|
||||
std::unique_ptr<scenes::Scene> createNextScene();
|
||||
[[nodiscard]] auto createNextScene() const -> std::unique_ptr<Scenes::Scene>;
|
||||
// Helpers d'iterate() — extrets per reduir complexitat cognitiva.
|
||||
void applyRestart();
|
||||
static void maybeStartTitleCredits();
|
||||
auto tickActiveScene() -> bool; // true = continuar; false = sortir del loop
|
||||
|
||||
// Helpers d'handleEvent() — cada un retorna true si l'event s'ha consumit.
|
||||
auto handleMenuEvent(const SDL_Event& event) -> bool;
|
||||
auto handleEscapeEvent(const SDL_Event& event) -> bool;
|
||||
|
||||
// Buffers persistents entre iteracions. Abans eren locals a run(),
|
||||
// ara són membres perquè iterate() els pot reutilitzar sense tornar-los
|
||||
@@ -78,7 +85,7 @@ class Director {
|
||||
|
||||
// Estat de l'escena actual. Abans vivia al stack del GameFiber; des
|
||||
// de la Phase B.2 de la migració viu directament al Director.
|
||||
std::unique_ptr<scenes::Scene> current_scene_;
|
||||
std::unique_ptr<Scenes::Scene> current_scene_;
|
||||
int game_state_{1}; // 0 = gameplay (ModuleGame), 1 = via SceneRegistry, -1 = quit
|
||||
Uint32 last_tick_ms_{0};
|
||||
bool context_initialized_{false};
|
||||
@@ -89,9 +96,9 @@ class Director {
|
||||
std::atomic<bool> esc_blocked_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
// Quan el menú tanca amb ESC, empassem-nos l'ESC fins que l'usuari la deixe anar,
|
||||
// per no fer eixir el joc al proper poll de JI_KeyPressed.
|
||||
// per no fer eixir el joc al proper poll de Ji::keyPressed.
|
||||
std::atomic<bool> esc_swallow_until_release_{false};
|
||||
// Tecles consumides pel menú (KEY_DOWN): el KEY_UP associat cal empassar-lo
|
||||
// per evitar que el joc (JI_AnyKey / JI_moveCheats) les veja quan el menú tanca.
|
||||
// per evitar que el joc (Ji::anyKey / Ji::moveCheats) les veja quan el menú tanca.
|
||||
bool menu_keys_held_[SDL_SCANCODE_COUNT]{};
|
||||
};
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
# source/external/.clang-tidy
|
||||
Checks: '-*'
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: ''
|
||||
HeaderFilterRegex: ''
|
||||
Vendored
+2
@@ -1,3 +1,4 @@
|
||||
// NOLINTBEGIN(clang-analyzer-unix.Malloc) — codi extern de tercers, no l'auditem
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -510,3 +511,4 @@ unsigned char* LoadGif(unsigned char *buffer, unsigned short* w, unsigned short*
|
||||
|
||||
fclose( gif_file );
|
||||
}*/
|
||||
// NOLINTEND(clang-analyzer-unix.Malloc)
|
||||
|
||||
+26
-16
@@ -1,12 +1,12 @@
|
||||
#include "game/bola.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "core/jail/jgame.hpp"
|
||||
|
||||
Bola::Bola(JD8_Surface gfx, Prota* sam)
|
||||
Bola::Bola(Jd8::Surface gfx, Prota* sam)
|
||||
: Sprite(gfx) {
|
||||
this->sam = sam;
|
||||
this->sam_ = sam;
|
||||
|
||||
entitat.frames.reserve(2);
|
||||
entitat.frames.push_back({30, 155, 15, 15});
|
||||
@@ -17,36 +17,46 @@ Bola::Bola(JD8_Surface gfx, Prota* sam)
|
||||
|
||||
this->cur_frame = 0;
|
||||
this->o = 0;
|
||||
this->cycles_per_frame = 4;
|
||||
this->cycles_per_frame_ = 4;
|
||||
this->x = 20;
|
||||
this->y = 100;
|
||||
this->contador = 0;
|
||||
this->contador_ = 0;
|
||||
}
|
||||
|
||||
void Bola::draw() {
|
||||
if (this->contador == 0) Sprite::draw();
|
||||
if (this->contador_ == 0) {
|
||||
Sprite::draw();
|
||||
}
|
||||
}
|
||||
|
||||
void Bola::update() {
|
||||
if (this->contador == 0) {
|
||||
if (this->contador_ == 0) {
|
||||
// Augmentem la x
|
||||
this->x++;
|
||||
if (this->x == 280) this->contador = 200;
|
||||
if (this->x == 280) {
|
||||
this->contador_ = 200;
|
||||
}
|
||||
|
||||
// Augmentem el frame
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
if (Jg::getCycleCounter() % this->cycles_per_frame_ == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == entitat.animacions[this->o].frames.size()) this->cur_frame = 0;
|
||||
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
|
||||
this->cur_frame = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Comprovem si ha tocat a Sam
|
||||
if (this->x > (this->sam->x - 7) && this->x < (this->sam->x + 7) && this->y > (this->sam->y - 7) && this->y < (this->sam->y + 7)) {
|
||||
this->contador = 200;
|
||||
info::ctx.vida--;
|
||||
if (info::ctx.vida == 0) this->sam->o = 5;
|
||||
if (this->x > (this->sam_->x - 7) && this->x < (this->sam_->x + 7) && this->y > (this->sam_->y - 7) && this->y < (this->sam_->y + 7)) {
|
||||
this->contador_ = 200;
|
||||
Info::ctx.vida--;
|
||||
if (Info::ctx.vida == 0) {
|
||||
this->sam_->o = 5;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this->contador--;
|
||||
if (this->contador == 0) this->x = 20;
|
||||
this->contador_--;
|
||||
if (this->contador_ == 0) {
|
||||
this->x = 20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
|
||||
class Bola : public Sprite {
|
||||
public:
|
||||
explicit Bola(JD8_Surface gfx, Prota* sam);
|
||||
explicit Bola(Jd8::Surface gfx, Prota* sam);
|
||||
|
||||
void draw() override;
|
||||
void update();
|
||||
|
||||
protected:
|
||||
Uint8 contador;
|
||||
Prota* sam;
|
||||
Uint8 contador_;
|
||||
Prota* sam_;
|
||||
};
|
||||
|
||||
@@ -16,10 +16,8 @@ namespace Defaults::KeysGame {
|
||||
namespace Defaults::Video {
|
||||
constexpr bool GPU_ACCELERATION = true;
|
||||
constexpr bool SHADER_ENABLED = false;
|
||||
constexpr bool SUPERSAMPLING = false;
|
||||
constexpr bool VSYNC = true;
|
||||
constexpr bool ASPECT_RATIO_4_3 = false; // CRT original estira 200→240
|
||||
constexpr int DOWNSCALE_ALGO = 1; // 0=bilinear, 1=Lanczos2, 2=Lanczos3
|
||||
constexpr int INTERNAL_RESOLUTION = 1; // Multiplicador enter de la textura font abans del pipeline
|
||||
// TextureFilter i ScalingMode viuen a Options (requereixen #include, evitem dependència circular).
|
||||
} // namespace Defaults::Video
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#include "game/engendro.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "core/jail/jgame.hpp"
|
||||
|
||||
Engendro::Engendro(JD8_Surface gfx, Uint16 x, Uint16 y)
|
||||
Engendro::Engendro(Jd8::Surface gfx, Uint16 x, Uint16 y)
|
||||
: Sprite(gfx) {
|
||||
entitat.frames.reserve(4);
|
||||
for (int py = 50; py <= 65; py += 15) {
|
||||
@@ -22,23 +22,27 @@ Engendro::Engendro(JD8_Surface gfx, Uint16 x, Uint16 y)
|
||||
entitat.animacions[0].frames = {0, 1, 2, 3, 2, 1};
|
||||
|
||||
this->cur_frame = 0;
|
||||
this->vida = 18;
|
||||
this->vida_ = 18;
|
||||
this->x = x;
|
||||
this->y = y;
|
||||
this->o = 0;
|
||||
this->cycles_per_frame = 30;
|
||||
this->cycles_per_frame_ = 30;
|
||||
}
|
||||
|
||||
bool Engendro::update() {
|
||||
auto Engendro::update() -> bool {
|
||||
bool mort = false;
|
||||
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
if (Jg::getCycleCounter() % this->cycles_per_frame_ == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == entitat.animacions[this->o].frames.size()) this->cur_frame = 0;
|
||||
this->vida--;
|
||||
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
|
||||
this->cur_frame = 0;
|
||||
}
|
||||
this->vida_--;
|
||||
}
|
||||
|
||||
if (vida == 0) mort = true;
|
||||
if (vida_ == 0) {
|
||||
mort = true;
|
||||
}
|
||||
|
||||
return mort;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
class Engendro : public Sprite {
|
||||
public:
|
||||
explicit Engendro(JD8_Surface gfx, Uint16 x, Uint16 y);
|
||||
explicit Engendro(Jd8::Surface gfx, Uint16 x, Uint16 y);
|
||||
|
||||
bool update();
|
||||
auto update() -> bool;
|
||||
|
||||
protected:
|
||||
Uint8 vida;
|
||||
Uint8 vida_;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "game/info.hpp"
|
||||
|
||||
// La instància `info::ctx` està definida com a `inline` al header;
|
||||
// La instància `Info::ctx` està definida com a `inline` al header;
|
||||
// aquest fitxer es manté per a si cal afegir lògica addicional més endavant.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
namespace info {
|
||||
namespace Info {
|
||||
|
||||
struct GameContext {
|
||||
int num_piramide = 0;
|
||||
@@ -17,8 +17,8 @@ namespace info {
|
||||
};
|
||||
|
||||
// Instància única de l'estat del joc. Reemplaça les variables soltes del
|
||||
// namespace `info::` per una struct encapsulada. A Fase 5 (single-threaded)
|
||||
// namespace `Info::` per una struct encapsulada. A Fase 5 (single-threaded)
|
||||
// es podrà passar per referència als mòduls en lloc d'accedir via singleton.
|
||||
inline GameContext ctx;
|
||||
|
||||
} // namespace info
|
||||
} // namespace Info
|
||||
|
||||
+127
-103
@@ -1,198 +1,215 @@
|
||||
#include "game/mapa.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "core/jail/jgame.hpp"
|
||||
#include "core/jail/jinput.hpp"
|
||||
|
||||
Mapa::Mapa(JD8_Surface gfx, Prota* sam) {
|
||||
this->gfx = gfx;
|
||||
this->sam = sam;
|
||||
Mapa::Mapa(Jd8::Surface gfx, Prota* sam) {
|
||||
this->gfx_ = gfx;
|
||||
this->sam_ = sam;
|
||||
|
||||
this->preparaFondoEstatic();
|
||||
this->preparaTombes();
|
||||
|
||||
this->ultim_vertex.columna = 255;
|
||||
this->frame_torxes = 0;
|
||||
this->ultim_vertex_.columna = 255;
|
||||
this->frame_torxes_ = 0;
|
||||
|
||||
this->farao = false;
|
||||
this->clau = false;
|
||||
this->porta_oberta = false;
|
||||
this->nova_momia = false;
|
||||
this->farao_ = false;
|
||||
this->clau_ = false;
|
||||
this->porta_oberta_ = false;
|
||||
this->nova_momia_ = false;
|
||||
}
|
||||
|
||||
Mapa::~Mapa(void) {
|
||||
JD8_FreeSurface(this->fondo);
|
||||
Mapa::~Mapa() {
|
||||
Jd8::freeSurface(this->fondo_);
|
||||
}
|
||||
|
||||
void Mapa::draw() {
|
||||
if (info::ctx.num_piramide != 4) {
|
||||
switch (sam->o) {
|
||||
if (Info::ctx.num_piramide != 4) {
|
||||
switch (sam_->o) {
|
||||
case 0: // Down
|
||||
JD8_BlitCKToSurface(sam->x, sam->y, this->gfx, 15, 125 + sam->frame_pejades, 15, 1, this->fondo, 255);
|
||||
Jd8::blitCKToSurface(sam_->x, sam_->y, this->gfx_, 15, 125 + sam_->frame_pejades, 15, 1, this->fondo_, 255);
|
||||
break;
|
||||
case 1: // Up
|
||||
JD8_BlitCKToSurface(sam->x, sam->y + 15, this->gfx, 0, 125 + (14 - sam->frame_pejades), 15, 1, this->fondo, 255);
|
||||
Jd8::blitCKToSurface(sam_->x, sam_->y + 15, this->gfx_, 0, 125 + (14 - sam_->frame_pejades), 15, 1, this->fondo_, 255);
|
||||
break;
|
||||
case 2: // Right
|
||||
JD8_BlitCKToSurface(sam->x + 7, sam->y, this->gfx, 30 + sam->frame_pejades, 125, 1, 15, this->fondo, 255);
|
||||
Jd8::blitCKToSurface(sam_->x + 7, sam_->y, this->gfx_, 30 + sam_->frame_pejades, 125, 1, 15, this->fondo_, 255);
|
||||
break;
|
||||
case 3: // Left
|
||||
JD8_BlitCKToSurface(sam->x + 8, sam->y, this->gfx, 45 + (14 - sam->frame_pejades), 125, 1, 15, this->fondo, 255);
|
||||
Jd8::blitCKToSurface(sam_->x + 8, sam_->y, this->gfx_, 45 + (14 - sam_->frame_pejades), 125, 1, 15, this->fondo_, 255);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
JD8_Blit(this->fondo);
|
||||
Jd8::blit(this->fondo_);
|
||||
|
||||
// Pinta tombes
|
||||
for (int y = 0; y < 4; y++) {
|
||||
for (int x = 0; x < 4; x++) {
|
||||
JD8_BlitCK(35 + (x * 65), 45 + (y * 35), this->gfx, this->tombes[x + y * 4].x, this->tombes[x + y * 4].y, 50, 20, 255);
|
||||
Jd8::blitCK(35 + (x * 65), 45 + (y * 35), this->gfx_, this->tombes[x + (y * 4)].x, this->tombes[x + (y * 4)].y, 50, 20, 255);
|
||||
}
|
||||
}
|
||||
|
||||
JD8_BlitCK(45, 15, this->gfx, 30 + (this->frame_torxes * 25), 80, 25, 15, 255);
|
||||
JD8_BlitCK(95, 15, this->gfx, 30 + (this->frame_torxes * 25), 80, 25, 15, 255);
|
||||
JD8_BlitCK(195, 15, this->gfx, 30 + (this->frame_torxes * 25), 80, 25, 15, 255);
|
||||
JD8_BlitCK(245, 15, this->gfx, 30 + (this->frame_torxes * 25), 80, 25, 15, 255);
|
||||
Jd8::blitCK(45, 15, this->gfx_, 30 + (this->frame_torxes_ * 25), 80, 25, 15, 255);
|
||||
Jd8::blitCK(95, 15, this->gfx_, 30 + (this->frame_torxes_ * 25), 80, 25, 15, 255);
|
||||
Jd8::blitCK(195, 15, this->gfx_, 30 + (this->frame_torxes_ * 25), 80, 25, 15, 255);
|
||||
Jd8::blitCK(245, 15, this->gfx_, 30 + (this->frame_torxes_ * 25), 80, 25, 15, 255);
|
||||
};
|
||||
|
||||
void Mapa::update() {
|
||||
if (((sam->x - 20) % 65 == 0) && ((sam->y - 30) % 35 == 0) && ((this->ultim_vertex.columna != (sam->x - 20) / 65) || (this->ultim_vertex.fila != (sam->y - 30) / 35))) {
|
||||
this->vertex.columna = (sam->x - 20) / 65;
|
||||
this->vertex.fila = (sam->y - 30) / 35;
|
||||
if (this->ultim_vertex.columna != 255) this->comprovaUltimCami();
|
||||
this->ultim_vertex = this->vertex;
|
||||
if (((sam_->x - 20) % 65 == 0) && ((sam_->y - 30) % 35 == 0) && ((this->ultim_vertex_.columna != (sam_->x - 20) / 65) || (this->ultim_vertex_.fila != (sam_->y - 30) / 35))) {
|
||||
this->vertex_.columna = (sam_->x - 20) / 65;
|
||||
this->vertex_.fila = (sam_->y - 30) / 35;
|
||||
if (this->ultim_vertex_.columna != 255) {
|
||||
this->comprovaUltimCami();
|
||||
}
|
||||
this->ultim_vertex_ = this->vertex_;
|
||||
}
|
||||
|
||||
if (this->porta_oberta && sam->x == 150 && sam->y == 30) {
|
||||
if (JI_KeyPressed(SDL_SCANCODE_UP)) {
|
||||
this->sam->o = 4;
|
||||
this->sam->y -= 15;
|
||||
if (this->porta_oberta_ && sam_->x == 150 && sam_->y == 30) {
|
||||
if (Ji::keyPressed(SDL_SCANCODE_UP)) {
|
||||
this->sam_->o = 4;
|
||||
this->sam_->y -= 15;
|
||||
}
|
||||
}
|
||||
|
||||
if (JG_GetCycleCounter() % 8 == 0) {
|
||||
this->frame_torxes++;
|
||||
this->frame_torxes = this->frame_torxes % 4;
|
||||
if (Jg::getCycleCounter() % 8 == 0) {
|
||||
this->frame_torxes_++;
|
||||
this->frame_torxes_ = this->frame_torxes_ % 4;
|
||||
}
|
||||
}
|
||||
|
||||
bool Mapa::novaMomia() {
|
||||
bool resultat = nova_momia;
|
||||
nova_momia = false;
|
||||
auto Mapa::novaMomia() -> bool {
|
||||
bool resultat = nova_momia_;
|
||||
nova_momia_ = false;
|
||||
return resultat;
|
||||
}
|
||||
|
||||
void Mapa::preparaFondoEstatic() {
|
||||
// Prepara el fondo est�tic de l'habitaci�
|
||||
this->fondo = JD8_NewSurface();
|
||||
if (info::ctx.num_piramide == 6) {
|
||||
JD8_BlitToSurface(9, 2, this->gfx, 227, 185, 92, 7, this->fondo); // Text "SECRETA"
|
||||
this->fondo_ = Jd8::newSurface();
|
||||
if (Info::ctx.num_piramide == 6) {
|
||||
Jd8::blitToSurface(9, 2, this->gfx_, 227, 185, 92, 7, this->fondo_); // Text "SECRETA"
|
||||
} else {
|
||||
JD8_BlitToSurface(9, 2, this->gfx, 60, 185, 39, 7, this->fondo); // Text "NIVELL"
|
||||
JD8_BlitToSurface(72, 6, this->gfx, 153, 189, 3, 1, this->fondo); // Ralleta entre num piramide i num habitacio
|
||||
Jd8::blitToSurface(9, 2, this->gfx_, 60, 185, 39, 7, this->fondo_); // Text "NIVELL"
|
||||
Jd8::blitToSurface(72, 6, this->gfx_, 153, 189, 3, 1, this->fondo_); // Ralleta entre num piramide i num habitacio
|
||||
}
|
||||
Jd8::blitToSurface(130, 2, this->gfx_, 225, 192, 19, 8, this->fondo_); // Montonet de monedes + signe '='
|
||||
Jd8::blitToSurface(220, 2, this->gfx_, 160, 185, 48, 7, this->fondo_); // Text "ENERGIA"
|
||||
if (Info::ctx.diners >= 200) {
|
||||
Jd8::blitToSurface(175, 3, this->gfx_, 60, 193, 7, 6, this->fondo_);
|
||||
}
|
||||
JD8_BlitToSurface(130, 2, this->gfx, 225, 192, 19, 8, this->fondo); // Montonet de monedes + signe '='
|
||||
JD8_BlitToSurface(220, 2, this->gfx, 160, 185, 48, 7, this->fondo); // Text "ENERGIA"
|
||||
if (info::ctx.diners >= 200) JD8_BlitToSurface(175, 3, this->gfx, 60, 193, 7, 6, this->fondo);
|
||||
|
||||
// Pinta taulells
|
||||
for (int y = 0; y < 11; y++) {
|
||||
for (int x = 0; x < 19; x++) {
|
||||
switch (info::ctx.num_piramide) {
|
||||
switch (Info::ctx.num_piramide) {
|
||||
case 1:
|
||||
JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 0, 80, 15, 15, this->fondo);
|
||||
Jd8::blitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx_, 0, 80, 15, 15, this->fondo_);
|
||||
break;
|
||||
case 2:
|
||||
JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 25, 95, 15, 15, this->fondo);
|
||||
Jd8::blitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx_, 25, 95, 15, 15, this->fondo_);
|
||||
break;
|
||||
case 3:
|
||||
JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 40, 95, 15, 15, this->fondo);
|
||||
Jd8::blitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx_, 40, 95, 15, 15, this->fondo_);
|
||||
break;
|
||||
case 4:
|
||||
JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 175 + ((rand() % 3) * 15), 80, 15, 15, this->fondo);
|
||||
Jd8::blitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx_, 175 + ((rand() % 3) * 15), 80, 15, 15, this->fondo_);
|
||||
break;
|
||||
case 5:
|
||||
JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 130, 80, 15, 15, this->fondo);
|
||||
Jd8::blitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx_, 130, 80, 15, 15, this->fondo_);
|
||||
break;
|
||||
case 6:
|
||||
JD8_BlitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx, 145, 80, 15, 15, this->fondo);
|
||||
Jd8::blitToSurface(20 + (x * 15), 30 + (y * 15), this->gfx_, 145, 80, 15, 15, this->fondo_);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pinta vores de les parets
|
||||
JD8_BlitCKToSurface(5, 15, this->gfx, 30, 110, 15, 15, this->fondo, 255);
|
||||
JD8_BlitCKToSurface(295, 15, this->gfx, 45, 110, 15, 15, this->fondo, 255);
|
||||
JD8_BlitCKToSurface(5, 180, this->gfx, 0, 155, 15, 20, this->fondo, 255);
|
||||
JD8_BlitCKToSurface(295, 180, this->gfx, 15, 155, 15, 20, this->fondo, 255);
|
||||
Jd8::blitCKToSurface(5, 15, this->gfx_, 30, 110, 15, 15, this->fondo_, 255);
|
||||
Jd8::blitCKToSurface(295, 15, this->gfx_, 45, 110, 15, 15, this->fondo_, 255);
|
||||
Jd8::blitCKToSurface(5, 180, this->gfx_, 0, 155, 15, 20, this->fondo_, 255);
|
||||
Jd8::blitCKToSurface(295, 180, this->gfx_, 15, 155, 15, 20, this->fondo_, 255);
|
||||
|
||||
// Pinta parets verticals
|
||||
for (int i = 0; i < 10; i++) {
|
||||
JD8_BlitToSurface(5, 30 + (i * 15), this->gfx, 0, 110, 15, 15, this->fondo);
|
||||
JD8_BlitToSurface(295, 30 + (i * 15), this->gfx, 15, 110, 15, 15, this->fondo);
|
||||
Jd8::blitToSurface(5, 30 + (i * 15), this->gfx_, 0, 110, 15, 15, this->fondo_);
|
||||
Jd8::blitToSurface(295, 30 + (i * 15), this->gfx_, 15, 110, 15, 15, this->fondo_);
|
||||
}
|
||||
|
||||
// Pinta parets hortzintals
|
||||
for (int i = 0; i < 11; i++) {
|
||||
JD8_BlitToSurface(20 + (i * 25), 185, this->gfx, 0, 95, 25, 15, this->fondo);
|
||||
JD8_BlitToSurface(20 + (i * 25), 15, this->gfx, 0, 95, 25, 15, this->fondo);
|
||||
Jd8::blitToSurface(20 + (i * 25), 185, this->gfx_, 0, 95, 25, 15, this->fondo_);
|
||||
Jd8::blitToSurface(20 + (i * 25), 15, this->gfx_, 0, 95, 25, 15, this->fondo_);
|
||||
}
|
||||
|
||||
// Pinta la porta
|
||||
JD8_BlitCKToSurface(150, 18, this->gfx, 0, 143, 15, 12, this->fondo, 255);
|
||||
Jd8::blitCKToSurface(150, 18, this->gfx_, 0, 143, 15, 12, this->fondo_, 255);
|
||||
|
||||
if (info::ctx.num_piramide == 2) {
|
||||
JD8_BlitToSurface(5, 100, this->gfx, 30, 140, 15, 15, this->fondo);
|
||||
if (Info::ctx.num_piramide == 2) {
|
||||
Jd8::blitToSurface(5, 100, this->gfx_, 30, 140, 15, 15, this->fondo_);
|
||||
}
|
||||
}
|
||||
|
||||
void swap(Uint8& a, Uint8& b) {
|
||||
void swap(Uint8& a, Uint8& b) noexcept {
|
||||
Uint8 temp = a;
|
||||
a = b;
|
||||
b = temp;
|
||||
}
|
||||
|
||||
void Mapa::preparaTombes() {
|
||||
const Uint8 contingut = info::ctx.num_piramide == 6 ? CONTE_DIAMANT : CONTE_RES;
|
||||
int cx = info::ctx.num_piramide == 6 ? 270 : 0;
|
||||
int cy = info::ctx.num_piramide == 6 ? 50 : 0;
|
||||
const Uint8 CONTINGUT = Info::ctx.num_piramide == 6 ? CONTE_DIAMANT : CONTE_RES;
|
||||
int cx = Info::ctx.num_piramide == 6 ? 270 : 0;
|
||||
int cy = Info::ctx.num_piramide == 6 ? 50 : 0;
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
this->tombes[i].contingut = contingut;
|
||||
this->tombes[i].oberta = false;
|
||||
this->tombes[i].costat[0] = false;
|
||||
this->tombes[i].costat[1] = false;
|
||||
this->tombes[i].costat[2] = false;
|
||||
this->tombes[i].costat[3] = false;
|
||||
this->tombes[i].x = cx;
|
||||
this->tombes[i].y = cy;
|
||||
for (auto& tombe : this->tombes) {
|
||||
tombe.contingut = CONTINGUT;
|
||||
tombe.oberta = false;
|
||||
tombe.costat[0] = false;
|
||||
tombe.costat[1] = false;
|
||||
tombe.costat[2] = false;
|
||||
tombe.costat[3] = false;
|
||||
tombe.x = cx;
|
||||
tombe.y = cy;
|
||||
}
|
||||
if (Info::ctx.num_piramide == 6) {
|
||||
return;
|
||||
}
|
||||
if (info::ctx.num_piramide == 6) return;
|
||||
this->tombes[0].contingut = CONTE_FARAO;
|
||||
this->tombes[1].contingut = CONTE_CLAU;
|
||||
this->tombes[2].contingut = CONTE_PERGAMI;
|
||||
this->tombes[3].contingut = CONTE_MOMIA;
|
||||
for (int i = 4; i < 8; i++) this->tombes[i].contingut = CONTE_RES;
|
||||
for (int i = 8; i < 16; i++) this->tombes[i].contingut = CONTE_TRESOR;
|
||||
for (int i = 4; i < 8; i++) {
|
||||
this->tombes[i].contingut = CONTE_RES;
|
||||
}
|
||||
for (int i = 8; i < 16; i++) {
|
||||
this->tombes[i].contingut = CONTE_TRESOR;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 50; i++) swap(this->tombes[rand() % 16].contingut, this->tombes[rand() % 16].contingut);
|
||||
for (int i = 0; i < 50; i++) {
|
||||
swap(this->tombes[rand() % 16].contingut, this->tombes[rand() % 16].contingut);
|
||||
}
|
||||
}
|
||||
|
||||
Uint8 minim(Uint8 a, Uint8 b) {
|
||||
auto minim(Uint8 a, Uint8 b) -> Uint8 {
|
||||
return (a < b) ? a : b;
|
||||
}
|
||||
|
||||
void Mapa::comprovaUltimCami() {
|
||||
Uint8 col_aux = abs(this->vertex.columna - this->ultim_vertex.columna);
|
||||
Uint8 fil_aux = abs(this->vertex.fila - this->ultim_vertex.fila);
|
||||
Uint8 col_aux = abs(this->vertex_.columna - this->ultim_vertex_.columna);
|
||||
Uint8 fil_aux = abs(this->vertex_.fila - this->ultim_vertex_.fila);
|
||||
|
||||
if (col_aux > fil_aux) { // Cam� horitzontal
|
||||
Uint8 cami_fila = this->vertex.fila;
|
||||
Uint8 cami_columna = minim(this->vertex.columna, this->ultim_vertex.columna);
|
||||
Uint8 cami_fila = this->vertex_.fila;
|
||||
Uint8 cami_columna = minim(this->vertex_.columna, this->ultim_vertex_.columna);
|
||||
|
||||
Sint8 caixa_avall = (cami_fila << 2) + cami_columna;
|
||||
Sint8 caixa_amunt = caixa_avall - 4;
|
||||
@@ -206,8 +223,8 @@ void Mapa::comprovaUltimCami() {
|
||||
this->comprovaCaixa(caixa_amunt);
|
||||
}
|
||||
} else { // Cam� vertical
|
||||
Uint8 cami_columna = this->vertex.columna;
|
||||
Uint8 cami_fila = minim(this->vertex.fila, this->ultim_vertex.fila);
|
||||
Uint8 cami_columna = this->vertex_.columna;
|
||||
Uint8 cami_fila = minim(this->vertex_.fila, this->ultim_vertex_.fila);
|
||||
|
||||
Sint8 caixa_dreta = (cami_fila << 2) + cami_columna;
|
||||
Sint8 caixa_esquerra = caixa_dreta - 1;
|
||||
@@ -225,11 +242,14 @@ void Mapa::comprovaUltimCami() {
|
||||
|
||||
void Mapa::comprovaCaixa(Uint8 num) {
|
||||
// Si la tomba ja està oberta, no hi ha res que mirar
|
||||
if (this->tombes[num].oberta) return;
|
||||
if (this->tombes[num].oberta) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Si algun costat encara no està passat, no hi ha res que fer
|
||||
for (int i = 0; i < 4; i++)
|
||||
if (!this->tombes[num].costat[i]) return;
|
||||
if (std::ranges::any_of(this->tombes[num].costat, [](bool c) { return !c; })) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sinó, pos la acabem d'obrir
|
||||
this->tombes[num].oberta = true;
|
||||
@@ -241,29 +261,33 @@ void Mapa::comprovaCaixa(Uint8 num) {
|
||||
break;
|
||||
case CONTE_TRESOR:
|
||||
this->tombes[num].x = 100;
|
||||
info::ctx.diners++;
|
||||
Info::ctx.diners++;
|
||||
break;
|
||||
case CONTE_FARAO:
|
||||
this->tombes[num].x = 150;
|
||||
this->farao = true;
|
||||
this->farao_ = true;
|
||||
break;
|
||||
case CONTE_CLAU:
|
||||
this->tombes[num].x = 200;
|
||||
this->clau = true;
|
||||
this->clau_ = true;
|
||||
break;
|
||||
case CONTE_MOMIA:
|
||||
this->tombes[num].y = 175;
|
||||
this->nova_momia = true;
|
||||
this->nova_momia_ = true;
|
||||
break;
|
||||
case CONTE_PERGAMI:
|
||||
this->tombes[num].x = 250;
|
||||
this->sam->pergami = true;
|
||||
this->sam_->pergami = true;
|
||||
break;
|
||||
case CONTE_DIAMANT:
|
||||
this->tombes[num].y = 70;
|
||||
info::ctx.diamants++;
|
||||
info::ctx.diners += VALOR_DIAMANT;
|
||||
if (info::ctx.diamants == 16) this->farao = this->clau = true;
|
||||
Info::ctx.diamants++;
|
||||
Info::ctx.diners += VALOR_DIAMANT;
|
||||
if (Info::ctx.diamants == 16) {
|
||||
this->farao_ = this->clau_ = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -271,8 +295,8 @@ void Mapa::comprovaCaixa(Uint8 num) {
|
||||
}
|
||||
|
||||
void Mapa::comprovaPorta() {
|
||||
if (this->clau && this->farao) {
|
||||
JD8_BlitCKToSurface(150, 18, this->gfx, 15, 143, 15, 12, this->fondo, 255);
|
||||
porta_oberta = true;
|
||||
if (this->clau_ && this->farao_) {
|
||||
Jd8::blitCKToSurface(150, 18, this->gfx_, 15, 143, 15, 12, this->fondo_, 255);
|
||||
porta_oberta_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
+27
-23
@@ -1,17 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
#include "game/info.hpp"
|
||||
#include "game/prota.hpp"
|
||||
|
||||
#define CONTE_RES 0
|
||||
#define CONTE_TRESOR 1
|
||||
#define CONTE_FARAO 2
|
||||
#define CONTE_CLAU 3
|
||||
#define CONTE_MOMIA 4
|
||||
#define CONTE_PERGAMI 5
|
||||
#define CONTE_DIAMANT 6
|
||||
#define VALOR_DIAMANT 5
|
||||
enum : std::uint8_t {
|
||||
CONTE_RES = 0,
|
||||
CONTE_TRESOR = 1,
|
||||
CONTE_FARAO = 2,
|
||||
CONTE_CLAU = 3,
|
||||
CONTE_MOMIA = 4,
|
||||
CONTE_PERGAMI = 5,
|
||||
CONTE_DIAMANT = 6,
|
||||
VALOR_DIAMANT = 5
|
||||
};
|
||||
|
||||
struct Tomba {
|
||||
bool costat[4];
|
||||
@@ -27,17 +31,17 @@ struct Vertex {
|
||||
|
||||
class Mapa {
|
||||
public:
|
||||
explicit Mapa(JD8_Surface gfx, Prota* sam);
|
||||
~Mapa(void);
|
||||
explicit Mapa(Jd8::Surface gfx, Prota* sam);
|
||||
~Mapa();
|
||||
|
||||
Mapa(const Mapa&) = delete;
|
||||
Mapa& operator=(const Mapa&) = delete;
|
||||
auto operator=(const Mapa&) -> Mapa& = delete;
|
||||
Mapa(Mapa&&) = delete;
|
||||
Mapa& operator=(Mapa&&) = delete;
|
||||
auto operator=(Mapa&&) -> Mapa& = delete;
|
||||
|
||||
void draw();
|
||||
void update();
|
||||
bool novaMomia();
|
||||
auto novaMomia() -> bool;
|
||||
void comprovaCaixa(Uint8 num);
|
||||
|
||||
Tomba tombes[16];
|
||||
@@ -49,16 +53,16 @@ class Mapa {
|
||||
void comprovaUltimCami();
|
||||
void comprovaPorta();
|
||||
|
||||
JD8_Surface gfx;
|
||||
JD8_Surface fondo;
|
||||
Vertex vertex;
|
||||
Vertex ultim_vertex;
|
||||
Uint8 frame_torxes;
|
||||
Jd8::Surface gfx_;
|
||||
Jd8::Surface fondo_;
|
||||
Vertex vertex_;
|
||||
Vertex ultim_vertex_;
|
||||
Uint8 frame_torxes_;
|
||||
|
||||
Prota* sam;
|
||||
Prota* sam_;
|
||||
|
||||
bool farao;
|
||||
bool clau;
|
||||
bool porta_oberta;
|
||||
bool nova_momia;
|
||||
bool farao_;
|
||||
bool clau_;
|
||||
bool porta_oberta_;
|
||||
bool nova_momia_;
|
||||
};
|
||||
|
||||
+28
-25
@@ -1,60 +1,63 @@
|
||||
#include "game/marcador.hpp"
|
||||
|
||||
Marcador::Marcador(JD8_Surface gfx, Prota* sam) {
|
||||
this->gfx = gfx;
|
||||
this->sam = sam;
|
||||
}
|
||||
|
||||
Marcador::~Marcador(void) {
|
||||
Marcador::Marcador(Jd8::Surface gfx, Prota* sam) {
|
||||
this->gfx_ = gfx;
|
||||
this->sam_ = sam;
|
||||
}
|
||||
|
||||
void Marcador::draw() {
|
||||
if (info::ctx.num_piramide < 6) {
|
||||
this->pintaNumero(55, 2, info::ctx.num_piramide);
|
||||
this->pintaNumero(80, 2, info::ctx.num_habitacio);
|
||||
if (Info::ctx.num_piramide < 6) {
|
||||
this->pintaNumero(55, 2, Info::ctx.num_piramide);
|
||||
this->pintaNumero(80, 2, Info::ctx.num_habitacio);
|
||||
}
|
||||
|
||||
this->pintaNumero(149, 2, info::ctx.diners / 100);
|
||||
this->pintaNumero(156, 2, (info::ctx.diners % 100) / 10);
|
||||
this->pintaNumero(163, 2, info::ctx.diners % 10);
|
||||
this->pintaNumero(149, 2, Info::ctx.diners / 100);
|
||||
this->pintaNumero(156, 2, (Info::ctx.diners % 100) / 10);
|
||||
this->pintaNumero(163, 2, Info::ctx.diners % 10);
|
||||
|
||||
if (this->sam->pergami) JD8_BlitCK(190, 1, this->gfx, 209, 185, 15, 14, 255);
|
||||
if (this->sam_->pergami) {
|
||||
Jd8::blitCK(190, 1, this->gfx_, 209, 185, 15, 14, 255);
|
||||
}
|
||||
|
||||
JD8_BlitCK(271, 1, this->gfx, 0, 20, 15, info::ctx.vida * 3, 255);
|
||||
if (info::ctx.vida < 5) JD8_BlitCK(271, 1 + (info::ctx.vida * 3), this->gfx, 75, 20, 15, 15 - (info::ctx.vida * 3), 255);
|
||||
Jd8::blitCK(271, 1, this->gfx_, 0, 20, 15, Info::ctx.vida * 3, 255);
|
||||
if (Info::ctx.vida < 5) {
|
||||
Jd8::blitCK(271, 1 + (Info::ctx.vida * 3), this->gfx_, 75, 20, 15, 15 - (Info::ctx.vida * 3), 255);
|
||||
}
|
||||
}
|
||||
|
||||
void Marcador::pintaNumero(Uint16 x, Uint16 y, Uint8 num) {
|
||||
switch (num) {
|
||||
case 0:
|
||||
JD8_BlitCK(x, y, this->gfx, 141, 193, 10, 7, 255);
|
||||
Jd8::blitCK(x, y, this->gfx_, 141, 193, 10, 7, 255);
|
||||
break;
|
||||
case 1:
|
||||
JD8_BlitCK(x, y, this->gfx, 100, 185, 10, 7, 255);
|
||||
Jd8::blitCK(x, y, this->gfx_, 100, 185, 10, 7, 255);
|
||||
break;
|
||||
case 2:
|
||||
JD8_BlitCK(x, y, this->gfx, 110, 185, 10, 7, 255);
|
||||
Jd8::blitCK(x, y, this->gfx_, 110, 185, 10, 7, 255);
|
||||
break;
|
||||
case 3:
|
||||
JD8_BlitCK(x, y, this->gfx, 120, 185, 10, 7, 255);
|
||||
Jd8::blitCK(x, y, this->gfx_, 120, 185, 10, 7, 255);
|
||||
break;
|
||||
case 4:
|
||||
JD8_BlitCK(x, y, this->gfx, 130, 185, 10, 7, 255);
|
||||
Jd8::blitCK(x, y, this->gfx_, 130, 185, 10, 7, 255);
|
||||
break;
|
||||
case 5:
|
||||
JD8_BlitCK(x, y, this->gfx, 140, 185, 10, 7, 255);
|
||||
Jd8::blitCK(x, y, this->gfx_, 140, 185, 10, 7, 255);
|
||||
break;
|
||||
case 6:
|
||||
JD8_BlitCK(x, y, this->gfx, 101, 193, 10, 7, 255);
|
||||
Jd8::blitCK(x, y, this->gfx_, 101, 193, 10, 7, 255);
|
||||
break;
|
||||
case 7:
|
||||
JD8_BlitCK(x, y, this->gfx, 111, 193, 10, 7, 255);
|
||||
Jd8::blitCK(x, y, this->gfx_, 111, 193, 10, 7, 255);
|
||||
break;
|
||||
case 8:
|
||||
JD8_BlitCK(x, y, this->gfx, 121, 193, 10, 7, 255);
|
||||
Jd8::blitCK(x, y, this->gfx_, 121, 193, 10, 7, 255);
|
||||
break;
|
||||
case 9:
|
||||
JD8_BlitCK(x, y, this->gfx, 131, 193, 10, 7, 255);
|
||||
Jd8::blitCK(x, y, this->gfx_, 131, 193, 10, 7, 255);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
|
||||
class Marcador {
|
||||
public:
|
||||
explicit Marcador(JD8_Surface gfx, Prota* sam);
|
||||
~Marcador(void);
|
||||
explicit Marcador(Jd8::Surface gfx, Prota* sam);
|
||||
~Marcador() = default;
|
||||
|
||||
void draw();
|
||||
|
||||
protected:
|
||||
void pintaNumero(Uint16 x, Uint16 y, Uint8 num);
|
||||
|
||||
JD8_Surface gfx;
|
||||
Prota* sam;
|
||||
Jd8::Surface gfx_;
|
||||
Prota* sam_;
|
||||
};
|
||||
|
||||
+101
-79
@@ -8,66 +8,72 @@
|
||||
#include "core/jail/jinput.hpp"
|
||||
|
||||
ModuleGame::ModuleGame() {
|
||||
this->gfx = JD8_LoadSurface(info::ctx.pepe_activat ? "gfx/frames2.gif" : "gfx/frames.gif");
|
||||
JG_SetUpdateTicks(10);
|
||||
this->gfx_ = Jd8::loadSurface(Info::ctx.pepe_activat ? "gfx/frames2.gif" : "gfx/frames.gif");
|
||||
Jg::setUpdateTicks(10);
|
||||
|
||||
this->sam = std::make_unique<Prota>(this->gfx);
|
||||
this->mapa = std::make_unique<Mapa>(this->gfx, this->sam.get());
|
||||
this->marcador = std::make_unique<Marcador>(this->gfx, this->sam.get());
|
||||
if (info::ctx.num_piramide == 2) {
|
||||
this->bola = std::make_unique<Bola>(this->gfx, this->sam.get());
|
||||
this->sam_ = std::make_unique<Prota>(this->gfx_);
|
||||
this->mapa_ = std::make_unique<Mapa>(this->gfx_, this->sam_.get());
|
||||
this->marcador_ = std::make_unique<Marcador>(this->gfx_, this->sam_.get());
|
||||
if (Info::ctx.num_piramide == 2) {
|
||||
this->bola_ = std::make_unique<Bola>(this->gfx_, this->sam_.get());
|
||||
}
|
||||
|
||||
this->iniciarMomies();
|
||||
}
|
||||
|
||||
ModuleGame::~ModuleGame() {
|
||||
JD8_FreeSurface(this->gfx);
|
||||
Jd8::freeSurface(this->gfx_);
|
||||
}
|
||||
|
||||
void ModuleGame::onEnter() {
|
||||
// Primera Draw per omplir `screen` amb el contingut del gameplay
|
||||
// abans que el fade-in arranque. Si no, les primeres iteracions del
|
||||
// fade interpolarien cap a una paleta amb pantalla buida.
|
||||
this->Draw();
|
||||
this->draw();
|
||||
|
||||
// Audio::playMusic ja és idempotent: si la pista actual coincideix amb la
|
||||
// demanada, no fa res. Per això podem cridar-lo cada onEnter sense
|
||||
// desencadenar restarts indesitjats.
|
||||
const char* music_name = info::ctx.num_piramide == 3 ? "piramide_3.ogg"
|
||||
: info::ctx.num_piramide == 2 ? "piramide_2.ogg"
|
||||
: info::ctx.num_piramide == 6 ? "secreta.ogg"
|
||||
: "piramide_1_4_5.ogg";
|
||||
const char* music_name = "piramide_1_4_5.ogg";
|
||||
if (Info::ctx.num_piramide == 3) {
|
||||
music_name = "piramide_3.ogg";
|
||||
} else if (Info::ctx.num_piramide == 2) {
|
||||
music_name = "piramide_2.ogg";
|
||||
} else if (Info::ctx.num_piramide == 6) {
|
||||
music_name = "secreta.ogg";
|
||||
}
|
||||
Audio::get()->playMusic(music_name);
|
||||
|
||||
// Arranca el fade-in tick-based. El `PaletteFade` avança un pas (de
|
||||
// 32) per cada tick; durant aquesta fase el gameplay no corre,
|
||||
// només Draw+fade. Substituïx la crida bloquejant `JD8_FadeToPal`.
|
||||
fade_.startFadeTo(JD8_LoadPalette(info::ctx.pepe_activat ? "gfx/frames2.gif" : "gfx/frames.gif"));
|
||||
phase_ = Phase::FadingIn;
|
||||
fade_.startFadeTo(Jd8::loadPalette(Info::ctx.pepe_activat ? "gfx/frames2.gif" : "gfx/frames.gif"));
|
||||
phase_ = Phase::FADING_IN;
|
||||
}
|
||||
|
||||
void ModuleGame::tick(int delta_ms) {
|
||||
switch (phase_) {
|
||||
case Phase::FadingIn:
|
||||
case Phase::FADING_IN:
|
||||
// No redibuixem durant el fade: el `screen` ja va ser omplit
|
||||
// per la Draw() d'onEnter. Només el JD8_Flip del caller muta
|
||||
// per la Draw() d'onEnter. Només el Jd8::flip del caller muta
|
||||
// pixel_data segons la paleta que avança pas a pas.
|
||||
fade_.tick(delta_ms);
|
||||
if (fade_.done()) phase_ = Phase::Playing;
|
||||
break;
|
||||
|
||||
case Phase::Playing:
|
||||
this->Draw();
|
||||
this->Update();
|
||||
if (this->final_ != 0) {
|
||||
this->applyFinalTransitions();
|
||||
fade_.startFadeOut();
|
||||
phase_ = Phase::FadingOut;
|
||||
if (fade_.done()) {
|
||||
phase_ = Phase::PLAYING;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::FadingOut:
|
||||
case Phase::PLAYING:
|
||||
this->draw();
|
||||
this->update();
|
||||
if (this->final_ != 0) {
|
||||
this->applyFinalTransitions();
|
||||
fade_.startFadeOut();
|
||||
phase_ = Phase::FADING_OUT;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::FADING_OUT:
|
||||
// No redibuixem: el `screen` té l'últim frame pintat per la
|
||||
// fase Playing (just abans que Update() setegés `final_`).
|
||||
// El vell `JD8_FadeOut` feia exactament això — flips amb
|
||||
@@ -75,95 +81,111 @@ void ModuleGame::tick(int delta_ms) {
|
||||
// mostraria l'estat post-Update del sprite (p.ex. el prota
|
||||
// "tornant" davant la porta després d'haver eixit).
|
||||
fade_.tick(delta_ms);
|
||||
if (fade_.done()) phase_ = Phase::Done;
|
||||
if (fade_.done()) {
|
||||
phase_ = Phase::DONE;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::Done:
|
||||
case Phase::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int ModuleGame::nextState() const {
|
||||
if (JG_Quitting()) return -1;
|
||||
if (info::ctx.num_habitacio == 1 ||
|
||||
info::ctx.num_piramide == 100 ||
|
||||
info::ctx.num_piramide == 7) {
|
||||
auto ModuleGame::nextState() const -> int {
|
||||
if (Jg::quitting()) {
|
||||
return -1;
|
||||
}
|
||||
if (Info::ctx.num_habitacio == 1 ||
|
||||
Info::ctx.num_piramide == 100 ||
|
||||
Info::ctx.num_piramide == 7) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ModuleGame::applyFinalTransitions() {
|
||||
void ModuleGame::applyFinalTransitions() const {
|
||||
if (this->final_ == 1) {
|
||||
info::ctx.num_habitacio++;
|
||||
if (info::ctx.num_habitacio == 6) {
|
||||
info::ctx.num_habitacio = 1;
|
||||
info::ctx.num_piramide++;
|
||||
Info::ctx.num_habitacio++;
|
||||
if (Info::ctx.num_habitacio == 6) {
|
||||
Info::ctx.num_habitacio = 1;
|
||||
Info::ctx.num_piramide++;
|
||||
}
|
||||
if (Info::ctx.num_piramide == 6 && Info::ctx.num_habitacio == 2) {
|
||||
Info::ctx.num_piramide++;
|
||||
}
|
||||
if (info::ctx.num_piramide == 6 && info::ctx.num_habitacio == 2) info::ctx.num_piramide++;
|
||||
} else if (this->final_ == 2) {
|
||||
info::ctx.num_piramide = 100;
|
||||
Info::ctx.num_piramide = 100;
|
||||
}
|
||||
}
|
||||
|
||||
void ModuleGame::Draw() {
|
||||
// No crida JD8_Flip — el caller (mini-loop del fiber, o Director a
|
||||
void ModuleGame::draw() {
|
||||
// No crida Jd8::flip — el caller (mini-loop del fiber, o Director a
|
||||
// Phase B.2) ho fa després de cada tick.
|
||||
this->mapa->draw();
|
||||
this->marcador->draw();
|
||||
this->sam->draw();
|
||||
for (auto& m : this->momies) m->draw();
|
||||
if (this->bola) this->bola->draw();
|
||||
this->mapa_->draw();
|
||||
this->marcador_->draw();
|
||||
this->sam_->draw();
|
||||
for (auto& m : this->momies_) {
|
||||
m->draw();
|
||||
}
|
||||
if (this->bola_) {
|
||||
this->bola_->draw();
|
||||
}
|
||||
}
|
||||
|
||||
void ModuleGame::Update() {
|
||||
if (JG_ShouldUpdate()) {
|
||||
JI_Update();
|
||||
void ModuleGame::update() {
|
||||
if (Jg::shouldUpdate()) {
|
||||
Ji::update();
|
||||
|
||||
this->final_ = this->sam->update();
|
||||
const auto erased = std::erase_if(this->momies, [](auto& m) { return m->update(); });
|
||||
info::ctx.momies -= static_cast<int>(erased);
|
||||
if (this->bola) this->bola->update();
|
||||
this->mapa->update();
|
||||
if (this->mapa->novaMomia()) {
|
||||
this->momies.emplace_back(std::make_unique<Momia>(this->gfx, true, 0, 0, this->sam.get()));
|
||||
info::ctx.momies++;
|
||||
this->final_ = this->sam_->update();
|
||||
const auto ERASED = std::erase_if(this->momies_, [](auto& m) { return m->update(); });
|
||||
Info::ctx.momies -= static_cast<int>(ERASED);
|
||||
if (this->bola_) {
|
||||
this->bola_->update();
|
||||
}
|
||||
this->mapa_->update();
|
||||
if (this->mapa_->novaMomia()) {
|
||||
this->momies_.emplace_back(std::make_unique<Momia>(this->gfx_, true, 0, 0, this->sam_.get()));
|
||||
Info::ctx.momies++;
|
||||
}
|
||||
|
||||
if (JI_CheatActivated("reviu")) info::ctx.vida = 5;
|
||||
if (JI_CheatActivated("alone")) {
|
||||
this->momies.clear();
|
||||
info::ctx.momies = 0;
|
||||
if (Ji::cheatActivated("reviu")) {
|
||||
Info::ctx.vida = 5;
|
||||
}
|
||||
if (JI_CheatActivated("obert")) {
|
||||
if (Ji::cheatActivated("alone")) {
|
||||
this->momies_.clear();
|
||||
Info::ctx.momies = 0;
|
||||
}
|
||||
if (Ji::cheatActivated("obert")) {
|
||||
for (int i = 0; i < 16; i++) {
|
||||
this->mapa->tombes[i].costat[0] = true;
|
||||
this->mapa->tombes[i].costat[1] = true;
|
||||
this->mapa->tombes[i].costat[2] = true;
|
||||
this->mapa->tombes[i].costat[3] = true;
|
||||
this->mapa->comprovaCaixa(i);
|
||||
this->mapa_->tombes[i].costat[0] = true;
|
||||
this->mapa_->tombes[i].costat[1] = true;
|
||||
this->mapa_->tombes[i].costat[2] = true;
|
||||
this->mapa_->tombes[i].costat[3] = true;
|
||||
this->mapa_->comprovaCaixa(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (JI_KeyPressed(SDL_SCANCODE_ESCAPE)) {
|
||||
JG_QuitSignal();
|
||||
if (Ji::keyPressed(SDL_SCANCODE_ESCAPE)) {
|
||||
Jg::quitSignal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ModuleGame::iniciarMomies() {
|
||||
if (info::ctx.num_habitacio == 1) {
|
||||
info::ctx.momies = 1;
|
||||
if (Info::ctx.num_habitacio == 1) {
|
||||
Info::ctx.momies = 1;
|
||||
} else {
|
||||
info::ctx.momies++;
|
||||
Info::ctx.momies++;
|
||||
}
|
||||
if (Info::ctx.num_piramide == 6) {
|
||||
Info::ctx.momies = 8;
|
||||
}
|
||||
if (info::ctx.num_piramide == 6) info::ctx.momies = 8;
|
||||
|
||||
int x = 20;
|
||||
int y = 170;
|
||||
bool dimonis = info::ctx.num_piramide == 6;
|
||||
for (int i = 0; i < info::ctx.momies; i++) {
|
||||
this->momies.emplace_back(std::make_unique<Momia>(this->gfx, dimonis, x, y, this->sam.get()));
|
||||
bool dimonis = Info::ctx.num_piramide == 6;
|
||||
for (int i = 0; i < Info::ctx.momies; i++) {
|
||||
this->momies_.emplace_back(std::make_unique<Momia>(this->gfx_, dimonis, x, y, this->sam_.get()));
|
||||
x += 65;
|
||||
if (x == 345) {
|
||||
x = 20;
|
||||
|
||||
+29
-28
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
@@ -9,60 +10,60 @@
|
||||
#include "game/marcador.hpp"
|
||||
#include "game/momia.hpp"
|
||||
#include "game/prota.hpp"
|
||||
#include "scenes/palette_fade.hpp"
|
||||
#include "scenes/scene.hpp"
|
||||
#include "game/scenes/palette_fade.hpp"
|
||||
#include "game/scenes/scene.hpp"
|
||||
|
||||
// Escena de gameplay pur. Reemplaça el vell `Go()` bloquejant amb
|
||||
// l'interfície `scenes::Scene` tick-based: `onEnter()` arranca la
|
||||
// l'interfície `Scenes::Scene` tick-based: `onEnter()` arranca la
|
||||
// música i un fade-in, el `tick()` avança un frame (Draw + Update
|
||||
// gated per JG_ShouldUpdate), i quan la partida acaba fa un fade-out
|
||||
// gated per Jg::shouldUpdate), i quan la partida acaba fa un fade-out
|
||||
// abans de retornar el next state.
|
||||
//
|
||||
// Tres fases internes:
|
||||
// 1. FadingIn — fade-in 32 passos mentre el render segueix viu.
|
||||
// 1. FADING_IN — fade-in 32 passos mentre el render segueix viu.
|
||||
// 2. Playing — gameplay normal; `final_` es setja quan el prota mor
|
||||
// o canvia de sala. `Update()` només avança cada 10 ms
|
||||
// via `JG_ShouldUpdate` (ticker fix del jail).
|
||||
// 3. FadingOut — fade-out 32 passos mantenint l'últim frame visible
|
||||
// via `Jg::shouldUpdate` (ticker fix del jail).
|
||||
// 3. FADING_OUT — fade-out 32 passos mantenint l'últim frame visible
|
||||
// (substituïx el `JD8_FadeOut` bloquejant que feia el
|
||||
// destructor legacy).
|
||||
class ModuleGame : public scenes::Scene {
|
||||
class ModuleGame : public Scenes::Scene {
|
||||
public:
|
||||
ModuleGame();
|
||||
~ModuleGame() override;
|
||||
|
||||
ModuleGame(const ModuleGame&) = delete;
|
||||
ModuleGame& operator=(const ModuleGame&) = delete;
|
||||
auto operator=(const ModuleGame&) -> ModuleGame& = delete;
|
||||
ModuleGame(ModuleGame&&) = delete;
|
||||
ModuleGame& operator=(ModuleGame&&) = delete;
|
||||
auto operator=(ModuleGame&&) -> ModuleGame& = delete;
|
||||
|
||||
void onEnter() override;
|
||||
void tick(int delta_ms) override;
|
||||
bool done() const override { return phase_ == Phase::Done; }
|
||||
int nextState() const override;
|
||||
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::DONE; }
|
||||
[[nodiscard]] auto nextState() const -> int override;
|
||||
|
||||
private:
|
||||
enum class Phase {
|
||||
FadingIn,
|
||||
Playing,
|
||||
FadingOut,
|
||||
Done,
|
||||
enum class Phase : std::uint8_t {
|
||||
FADING_IN,
|
||||
PLAYING,
|
||||
FADING_OUT,
|
||||
DONE,
|
||||
};
|
||||
|
||||
void Draw(); // render a `screen`; no crida JD8_Flip (ho fa el caller)
|
||||
void Update(); // gated per JG_ShouldUpdate
|
||||
void draw(); // render a `screen`; no crida Jd8::flip (ho fa el caller)
|
||||
void update(); // gated per Jg::shouldUpdate
|
||||
|
||||
void iniciarMomies();
|
||||
void applyFinalTransitions(); // muta info::ctx quan final_ passa a !=0
|
||||
void applyFinalTransitions() const; // muta Info::ctx quan final_ passa a !=0
|
||||
|
||||
Phase phase_{Phase::FadingIn};
|
||||
scenes::PaletteFade fade_;
|
||||
Phase phase_{Phase::FADING_IN};
|
||||
Scenes::PaletteFade fade_;
|
||||
Uint8 final_{0};
|
||||
JD8_Surface gfx{nullptr};
|
||||
Jd8::Surface gfx_{nullptr};
|
||||
|
||||
std::unique_ptr<Mapa> mapa;
|
||||
std::unique_ptr<Prota> sam;
|
||||
std::unique_ptr<Marcador> marcador;
|
||||
std::vector<std::unique_ptr<Momia>> momies;
|
||||
std::unique_ptr<Bola> bola;
|
||||
std::unique_ptr<Mapa> mapa_;
|
||||
std::unique_ptr<Prota> sam_;
|
||||
std::unique_ptr<Marcador> marcador_;
|
||||
std::vector<std::unique_ptr<Momia>> momies_;
|
||||
std::unique_ptr<Bola> bola_;
|
||||
};
|
||||
|
||||
+123
-92
@@ -1,13 +1,13 @@
|
||||
#include "game/momia.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "core/jail/jgame.hpp"
|
||||
|
||||
Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
|
||||
Momia::Momia(Jd8::Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
|
||||
: Sprite(gfx) {
|
||||
this->dimoni = dimoni;
|
||||
this->sam = sam;
|
||||
this->sam_ = sam;
|
||||
|
||||
entitat.frames.reserve(20);
|
||||
for (int row = 0; row < 4; row++) {
|
||||
@@ -15,9 +15,13 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
|
||||
Frame f;
|
||||
f.w = 15;
|
||||
f.h = 15;
|
||||
if (info::ctx.num_piramide == 4) f.h -= 5;
|
||||
if (Info::ctx.num_piramide == 4) {
|
||||
f.h -= 5;
|
||||
}
|
||||
f.x = (col * 15) + 75;
|
||||
if (this->dimoni) f.x += 75;
|
||||
if (this->dimoni) {
|
||||
f.x += 75;
|
||||
}
|
||||
f.y = 20 + (row * 15);
|
||||
entitat.frames.push_back(f);
|
||||
}
|
||||
@@ -26,20 +30,20 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
|
||||
entitat.animacions.resize(4);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
entitat.animacions[i].frames = {
|
||||
static_cast<Uint8>(0 + i * 5),
|
||||
static_cast<Uint8>(1 + i * 5),
|
||||
static_cast<Uint8>(2 + i * 5),
|
||||
static_cast<Uint8>(1 + i * 5),
|
||||
static_cast<Uint8>(0 + i * 5),
|
||||
static_cast<Uint8>(3 + i * 5),
|
||||
static_cast<Uint8>(4 + i * 5),
|
||||
static_cast<Uint8>(3 + i * 5),
|
||||
static_cast<Uint8>(0 + (i * 5)),
|
||||
static_cast<Uint8>(1 + (i * 5)),
|
||||
static_cast<Uint8>(2 + (i * 5)),
|
||||
static_cast<Uint8>(1 + (i * 5)),
|
||||
static_cast<Uint8>(0 + (i * 5)),
|
||||
static_cast<Uint8>(3 + (i * 5)),
|
||||
static_cast<Uint8>(4 + (i * 5)),
|
||||
static_cast<Uint8>(3 + (i * 5)),
|
||||
};
|
||||
}
|
||||
|
||||
this->cur_frame = 0;
|
||||
this->o = rand() % 4;
|
||||
this->cycles_per_frame = 4;
|
||||
this->cycles_per_frame_ = 4;
|
||||
|
||||
if (this->dimoni) {
|
||||
if (x == 0) {
|
||||
@@ -48,7 +52,7 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
|
||||
this->x = x;
|
||||
}
|
||||
if (y == 0) {
|
||||
if (this->sam->y > 100) {
|
||||
if (this->sam_->y > 100) {
|
||||
this->y = 30;
|
||||
} else {
|
||||
this->y = 170;
|
||||
@@ -56,7 +60,7 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
|
||||
} else {
|
||||
this->y = y;
|
||||
}
|
||||
this->engendro = std::make_unique<Engendro>(gfx, this->x, this->y);
|
||||
this->engendro_ = std::make_unique<Engendro>(gfx, this->x, this->y);
|
||||
} else {
|
||||
this->x = x;
|
||||
this->y = y;
|
||||
@@ -64,90 +68,117 @@ Momia::Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam)
|
||||
}
|
||||
|
||||
void Momia::draw() {
|
||||
if (this->engendro) {
|
||||
this->engendro->draw();
|
||||
if (this->engendro_) {
|
||||
this->engendro_->draw();
|
||||
} else {
|
||||
Sprite::draw();
|
||||
|
||||
if (info::ctx.num_piramide == 4) {
|
||||
if ((JG_GetCycleCounter() % 40) < 20) {
|
||||
JD8_BlitCK(this->x, this->y, this->gfx, 220, 80, 15, 15, 255);
|
||||
if (Info::ctx.num_piramide == 4) {
|
||||
if ((Jg::getCycleCounter() % 40) < 20) {
|
||||
Jd8::blitCK(this->x, this->y, this->gfx_, 220, 80, 15, 15, 255);
|
||||
} else {
|
||||
JD8_BlitCK(this->x, this->y, this->gfx, 235, 80, 15, 15, 255);
|
||||
Jd8::blitCK(this->x, this->y, this->gfx_, 235, 80, 15, 15, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Momia::update() {
|
||||
bool morta = false;
|
||||
|
||||
if (this->engendro) {
|
||||
if (this->engendro->update()) {
|
||||
this->engendro.reset();
|
||||
}
|
||||
return morta;
|
||||
void Momia::pickHorizontalThenVertical() {
|
||||
if (this->x > this->sam_->x) {
|
||||
this->o = 3;
|
||||
} else if (this->x < this->sam_->x) {
|
||||
this->o = 2;
|
||||
} else if (this->y < this->sam_->y) {
|
||||
this->o = 0;
|
||||
} else if (this->y > this->sam_->y) {
|
||||
this->o = 1;
|
||||
}
|
||||
|
||||
if (this->sam->o < 4 && (this->dimoni || info::ctx.num_piramide == 5 || JG_GetCycleCounter() % 2 == 0)) {
|
||||
if ((this->x - 20) % 65 == 0 && (this->y - 30) % 35 == 0) {
|
||||
if (this->dimoni) {
|
||||
if (rand() % 2 == 0) {
|
||||
if (this->x > this->sam->x) {
|
||||
this->o = 3;
|
||||
} else if (this->x < this->sam->x) {
|
||||
this->o = 2;
|
||||
} else if (this->y < this->sam->y) {
|
||||
this->o = 0;
|
||||
} else if (this->y > this->sam->y) {
|
||||
this->o = 1;
|
||||
}
|
||||
} else {
|
||||
if (this->y < this->sam->y) {
|
||||
this->o = 0;
|
||||
} else if (this->y > this->sam->y) {
|
||||
this->o = 1;
|
||||
} else if (this->x > this->sam->x) {
|
||||
this->o = 3;
|
||||
} else if (this->x < this->sam->x) {
|
||||
this->o = 2;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this->o = rand() % 4;
|
||||
}
|
||||
}
|
||||
|
||||
switch (this->o) {
|
||||
case 0:
|
||||
if (y < 170) this->y++;
|
||||
break;
|
||||
case 1:
|
||||
if (y > 30) this->y--;
|
||||
break;
|
||||
case 2:
|
||||
if (x < 280) this->x++;
|
||||
break;
|
||||
case 3:
|
||||
if (x > 20) this->x--;
|
||||
break;
|
||||
}
|
||||
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == entitat.animacions[this->o].frames.size()) this->cur_frame = 0;
|
||||
}
|
||||
|
||||
if (this->x > (this->sam->x - 7) && this->x < (this->sam->x + 7) && this->y > (this->sam->y - 7) && this->y < (this->sam->y + 7)) {
|
||||
morta = true;
|
||||
if (this->sam->pergami) {
|
||||
this->sam->pergami = false;
|
||||
} else {
|
||||
info::ctx.vida--;
|
||||
if (info::ctx.vida == 0) this->sam->o = 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return morta;
|
||||
}
|
||||
|
||||
void Momia::pickVerticalThenHorizontal() {
|
||||
if (this->y < this->sam_->y) {
|
||||
this->o = 0;
|
||||
} else if (this->y > this->sam_->y) {
|
||||
this->o = 1;
|
||||
} else if (this->x > this->sam_->x) {
|
||||
this->o = 3;
|
||||
} else if (this->x < this->sam_->x) {
|
||||
this->o = 2;
|
||||
}
|
||||
}
|
||||
|
||||
void Momia::pickDirection() {
|
||||
if (!this->dimoni) {
|
||||
this->o = rand() % 4;
|
||||
return;
|
||||
}
|
||||
if (rand() % 2 == 0) {
|
||||
pickHorizontalThenVertical();
|
||||
} else {
|
||||
pickVerticalThenHorizontal();
|
||||
}
|
||||
}
|
||||
|
||||
void Momia::stepInDirection() {
|
||||
switch (this->o) {
|
||||
case 0:
|
||||
if (y < 170) { this->y++; }
|
||||
break;
|
||||
case 1:
|
||||
if (y > 30) { this->y--; }
|
||||
break;
|
||||
case 2:
|
||||
if (x < 280) { this->x++; }
|
||||
break;
|
||||
case 3:
|
||||
if (x > 20) { this->x--; }
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto Momia::collidesWithSam() const -> bool {
|
||||
return this->x > (this->sam_->x - 7) && this->x < (this->sam_->x + 7) &&
|
||||
this->y > (this->sam_->y - 7) && this->y < (this->sam_->y + 7);
|
||||
}
|
||||
|
||||
void Momia::applyCollisionWithSam() {
|
||||
if (this->sam_->pergami) {
|
||||
this->sam_->pergami = false;
|
||||
return;
|
||||
}
|
||||
Info::ctx.vida--;
|
||||
if (Info::ctx.vida == 0) {
|
||||
this->sam_->o = 5;
|
||||
}
|
||||
}
|
||||
|
||||
auto Momia::update() -> bool {
|
||||
if (this->engendro_) {
|
||||
if (this->engendro_->update()) {
|
||||
this->engendro_.reset();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const bool SAM_ALIVE = this->sam_->o < 4;
|
||||
const bool MAY_STEP = this->dimoni || Info::ctx.num_piramide == 5 || Jg::getCycleCounter() % 2 == 0;
|
||||
if (!SAM_ALIVE || !MAY_STEP) {
|
||||
return false;
|
||||
}
|
||||
if ((this->x - 20) % 65 == 0 && (this->y - 30) % 35 == 0) {
|
||||
pickDirection();
|
||||
}
|
||||
stepInDirection();
|
||||
if (Jg::getCycleCounter() % this->cycles_per_frame_ == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
|
||||
this->cur_frame = 0;
|
||||
}
|
||||
}
|
||||
if (collidesWithSam()) {
|
||||
applyCollisionWithSam();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
+11
-4
@@ -9,14 +9,21 @@
|
||||
|
||||
class Momia : public Sprite {
|
||||
public:
|
||||
explicit Momia(JD8_Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam);
|
||||
explicit Momia(Jd8::Surface gfx, bool dimoni, Uint16 x, Uint16 y, Prota* sam);
|
||||
|
||||
void draw() override;
|
||||
bool update();
|
||||
auto update() -> bool;
|
||||
|
||||
bool dimoni;
|
||||
|
||||
protected:
|
||||
Prota* sam;
|
||||
std::unique_ptr<Engendro> engendro;
|
||||
Prota* sam_;
|
||||
std::unique_ptr<Engendro> engendro_;
|
||||
|
||||
void pickDirection();
|
||||
void pickHorizontalThenVertical();
|
||||
void pickVerticalThenHorizontal();
|
||||
void stepInDirection();
|
||||
[[nodiscard]] auto collidesWithSam() const -> bool;
|
||||
void applyCollisionWithSam();
|
||||
};
|
||||
|
||||
+150
-89
@@ -1,5 +1,6 @@
|
||||
#include "game/options.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
@@ -58,16 +59,21 @@ namespace Options {
|
||||
auto yaml = fkyaml::node::deserialize(content);
|
||||
if (yaml.contains("game")) {
|
||||
const auto& node = yaml["game"];
|
||||
if (node.contains("habitacio_inicial"))
|
||||
if (node.contains("habitacio_inicial")) {
|
||||
game.habitacio_inicial = node["habitacio_inicial"].get_value<int>();
|
||||
if (node.contains("piramide_inicial"))
|
||||
}
|
||||
if (node.contains("piramide_inicial")) {
|
||||
game.piramide_inicial = node["piramide_inicial"].get_value<int>();
|
||||
if (node.contains("vides"))
|
||||
}
|
||||
if (node.contains("vides")) {
|
||||
game.vides = node["vides"].get_value<int>();
|
||||
if (node.contains("diamants_inicial"))
|
||||
}
|
||||
if (node.contains("diamants_inicial")) {
|
||||
game.diamants_inicial = node["diamants_inicial"].get_value<int>();
|
||||
if (node.contains("diners_inicial"))
|
||||
}
|
||||
if (node.contains("diners_inicial")) {
|
||||
game.diners_inicial = node["diners_inicial"].get_value<int>();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (const fkyaml::exception& e) {
|
||||
@@ -80,7 +86,9 @@ namespace Options {
|
||||
// com a punt d'entrada únic per als callsites legacy del menú; el cos
|
||||
// ja no toca jail_audio directament.
|
||||
void applyAudio() {
|
||||
if (::Audio::get() == nullptr) return;
|
||||
if (::Audio::get() == nullptr) {
|
||||
return;
|
||||
}
|
||||
::Audio::get()->enable(audio.enabled);
|
||||
::Audio::get()->enableMusic(audio.music.enabled);
|
||||
::Audio::get()->enableSound(audio.sound.enabled);
|
||||
@@ -91,114 +99,142 @@ namespace Options {
|
||||
// --- Funcions helper de càrrega ---
|
||||
|
||||
static void loadAudioConfigFromYaml(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("audio")) return;
|
||||
if (!yaml.contains("audio")) {
|
||||
return;
|
||||
}
|
||||
const auto& node = yaml["audio"];
|
||||
|
||||
if (node.contains("enabled"))
|
||||
if (node.contains("enabled")) {
|
||||
audio.enabled = node["enabled"].get_value<bool>();
|
||||
}
|
||||
|
||||
if (node.contains("volume"))
|
||||
if (node.contains("volume")) {
|
||||
audio.volume = node["volume"].get_value<float>();
|
||||
}
|
||||
|
||||
if (node.contains("music")) {
|
||||
const auto& music = node["music"];
|
||||
if (music.contains("enabled"))
|
||||
if (music.contains("enabled")) {
|
||||
audio.music.enabled = music["enabled"].get_value<bool>();
|
||||
if (music.contains("volume"))
|
||||
}
|
||||
if (music.contains("volume")) {
|
||||
audio.music.volume = music["volume"].get_value<float>();
|
||||
}
|
||||
}
|
||||
|
||||
if (node.contains("sound")) {
|
||||
const auto& sound = node["sound"];
|
||||
if (sound.contains("enabled"))
|
||||
if (sound.contains("enabled")) {
|
||||
audio.sound.enabled = sound["enabled"].get_value<bool>();
|
||||
if (sound.contains("volume"))
|
||||
}
|
||||
if (sound.contains("volume")) {
|
||||
audio.sound.volume = sound["volume"].get_value<float>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void loadVideoConfigFromYaml(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("video")) return;
|
||||
if (!yaml.contains("video")) {
|
||||
return;
|
||||
}
|
||||
const auto& node = yaml["video"];
|
||||
|
||||
if (node.contains("gpu_acceleration"))
|
||||
if (node.contains("gpu_acceleration")) {
|
||||
video.gpu_acceleration = node["gpu_acceleration"].get_value<bool>();
|
||||
if (node.contains("shader_enabled"))
|
||||
}
|
||||
if (node.contains("shader_enabled")) {
|
||||
video.shader_enabled = node["shader_enabled"].get_value<bool>();
|
||||
if (node.contains("supersampling"))
|
||||
video.supersampling = node["supersampling"].get_value<bool>();
|
||||
}
|
||||
if (node.contains("scaling_mode")) {
|
||||
auto s = node["scaling_mode"].get_value<std::string>();
|
||||
if (s == "disabled")
|
||||
if (s == "disabled") {
|
||||
video.scaling_mode = ScalingMode::DISABLED;
|
||||
else if (s == "stretch")
|
||||
} else if (s == "stretch") {
|
||||
video.scaling_mode = ScalingMode::STRETCH;
|
||||
else if (s == "letterbox")
|
||||
} else if (s == "letterbox") {
|
||||
video.scaling_mode = ScalingMode::LETTERBOX;
|
||||
else if (s == "overscan")
|
||||
} else if (s == "overscan") {
|
||||
video.scaling_mode = ScalingMode::OVERSCAN;
|
||||
else
|
||||
} else {
|
||||
video.scaling_mode = ScalingMode::INTEGER;
|
||||
}
|
||||
}
|
||||
if (node.contains("vsync"))
|
||||
if (node.contains("vsync")) {
|
||||
video.vsync = node["vsync"].get_value<bool>();
|
||||
if (node.contains("aspect_ratio_4_3"))
|
||||
}
|
||||
if (node.contains("aspect_ratio_4_3")) {
|
||||
video.aspect_ratio_4_3 = node["aspect_ratio_4_3"].get_value<bool>();
|
||||
}
|
||||
if (node.contains("texture_filter")) {
|
||||
auto s = node["texture_filter"].get_value<std::string>();
|
||||
video.texture_filter = (s == "linear") ? TextureFilter::LINEAR : TextureFilter::NEAREST;
|
||||
}
|
||||
if (node.contains("downscale_algo"))
|
||||
video.downscale_algo = node["downscale_algo"].get_value<int>();
|
||||
if (node.contains("internal_resolution")) {
|
||||
video.internal_resolution = node["internal_resolution"].get_value<int>();
|
||||
if (video.internal_resolution < 1) video.internal_resolution = 1;
|
||||
video.internal_resolution = std::max(video.internal_resolution, 1);
|
||||
}
|
||||
if (node.contains("current_shader"))
|
||||
if (node.contains("current_shader")) {
|
||||
video.current_shader = node["current_shader"].get_value<std::string>();
|
||||
if (node.contains("current_postfx_preset"))
|
||||
}
|
||||
if (node.contains("current_postfx_preset")) {
|
||||
video.current_postfx_preset = node["current_postfx_preset"].get_value<std::string>();
|
||||
if (node.contains("current_crtpi_preset"))
|
||||
}
|
||||
if (node.contains("current_crtpi_preset")) {
|
||||
video.current_crtpi_preset = node["current_crtpi_preset"].get_value<std::string>();
|
||||
}
|
||||
}
|
||||
|
||||
static void loadRenderInfoFromYaml(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("render_info")) return;
|
||||
if (!yaml.contains("render_info")) {
|
||||
return;
|
||||
}
|
||||
const auto& node = yaml["render_info"];
|
||||
|
||||
if (node.contains("position")) {
|
||||
auto pos = node["position"].get_value<std::string>();
|
||||
if (pos == "top")
|
||||
if (pos == "top") {
|
||||
render_info.position = RenderInfoPosition::TOP;
|
||||
else if (pos == "bottom")
|
||||
} else if (pos == "bottom") {
|
||||
render_info.position = RenderInfoPosition::BOTTOM;
|
||||
else
|
||||
} else {
|
||||
render_info.position = RenderInfoPosition::OFF;
|
||||
}
|
||||
}
|
||||
if (node.contains("show_time"))
|
||||
if (node.contains("show_time")) {
|
||||
render_info.show_time = node["show_time"].get_value<bool>();
|
||||
if (node.contains("text_color"))
|
||||
}
|
||||
if (node.contains("text_color")) {
|
||||
render_info.text_color = static_cast<Uint32>(node["text_color"].get_value<uint64_t>());
|
||||
if (node.contains("shadow_color"))
|
||||
}
|
||||
if (node.contains("shadow_color")) {
|
||||
render_info.shadow_color = static_cast<Uint32>(node["shadow_color"].get_value<uint64_t>());
|
||||
}
|
||||
}
|
||||
|
||||
static void loadWindowConfigFromYaml(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("window")) return;
|
||||
if (!yaml.contains("window")) {
|
||||
return;
|
||||
}
|
||||
const auto& node = yaml["window"];
|
||||
|
||||
if (node.contains("zoom"))
|
||||
if (node.contains("zoom")) {
|
||||
window.zoom = node["zoom"].get_value<int>();
|
||||
if (node.contains("fullscreen"))
|
||||
}
|
||||
if (node.contains("fullscreen")) {
|
||||
window.fullscreen = node["fullscreen"].get_value<bool>();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: carrega una SDL_Scancode des d'un string (nom SDL de la tecla).
|
||||
static void loadScancodeField(const fkyaml::node& node, const std::string& key, SDL_Scancode& target) {
|
||||
if (!node.contains(key)) return;
|
||||
if (!node.contains(key)) {
|
||||
return;
|
||||
}
|
||||
auto name = node[key].get_value<std::string>();
|
||||
SDL_Scancode sc = SDL_GetScancodeFromName(name.c_str());
|
||||
if (sc != SDL_SCANCODE_UNKNOWN) target = sc;
|
||||
if (sc != SDL_SCANCODE_UNKNOWN) {
|
||||
target = sc;
|
||||
}
|
||||
}
|
||||
|
||||
static void loadControlsFromYaml(const fkyaml::node& yaml) {
|
||||
@@ -212,15 +248,20 @@ namespace Options {
|
||||
}
|
||||
|
||||
static void loadGameConfigFromYaml(const fkyaml::node& yaml) {
|
||||
if (!yaml.contains("game")) return;
|
||||
if (!yaml.contains("game")) {
|
||||
return;
|
||||
}
|
||||
const auto& node = yaml["game"];
|
||||
|
||||
if (node.contains("use_new_logo"))
|
||||
if (node.contains("use_new_logo")) {
|
||||
game.use_new_logo = node["use_new_logo"].get_value<bool>();
|
||||
if (node.contains("show_title_credits"))
|
||||
}
|
||||
if (node.contains("show_title_credits")) {
|
||||
game.show_title_credits = node["show_title_credits"].get_value<bool>();
|
||||
if (node.contains("show_preload"))
|
||||
}
|
||||
if (node.contains("show_preload")) {
|
||||
game.show_preload = node["show_preload"].get_value<bool>();
|
||||
}
|
||||
}
|
||||
|
||||
// Carrega les opcions des del fitxer configurat
|
||||
@@ -297,9 +338,8 @@ namespace Options {
|
||||
file << "video:\n";
|
||||
file << " gpu_acceleration: " << (video.gpu_acceleration ? "true" : "false") << "\n";
|
||||
file << " shader_enabled: " << (video.shader_enabled ? "true" : "false") << "\n";
|
||||
file << " supersampling: " << (video.supersampling ? "true" : "false") << "\n";
|
||||
{
|
||||
const char* m = "integer";
|
||||
const char* m = nullptr;
|
||||
switch (video.scaling_mode) {
|
||||
case ScalingMode::DISABLED:
|
||||
m = "disabled";
|
||||
@@ -314,6 +354,7 @@ namespace Options {
|
||||
m = "overscan";
|
||||
break;
|
||||
case ScalingMode::INTEGER:
|
||||
default:
|
||||
m = "integer";
|
||||
break;
|
||||
}
|
||||
@@ -322,7 +363,6 @@ namespace Options {
|
||||
file << " vsync: " << (video.vsync ? "true" : "false") << "\n";
|
||||
file << " aspect_ratio_4_3: " << (video.aspect_ratio_4_3 ? "true" : "false") << "\n";
|
||||
file << " texture_filter: " << (video.texture_filter == TextureFilter::LINEAR ? "linear" : "nearest") << " # nearest|linear\n";
|
||||
file << " downscale_algo: " << video.downscale_algo << " # 0=bilinear, 1=Lanczos2, 2=Lanczos3\n";
|
||||
file << " internal_resolution: " << video.internal_resolution << " # multiplicador enter font, clampat a max_zoom\n";
|
||||
file << " current_shader: " << video.current_shader << "\n";
|
||||
file << " current_postfx_preset: " << video.current_postfx_preset << "\n";
|
||||
@@ -334,10 +374,11 @@ namespace Options {
|
||||
file << "render_info:\n";
|
||||
{
|
||||
const char* pos = "off";
|
||||
if (render_info.position == RenderInfoPosition::TOP)
|
||||
if (render_info.position == RenderInfoPosition::TOP) {
|
||||
pos = "top";
|
||||
else if (render_info.position == RenderInfoPosition::BOTTOM)
|
||||
} else if (render_info.position == RenderInfoPosition::BOTTOM) {
|
||||
pos = "bottom";
|
||||
}
|
||||
file << " position: " << pos << " # off/top/bottom\n";
|
||||
}
|
||||
file << " show_time: " << (render_info.show_time ? "true" : "false") << "\n";
|
||||
@@ -388,12 +429,34 @@ namespace Options {
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Helper per a parsejar floats de YAML ---
|
||||
// --- Helpers per a parsejar camps de YAML ---
|
||||
static void parseFloatField(const fkyaml::node& node, const std::string& key, float& target) {
|
||||
if (node.contains(key)) {
|
||||
try {
|
||||
target = node[key].get_value<float>();
|
||||
} catch (...) {}
|
||||
} catch (...) {
|
||||
// @INTENTIONAL: si el camp YAML no és float vàlid, mantenim el valor per defecte.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void parseIntField(const fkyaml::node& node, const std::string& key, int& target) {
|
||||
if (node.contains(key)) {
|
||||
try {
|
||||
target = node[key].get_value<int>();
|
||||
} catch (...) {
|
||||
// @INTENTIONAL: camp YAML no és int vàlid, mantenim valor per defecte.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void parseBoolField(const fkyaml::node& node, const std::string& key, bool& target) {
|
||||
if (node.contains(key)) {
|
||||
try {
|
||||
target = node[key].get_value<bool>();
|
||||
} catch (...) {
|
||||
// @INTENTIONAL: camp YAML no és bool vàlid, mantenim valor per defecte.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,22 +473,24 @@ namespace Options {
|
||||
// Escriure defaults
|
||||
std::ofstream out(postfx_file_path);
|
||||
if (out.is_open()) {
|
||||
out << "# Aventures En Egipte - PostFX Shader Presets\n\n";
|
||||
out << "# Aventures En Egipte - PostFX Shader Presets\n";
|
||||
out << "# chroma_min/max: min == max → estàtic; min != max → pulsa sinusoïdalment.\n";
|
||||
out << "# scan_dark_ratio/floor/edge_soft: forma analítica de les scanlines.\n\n";
|
||||
out << "presets:\n";
|
||||
out << " - name: \"CRT\"\n vignette: 0.6\n scanlines: 0.7\n chroma: 0.15\n mask: 0.6\n gamma: 0.8\n";
|
||||
out << " - name: \"NTSC\"\n vignette: 0.4\n scanlines: 0.5\n chroma: 0.2\n mask: 0.4\n gamma: 0.5\n bleeding: 0.6\n";
|
||||
out << " - name: \"CURVED\"\n vignette: 0.5\n scanlines: 0.6\n chroma: 0.1\n mask: 0.5\n gamma: 0.7\n curvature: 0.8\n";
|
||||
out << " - name: \"SCANLINES\"\n scanlines: 0.8\n vignette: 0.0\n chroma: 0.0\n";
|
||||
out << " - name: \"SUBTLE\"\n vignette: 0.3\n scanlines: 0.4\n chroma: 0.05\n gamma: 0.3\n";
|
||||
out << " - name: \"CRT LIVE\"\n vignette: 0.5\n scanlines: 0.6\n chroma: 0.3\n mask: 0.3\n gamma: 0.4\n curvature: 0.3\n bleeding: 0.4\n flicker: 0.8\n";
|
||||
out << " - name: \"CRT\"\n vignette: 0.6\n scanlines: 0.7\n chroma_min: 0.15\n chroma_max: 0.15\n mask: 0.6\n gamma: 0.8\n";
|
||||
out << " - name: \"NTSC\"\n vignette: 0.4\n scanlines: 0.5\n chroma_min: 0.2\n chroma_max: 0.2\n mask: 0.4\n gamma: 0.5\n bleeding: 0.6\n";
|
||||
out << " - name: \"CURVED\"\n vignette: 0.5\n scanlines: 0.6\n chroma_min: 0.1\n chroma_max: 0.1\n mask: 0.5\n gamma: 0.7\n curvature: 0.8\n";
|
||||
out << " - name: \"SCANLINES\"\n scanlines: 0.8\n vignette: 0.0\n chroma_min: 0.0\n chroma_max: 0.0\n";
|
||||
out << " - name: \"SUBTLE\"\n vignette: 0.3\n scanlines: 0.4\n chroma_min: 0.05\n chroma_max: 0.05\n gamma: 0.3\n";
|
||||
out << " - name: \"CRT LIVE\"\n vignette: 0.5\n scanlines: 0.6\n chroma_min: 0.3\n chroma_max: 0.3\n mask: 0.3\n gamma: 0.4\n curvature: 0.3\n bleeding: 0.4\n flicker: 0.8\n";
|
||||
out.close();
|
||||
}
|
||||
postfx_presets.push_back({"CRT", 0.6F, 0.7F, 0.15F, 0.6F, 0.8F});
|
||||
postfx_presets.push_back({"NTSC", 0.4F, 0.5F, 0.2F, 0.4F, 0.5F, 0.0F, 0.6F});
|
||||
postfx_presets.push_back({"CURVED", 0.5F, 0.6F, 0.1F, 0.5F, 0.7F, 0.8F});
|
||||
postfx_presets.push_back({"SCANLINES", 0.0F, 0.8F});
|
||||
postfx_presets.push_back({"SUBTLE", 0.3F, 0.4F, 0.05F, 0.0F, 0.3F});
|
||||
postfx_presets.push_back({"CRT LIVE", 0.5F, 0.6F, 0.3F, 0.3F, 0.4F, 0.3F, 0.4F, 0.8F});
|
||||
postfx_presets.push_back({.name = "CRT", .vignette = 0.6F, .scanlines = 0.7F, .chroma_min = 0.15F, .chroma_max = 0.15F, .mask = 0.6F, .gamma = 0.8F});
|
||||
postfx_presets.push_back({.name = "NTSC", .vignette = 0.4F, .scanlines = 0.5F, .chroma_min = 0.2F, .chroma_max = 0.2F, .mask = 0.4F, .gamma = 0.5F, .bleeding = 0.6F});
|
||||
postfx_presets.push_back({.name = "CURVED", .vignette = 0.5F, .scanlines = 0.6F, .chroma_min = 0.1F, .chroma_max = 0.1F, .mask = 0.5F, .gamma = 0.7F, .curvature = 0.8F});
|
||||
postfx_presets.push_back({.name = "SCANLINES", .vignette = 0.0F, .scanlines = 0.8F, .chroma_min = 0.0F, .chroma_max = 0.0F});
|
||||
postfx_presets.push_back({.name = "SUBTLE", .vignette = 0.3F, .scanlines = 0.4F, .chroma_min = 0.05F, .chroma_max = 0.05F, .gamma = 0.3F});
|
||||
postfx_presets.push_back({.name = "CRT LIVE", .vignette = 0.5F, .scanlines = 0.6F, .chroma_min = 0.3F, .chroma_max = 0.3F, .mask = 0.3F, .gamma = 0.4F, .curvature = 0.3F, .bleeding = 0.4F, .flicker = 0.8F});
|
||||
current_postfx_preset = 0;
|
||||
return true;
|
||||
}
|
||||
@@ -438,15 +503,21 @@ namespace Options {
|
||||
if (yaml.contains("presets")) {
|
||||
for (const auto& p : yaml["presets"]) {
|
||||
PostFXPreset preset;
|
||||
if (p.contains("name")) preset.name = p["name"].get_value<std::string>();
|
||||
if (p.contains("name")) {
|
||||
preset.name = p["name"].get_value<std::string>();
|
||||
}
|
||||
parseFloatField(p, "vignette", preset.vignette);
|
||||
parseFloatField(p, "scanlines", preset.scanlines);
|
||||
parseFloatField(p, "chroma", preset.chroma);
|
||||
parseFloatField(p, "chroma_min", preset.chroma_min);
|
||||
parseFloatField(p, "chroma_max", preset.chroma_max);
|
||||
parseFloatField(p, "mask", preset.mask);
|
||||
parseFloatField(p, "gamma", preset.gamma);
|
||||
parseFloatField(p, "curvature", preset.curvature);
|
||||
parseFloatField(p, "bleeding", preset.bleeding);
|
||||
parseFloatField(p, "flicker", preset.flicker);
|
||||
parseFloatField(p, "scan_dark_ratio", preset.scan_dark_ratio);
|
||||
parseFloatField(p, "scan_dark_floor", preset.scan_dark_floor);
|
||||
parseFloatField(p, "scan_edge_soft", preset.scan_edge_soft);
|
||||
postfx_presets.push_back(preset);
|
||||
}
|
||||
}
|
||||
@@ -455,7 +526,7 @@ namespace Options {
|
||||
return true;
|
||||
} catch (const fkyaml::exception& e) {
|
||||
std::cerr << "Error parsing PostFX YAML: " << e.what() << '\n';
|
||||
postfx_presets.push_back({"CRT", 0.6F, 0.7F, 0.15F, 0.6F, 0.8F});
|
||||
postfx_presets.push_back({.name = "CRT", .vignette = 0.6F, .scanlines = 0.7F, .chroma_min = 0.15F, .chroma_max = 0.15F, .mask = 0.6F, .gamma = 0.8F});
|
||||
current_postfx_preset = 0;
|
||||
return false;
|
||||
}
|
||||
@@ -497,7 +568,9 @@ namespace Options {
|
||||
if (yaml.contains("presets")) {
|
||||
for (const auto& p : yaml["presets"]) {
|
||||
CrtPiPreset preset;
|
||||
if (p.contains("name")) preset.name = p["name"].get_value<std::string>();
|
||||
if (p.contains("name")) {
|
||||
preset.name = p["name"].get_value<std::string>();
|
||||
}
|
||||
parseFloatField(p, "scanline_weight", preset.scanline_weight);
|
||||
parseFloatField(p, "scanline_gap_brightness", preset.scanline_gap_brightness);
|
||||
parseFloatField(p, "bloom_factor", preset.bloom_factor);
|
||||
@@ -506,24 +579,12 @@ namespace Options {
|
||||
parseFloatField(p, "mask_brightness", preset.mask_brightness);
|
||||
parseFloatField(p, "curvature_x", preset.curvature_x);
|
||||
parseFloatField(p, "curvature_y", preset.curvature_y);
|
||||
if (p.contains("mask_type")) try {
|
||||
preset.mask_type = p["mask_type"].get_value<int>();
|
||||
} catch (...) {}
|
||||
if (p.contains("enable_scanlines")) try {
|
||||
preset.enable_scanlines = p["enable_scanlines"].get_value<bool>();
|
||||
} catch (...) {}
|
||||
if (p.contains("enable_multisample")) try {
|
||||
preset.enable_multisample = p["enable_multisample"].get_value<bool>();
|
||||
} catch (...) {}
|
||||
if (p.contains("enable_gamma")) try {
|
||||
preset.enable_gamma = p["enable_gamma"].get_value<bool>();
|
||||
} catch (...) {}
|
||||
if (p.contains("enable_curvature")) try {
|
||||
preset.enable_curvature = p["enable_curvature"].get_value<bool>();
|
||||
} catch (...) {}
|
||||
if (p.contains("enable_sharper")) try {
|
||||
preset.enable_sharper = p["enable_sharper"].get_value<bool>();
|
||||
} catch (...) {}
|
||||
parseIntField(p, "mask_type", preset.mask_type);
|
||||
parseBoolField(p, "enable_scanlines", preset.enable_scanlines);
|
||||
parseBoolField(p, "enable_multisample", preset.enable_multisample);
|
||||
parseBoolField(p, "enable_gamma", preset.enable_gamma);
|
||||
parseBoolField(p, "enable_curvature", preset.enable_curvature);
|
||||
parseBoolField(p, "enable_sharper", preset.enable_sharper);
|
||||
crtpi_presets.push_back(preset);
|
||||
}
|
||||
}
|
||||
|
||||
+11
-6
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -19,17 +20,17 @@ namespace Options {
|
||||
};
|
||||
|
||||
// Posició del render info
|
||||
enum class RenderInfoPosition { OFF = 0,
|
||||
enum class RenderInfoPosition : std::uint8_t { OFF = 0,
|
||||
TOP = 1,
|
||||
BOTTOM = 2 };
|
||||
|
||||
// Filtre de textura per a l'upscale final (sempre, no només en 4:3)
|
||||
enum class TextureFilter { NEAREST = 0,
|
||||
enum class TextureFilter : std::uint8_t { NEAREST = 0,
|
||||
LINEAR = 1 };
|
||||
|
||||
// Mode de presentació lògica (escala finestra): mapeja directament
|
||||
// als valors de SDL_RendererLogicalPresentation.
|
||||
enum class ScalingMode { DISABLED = 0,
|
||||
enum class ScalingMode : std::uint8_t { DISABLED = 0,
|
||||
STRETCH = 1,
|
||||
LETTERBOX = 2,
|
||||
OVERSCAN = 3,
|
||||
@@ -39,12 +40,10 @@ namespace Options {
|
||||
struct Video {
|
||||
bool gpu_acceleration{Defaults::Video::GPU_ACCELERATION};
|
||||
bool shader_enabled{Defaults::Video::SHADER_ENABLED};
|
||||
bool supersampling{Defaults::Video::SUPERSAMPLING};
|
||||
ScalingMode scaling_mode{ScalingMode::INTEGER};
|
||||
bool vsync{Defaults::Video::VSYNC};
|
||||
bool aspect_ratio_4_3{Defaults::Video::ASPECT_RATIO_4_3};
|
||||
TextureFilter texture_filter{TextureFilter::NEAREST};
|
||||
int downscale_algo{Defaults::Video::DOWNSCALE_ALGO};
|
||||
int internal_resolution{Defaults::Video::INTERNAL_RESOLUTION}; // Multiplicador enter ≥ 1, clampat a max_zoom
|
||||
std::string current_shader{"postfx"}; // "postfx" o "crtpi"
|
||||
std::string current_postfx_preset{"CRT"}; // Nom del preset PostFX actiu
|
||||
@@ -98,12 +97,18 @@ namespace Options {
|
||||
std::string name;
|
||||
float vignette{0.6F};
|
||||
float scanlines{0.7F};
|
||||
float chroma{0.15F};
|
||||
// Aberració cromàtica: min == max → estàtic; min != max → pulsa sinusoïdalment.
|
||||
float chroma_min{0.15F};
|
||||
float chroma_max{0.15F};
|
||||
float mask{0.0F};
|
||||
float gamma{0.0F};
|
||||
float curvature{0.0F};
|
||||
float bleeding{0.0F};
|
||||
float flicker{0.0F};
|
||||
// Forma de les scanlines — 3 subpíxels per fila lògica per defecte.
|
||||
float scan_dark_ratio{0.333F};
|
||||
float scan_dark_floor{0.42F};
|
||||
float scan_edge_soft{1.0F};
|
||||
};
|
||||
|
||||
// Preset CrtPi
|
||||
|
||||
+132
-114
@@ -1,76 +1,79 @@
|
||||
#include "game/prota.hpp"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "core/jail/jgame.hpp"
|
||||
#include "core/jail/jinput.hpp"
|
||||
|
||||
Prota::Prota(JD8_Surface gfx)
|
||||
: Sprite(gfx) {
|
||||
entitat.frames.reserve(82);
|
||||
|
||||
for (int y = 0; y < 4; y++) {
|
||||
for (int x = 0; x < 5; x++) {
|
||||
Frame f;
|
||||
f.w = 15;
|
||||
f.h = 15;
|
||||
if (info::ctx.num_piramide == 4) f.h -= 5;
|
||||
f.x = x * 15;
|
||||
f.y = 20 + (y * 15);
|
||||
entitat.frames.push_back(f);
|
||||
}
|
||||
namespace {
|
||||
// Atura el frame.h a 10 quan piràmide 4 (sprite "petit" amb pijama de presoner).
|
||||
auto adjustedHeight(int base_h) -> int {
|
||||
return (Info::ctx.num_piramide == 4) ? base_h - 5 : base_h;
|
||||
}
|
||||
for (int y = 95; y < 185; y += 30) {
|
||||
for (int x = 60; x < 315; x += 15) {
|
||||
if (x != 300 || y != 155) {
|
||||
|
||||
void addFrameGrid(Entitat& entitat, int x0, int x1, int x_step, int y0, int y1, int y_step, int w, int h, int skip_x = -1, int skip_y = -1) {
|
||||
for (int yy = y0; yy < y1; yy += y_step) {
|
||||
for (int xx = x0; xx < x1; xx += x_step) {
|
||||
if (xx == skip_x && yy == skip_y) {
|
||||
continue;
|
||||
}
|
||||
Frame f;
|
||||
f.w = 15;
|
||||
f.h = 30;
|
||||
if (info::ctx.num_piramide == 4) f.h -= 5;
|
||||
f.x = x;
|
||||
f.y = y;
|
||||
f.w = w;
|
||||
f.h = adjustedHeight(h);
|
||||
f.x = xx;
|
||||
f.y = yy;
|
||||
entitat.frames.push_back(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int y = 20; y < 50; y += 15) {
|
||||
for (int x = 225; x < 315; x += 15) {
|
||||
Frame f;
|
||||
f.w = 15;
|
||||
f.h = 15;
|
||||
if (info::ctx.num_piramide == 4) f.h -= 5;
|
||||
f.x = x;
|
||||
f.y = y;
|
||||
entitat.frames.push_back(f);
|
||||
|
||||
void buildProtaFrames(Entitat& entitat) {
|
||||
entitat.frames.reserve(82);
|
||||
// Cara/quatre direccions (4×5 sprites de 15×15 a y=20..)
|
||||
addFrameGrid(entitat, 0, 75, 15, 20, 80, 15, 15, 15);
|
||||
// Animació de mort (15×30 a y=95..; salta x=300/y=155)
|
||||
addFrameGrid(entitat, 60, 315, 15, 95, 185, 30, 15, 30, 300, 155);
|
||||
// Animació de victòria (15×15 a y=20.., x=225..)
|
||||
addFrameGrid(entitat, 225, 315, 15, 20, 50, 15, 15, 15);
|
||||
}
|
||||
|
||||
void buildProtaAnimations(Entitat& entitat) {
|
||||
entitat.animacions.resize(6);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
entitat.animacions[i].frames = {
|
||||
static_cast<Uint8>(0 + (i * 5)),
|
||||
static_cast<Uint8>(1 + (i * 5)),
|
||||
static_cast<Uint8>(2 + (i * 5)),
|
||||
static_cast<Uint8>(1 + (i * 5)),
|
||||
static_cast<Uint8>(0 + (i * 5)),
|
||||
static_cast<Uint8>(3 + (i * 5)),
|
||||
static_cast<Uint8>(4 + (i * 5)),
|
||||
static_cast<Uint8>(3 + (i * 5)),
|
||||
};
|
||||
}
|
||||
entitat.animacions[4].frames.resize(50);
|
||||
for (int i = 0; i < 50; i++) {
|
||||
entitat.animacions[4].frames[i] = i + 20;
|
||||
}
|
||||
entitat.animacions[5].frames.resize(48);
|
||||
for (int i = 0; i < 12; i++) {
|
||||
entitat.animacions[5].frames[i] = i + 70;
|
||||
}
|
||||
for (int i = 12; i < 48; i++) {
|
||||
entitat.animacions[5].frames[i] = 81;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
entitat.animacions.resize(6);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
entitat.animacions[i].frames = {
|
||||
static_cast<Uint8>(0 + i * 5),
|
||||
static_cast<Uint8>(1 + i * 5),
|
||||
static_cast<Uint8>(2 + i * 5),
|
||||
static_cast<Uint8>(1 + i * 5),
|
||||
static_cast<Uint8>(0 + i * 5),
|
||||
static_cast<Uint8>(3 + i * 5),
|
||||
static_cast<Uint8>(4 + i * 5),
|
||||
static_cast<Uint8>(3 + i * 5),
|
||||
};
|
||||
}
|
||||
|
||||
entitat.animacions[4].frames.resize(50);
|
||||
for (int i = 0; i < 50; i++) entitat.animacions[4].frames[i] = i + 20;
|
||||
|
||||
entitat.animacions[5].frames.resize(48);
|
||||
for (int i = 0; i < 12; i++) entitat.animacions[5].frames[i] = i + 70;
|
||||
for (int i = 12; i < 48; i++) entitat.animacions[5].frames[i] = 81;
|
||||
|
||||
Prota::Prota(Jd8::Surface gfx)
|
||||
: Sprite(gfx) {
|
||||
buildProtaFrames(entitat);
|
||||
buildProtaAnimations(entitat);
|
||||
cur_frame = 0;
|
||||
x = 150;
|
||||
y = 30;
|
||||
o = 0;
|
||||
cycles_per_frame = 4;
|
||||
cycles_per_frame_ = 4;
|
||||
pergami = false;
|
||||
frame_pejades = 0;
|
||||
}
|
||||
@@ -78,74 +81,89 @@ Prota::Prota(JD8_Surface gfx)
|
||||
void Prota::draw() {
|
||||
Sprite::draw();
|
||||
|
||||
if (info::ctx.num_piramide == 4 && this->o != 4) {
|
||||
if ((JG_GetCycleCounter() % 40) < 20) {
|
||||
JD8_BlitCK(this->x, this->y, this->gfx, 220, 80, 15, 15, 255);
|
||||
if (Info::ctx.num_piramide == 4 && this->o != 4) {
|
||||
if ((Jg::getCycleCounter() % 40) < 20) {
|
||||
Jd8::blitCK(this->x, this->y, this->gfx_, 220, 80, 15, 15, 255);
|
||||
} else {
|
||||
JD8_BlitCK(this->x, this->y, this->gfx, 235, 80, 15, 15, 255);
|
||||
Jd8::blitCK(this->x, this->y, this->gfx_, 235, 80, 15, 15, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Uint8 Prota::update() {
|
||||
Uint8 eixir = 0;
|
||||
auto Prota::readDirection() -> Uint8 {
|
||||
Uint8 dir = 4;
|
||||
if (Ji::keyPressed(SDL_SCANCODE_DOWN)) {
|
||||
if ((this->x - 20) % 65 == 0) { this->o = 0; }
|
||||
dir = this->o;
|
||||
}
|
||||
if (Ji::keyPressed(SDL_SCANCODE_UP)) {
|
||||
if ((this->x - 20) % 65 == 0) { this->o = 1; }
|
||||
dir = this->o;
|
||||
}
|
||||
if (Ji::keyPressed(SDL_SCANCODE_RIGHT)) {
|
||||
if ((this->y - 30) % 35 == 0) { this->o = 2; }
|
||||
dir = this->o;
|
||||
}
|
||||
if (Ji::keyPressed(SDL_SCANCODE_LEFT)) {
|
||||
if ((this->y - 30) % 35 == 0) { this->o = 3; }
|
||||
dir = this->o;
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
if (this->o < 4) {
|
||||
Uint8 dir = 4;
|
||||
if (JI_KeyPressed(SDL_SCANCODE_DOWN)) {
|
||||
if ((this->x - 20) % 65 == 0) this->o = 0;
|
||||
dir = this->o;
|
||||
}
|
||||
if (JI_KeyPressed(SDL_SCANCODE_UP)) {
|
||||
if ((this->x - 20) % 65 == 0) this->o = 1;
|
||||
dir = this->o;
|
||||
}
|
||||
if (JI_KeyPressed(SDL_SCANCODE_RIGHT)) {
|
||||
if ((this->y - 30) % 35 == 0) this->o = 2;
|
||||
dir = this->o;
|
||||
}
|
||||
if (JI_KeyPressed(SDL_SCANCODE_LEFT)) {
|
||||
if ((this->y - 30) % 35 == 0) this->o = 3;
|
||||
dir = this->o;
|
||||
}
|
||||
void Prota::stepInDirection(Uint8 dir) {
|
||||
switch (dir) {
|
||||
case 0:
|
||||
if (this->y < 170) { this->y++; }
|
||||
break;
|
||||
case 1:
|
||||
if (this->y > 30) { this->y--; }
|
||||
break;
|
||||
case 2:
|
||||
if (this->x < 280) { this->x++; }
|
||||
break;
|
||||
case 3:
|
||||
if (this->x > 20) { this->x--; }
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (dir) {
|
||||
case 0:
|
||||
if (this->y < 170) this->y++;
|
||||
break;
|
||||
case 1:
|
||||
if (this->y > 30) this->y--;
|
||||
break;
|
||||
case 2:
|
||||
if (this->x < 280) this->x++;
|
||||
break;
|
||||
case 3:
|
||||
if (this->x > 20) this->x--;
|
||||
break;
|
||||
}
|
||||
|
||||
if (dir == 4) {
|
||||
void Prota::advanceWalkingFrame(Uint8 dir) {
|
||||
if (dir == 4) {
|
||||
this->cur_frame = 0;
|
||||
return;
|
||||
}
|
||||
this->frame_pejades++;
|
||||
if (this->frame_pejades == 15) {
|
||||
this->frame_pejades = 0;
|
||||
}
|
||||
if (Jg::getCycleCounter() % this->cycles_per_frame_ == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
|
||||
this->cur_frame = 0;
|
||||
} else {
|
||||
this->frame_pejades++;
|
||||
if (this->frame_pejades == 15) this->frame_pejades = 0;
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == entitat.animacions[this->o].frames.size()) this->cur_frame = 0;
|
||||
}
|
||||
}
|
||||
eixir = false;
|
||||
} else {
|
||||
if (JG_GetCycleCounter() % this->cycles_per_frame == 0) {
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame == entitat.animacions[this->o].frames.size()) {
|
||||
if (this->o == 4) {
|
||||
eixir = 1;
|
||||
} else {
|
||||
eixir = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return eixir;
|
||||
}
|
||||
|
||||
auto Prota::advanceFinalAnimation() -> Uint8 {
|
||||
if (Jg::getCycleCounter() % this->cycles_per_frame_ != 0) {
|
||||
return 0;
|
||||
}
|
||||
this->cur_frame++;
|
||||
if (this->cur_frame != entitat.animacions[this->o].frames.size()) {
|
||||
return 0;
|
||||
}
|
||||
return (this->o == 4) ? 1 : 2;
|
||||
}
|
||||
|
||||
auto Prota::update() -> Uint8 {
|
||||
if (this->o >= 4) {
|
||||
return advanceFinalAnimation();
|
||||
}
|
||||
const Uint8 DIR = readDirection();
|
||||
stepInDirection(DIR);
|
||||
advanceWalkingFrame(DIR);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -5,13 +5,17 @@
|
||||
|
||||
class Prota : public Sprite {
|
||||
public:
|
||||
explicit Prota(JD8_Surface gfx);
|
||||
explicit Prota(Jd8::Surface gfx);
|
||||
|
||||
void draw() override;
|
||||
Uint8 update();
|
||||
auto update() -> Uint8;
|
||||
|
||||
Uint8 frame_pejades;
|
||||
bool pergami;
|
||||
|
||||
protected:
|
||||
auto readDirection() -> Uint8;
|
||||
void stepInDirection(Uint8 dir);
|
||||
void advanceWalkingFrame(Uint8 dir);
|
||||
auto advanceFinalAnimation() -> Uint8;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "scenes/banner_scene.hpp"
|
||||
#include "game/scenes/banner_scene.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
@@ -6,47 +6,49 @@
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
#include "core/jail/jinput.hpp"
|
||||
#include "game/info.hpp"
|
||||
#include "scenes/scene_utils.hpp"
|
||||
#include "game/scenes/scene_utils.hpp"
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
void BannerScene::onEnter() {
|
||||
playMusic("music/banner.ogg");
|
||||
|
||||
gfx_ = SurfaceHandle("gfx/ffase.gif");
|
||||
|
||||
JD8_ClearScreen(0);
|
||||
Jd8::clearScreen(0);
|
||||
// Títols superior i inferior del banner (compartits per tots els nivells)
|
||||
JD8_Blit(81, 24, gfx_, 81, 155, 168, 21);
|
||||
JD8_Blit(39, 150, gfx_, 39, 175, 248, 20);
|
||||
Jd8::blit(81, 24, gfx_, 81, 155, 168, 21);
|
||||
Jd8::blit(39, 150, gfx_, 39, 175, 248, 20);
|
||||
|
||||
// Número de piràmide: les 4 variants del vell `doBanner` es reduïxen
|
||||
// a coordenades (sx,sy) calculades a partir de l'índex 0..3.
|
||||
const int idx = info::ctx.num_piramide - 2; // 2..5 → 0..3
|
||||
if (idx >= 0 && idx <= 3) {
|
||||
const int sx = (idx % 2) * 160;
|
||||
const int sy = (idx / 2) * 75;
|
||||
JD8_Blit(82, 60, gfx_, sx, sy, 160, 75);
|
||||
// a coordenades (SX,SY) calculades a partir de l'índex 0..3.
|
||||
const int IDX = Info::ctx.num_piramide - 2; // 2..5 → 0..3
|
||||
if (IDX >= 0 && IDX <= 3) {
|
||||
const int SX = (IDX % 2) * 160;
|
||||
const int SY = (IDX / 2) * 75;
|
||||
Jd8::blit(82, 60, gfx_, SX, SY, 160, 75);
|
||||
}
|
||||
|
||||
// PaletteFade copia internament amb memcpy; alliberem la paleta temporal.
|
||||
JD8_Palette pal = JD8_LoadPalette("gfx/ffase.gif");
|
||||
Jd8::Palette pal = Jd8::loadPalette("gfx/ffase.gif");
|
||||
fade_.startFadeTo(pal);
|
||||
delete[] pal;
|
||||
|
||||
phase_ = Phase::FadingIn;
|
||||
phase_ = Phase::FADING_IN;
|
||||
remaining_ms_ = 5000;
|
||||
}
|
||||
|
||||
void BannerScene::tick(int delta_ms) {
|
||||
switch (phase_) {
|
||||
case Phase::FadingIn:
|
||||
case Phase::FADING_IN:
|
||||
fade_.tick(delta_ms);
|
||||
if (fade_.done()) phase_ = Phase::Showing;
|
||||
if (fade_.done()) {
|
||||
phase_ = Phase::SHOWING;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::Showing:
|
||||
if (JI_AnyKey()) {
|
||||
case Phase::SHOWING:
|
||||
if (Ji::anyKey()) {
|
||||
remaining_ms_ = 0;
|
||||
} else {
|
||||
remaining_ms_ -= delta_ms;
|
||||
@@ -54,18 +56,20 @@ namespace scenes {
|
||||
if (remaining_ms_ <= 0) {
|
||||
Audio::get()->fadeOutMusic(250);
|
||||
fade_.startFadeOut();
|
||||
phase_ = Phase::FadingOut;
|
||||
phase_ = Phase::FADING_OUT;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::FadingOut:
|
||||
case Phase::FADING_OUT:
|
||||
fade_.tick(delta_ms);
|
||||
if (fade_.done()) phase_ = Phase::Done;
|
||||
if (fade_.done()) {
|
||||
phase_ = Phase::DONE;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::Done:
|
||||
case Phase::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -1,19 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "scenes/palette_fade.hpp"
|
||||
#include "scenes/scene.hpp"
|
||||
#include "scenes/surface_handle.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
namespace scenes {
|
||||
#include "game/scenes/palette_fade.hpp"
|
||||
#include "game/scenes/scene.hpp"
|
||||
#include "game/scenes/surface_handle.hpp"
|
||||
|
||||
namespace Scenes {
|
||||
|
||||
// Banner pre-piràmide ("PIRÀMIDE X"). Reemplaça `ModuleSequence::doBanner()`.
|
||||
//
|
||||
// Flux:
|
||||
// 1. Arranca música "music/banner.ogg" i carrega gfx/ffase.gif.
|
||||
// 2. Pinta títol, subtítol i número de piràmide segons info::ctx.num_piramide.
|
||||
// 2. Pinta títol, subtítol i número de piràmide segons Info::ctx.num_piramide.
|
||||
// 3. Fade-in de paleta.
|
||||
// 4. Mostra ~5s o fins que es polse una tecla.
|
||||
// 5. JA_FadeOutMusic(250) + fade-out de paleta.
|
||||
// 5. Ja::fadeOutMusic(250) + fade-out de paleta.
|
||||
// 6. Retorna nextState=0 per a entrar al ModuleGame.
|
||||
//
|
||||
// Registrat al SceneRegistry amb state_keys 2..5 (els num_piramide on
|
||||
@@ -22,19 +24,19 @@ namespace scenes {
|
||||
public:
|
||||
void onEnter() override;
|
||||
void tick(int delta_ms) override;
|
||||
bool done() const override { return phase_ == Phase::Done; }
|
||||
int nextState() const override { return 0; }
|
||||
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::DONE; }
|
||||
[[nodiscard]] auto nextState() const -> int override { return 0; }
|
||||
|
||||
private:
|
||||
enum class Phase { FadingIn,
|
||||
Showing,
|
||||
FadingOut,
|
||||
Done };
|
||||
enum class Phase : std::uint8_t { FADING_IN,
|
||||
SHOWING,
|
||||
FADING_OUT,
|
||||
DONE };
|
||||
|
||||
SurfaceHandle gfx_;
|
||||
PaletteFade fade_;
|
||||
Phase phase_{Phase::FadingIn};
|
||||
Phase phase_{Phase::FADING_IN};
|
||||
int remaining_ms_{5000};
|
||||
};
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -1,10 +1,10 @@
|
||||
#include "scenes/boot_loader_scene.hpp"
|
||||
#include "game/scenes/boot_loader_scene.hpp"
|
||||
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "game/options.hpp"
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
namespace {
|
||||
constexpr int SCREEN_W = 320;
|
||||
@@ -24,8 +24,8 @@ namespace scenes {
|
||||
// Inicialitza la paleta mínima per a la barra. La resta de
|
||||
// colors queden a negre — després cada escena del joc carregarà
|
||||
// la seua pròpia paleta.
|
||||
JD8_SetPaletteColor(BG_COLOR, 0, 0, 0);
|
||||
JD8_SetPaletteColor(BAR_COLOR, 63, 63, 63);
|
||||
Jd8::setPaletteColor(BG_COLOR, 0, 0, 0);
|
||||
Jd8::setPaletteColor(BAR_COLOR, 63, 63, 63);
|
||||
}
|
||||
|
||||
void BootLoaderScene::tick(int /*delta_ms*/) {
|
||||
@@ -35,24 +35,26 @@ namespace scenes {
|
||||
render();
|
||||
}
|
||||
|
||||
void BootLoaderScene::render() const {
|
||||
JD8_ClearScreen(BG_COLOR);
|
||||
void BootLoaderScene::render() {
|
||||
Jd8::clearScreen(BG_COLOR);
|
||||
|
||||
if (!Options::game.show_preload) return;
|
||||
if (!Options::game.show_preload) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float pct = Resource::Cache::get()->getProgress();
|
||||
const int filled = static_cast<int>(static_cast<float>(BAR_W) * pct);
|
||||
const float PCT = Resource::Cache::get()->getProgress();
|
||||
const int FILLED = static_cast<int>(static_cast<float>(BAR_W) * PCT);
|
||||
|
||||
// Vora de la barra (línia 1 píxel a dalt i a baix).
|
||||
JD8_FillRect(BAR_X - 1, BAR_Y - 1, BAR_W + 2, 1, BAR_COLOR);
|
||||
JD8_FillRect(BAR_X - 1, BAR_Y + BAR_H, BAR_W + 2, 1, BAR_COLOR);
|
||||
JD8_FillRect(BAR_X - 1, BAR_Y, 1, BAR_H, BAR_COLOR);
|
||||
JD8_FillRect(BAR_X + BAR_W, BAR_Y, 1, BAR_H, BAR_COLOR);
|
||||
Jd8::fillRect(BAR_X - 1, BAR_Y - 1, BAR_W + 2, 1, BAR_COLOR);
|
||||
Jd8::fillRect(BAR_X - 1, BAR_Y + BAR_H, BAR_W + 2, 1, BAR_COLOR);
|
||||
Jd8::fillRect(BAR_X - 1, BAR_Y, 1, BAR_H, BAR_COLOR);
|
||||
Jd8::fillRect(BAR_X + BAR_W, BAR_Y, 1, BAR_H, BAR_COLOR);
|
||||
|
||||
// Ompliment proporcional al progrés.
|
||||
if (filled > 0) {
|
||||
JD8_FillRect(BAR_X, BAR_Y, filled, BAR_H, BAR_COLOR);
|
||||
if (FILLED > 0) {
|
||||
Jd8::fillRect(BAR_X, BAR_Y, FILLED, BAR_H, BAR_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -1,8 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "scenes/scene.hpp"
|
||||
#include "game/scenes/scene.hpp"
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
// Escena de boot que conduix la càrrega incremental del Resource::Cache.
|
||||
// tick() crida loadStep amb un pressupost de ~8ms i pinta una barra
|
||||
@@ -15,12 +15,12 @@ namespace scenes {
|
||||
|
||||
void onEnter() override;
|
||||
void tick(int delta_ms) override;
|
||||
bool done() const override { return done_; }
|
||||
[[nodiscard]] auto done() const -> bool override { return done_; }
|
||||
|
||||
private:
|
||||
void render() const;
|
||||
static void render();
|
||||
|
||||
bool done_{false};
|
||||
};
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "scenes/credits_scene.hpp"
|
||||
#include "game/scenes/credits_scene.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -7,7 +7,7 @@
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
#include "core/jail/jinput.hpp"
|
||||
#include "game/info.hpp"
|
||||
#include "scenes/scene_utils.hpp"
|
||||
#include "game/scenes/scene_utils.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -18,34 +18,33 @@ namespace {
|
||||
};
|
||||
|
||||
constexpr CocheFrame COCHE_FRAMES[8] = {
|
||||
{214, 152},
|
||||
{214, 104},
|
||||
{214, 56},
|
||||
{214, 104},
|
||||
{214, 152},
|
||||
{214, 8},
|
||||
{108, 152},
|
||||
{214, 8},
|
||||
{.x = 214, .y = 152},
|
||||
{.x = 214, .y = 104},
|
||||
{.x = 214, .y = 56},
|
||||
{.x = 214, .y = 104},
|
||||
{.x = 214, .y = 152},
|
||||
{.x = 214, .y = 8},
|
||||
{.x = 108, .y = 152},
|
||||
{.x = 214, .y = 8},
|
||||
};
|
||||
|
||||
constexpr int CONTADOR_MAX = 3100; // ~62 s de crèdits a 20 ms/tick
|
||||
constexpr int TICK_MS = 20; // JG_SetUpdateTicks heretat del doSlides previ
|
||||
constexpr int TICK_MS = 20; // Jg::setUpdateTicks heretat del doSlides previ
|
||||
constexpr int BG_INDEX = 255;
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
CreditsScene::~CreditsScene() {
|
||||
// No toquem la paleta activa: SetScreenPalette n'ha pres ownership.
|
||||
}
|
||||
// No toquem la paleta activa: SetScreenPalette n'ha pres ownership.
|
||||
CreditsScene::~CreditsScene() = default;
|
||||
|
||||
void CreditsScene::onEnter() {
|
||||
// El vell doCredits no tocava música — heretava la del doSlides
|
||||
// previ ("music/final.ogg"). Si l'escena s'arrenca directament (test
|
||||
// amb piramide_inicial=8) no hi ha res que heretar, així que
|
||||
// arranquem la mateixa pista només si no sona res. Inocu en el
|
||||
// flux normal: JA_MUSIC_PLAYING fa que no la tornem a tocar.
|
||||
// flux normal: Ja::MusicState::PLAYING fa que no la tornem a tocar.
|
||||
if (Audio::getRealMusicState() != Audio::MusicState::PLAYING) {
|
||||
playMusic("music/final.ogg");
|
||||
}
|
||||
@@ -53,66 +52,66 @@ namespace scenes {
|
||||
vaddr2_ = SurfaceHandle("gfx/final.gif");
|
||||
vaddr3_ = SurfaceHandle("gfx/finals.gif");
|
||||
|
||||
JD8_Palette pal = JD8_LoadPalette("gfx/final.gif");
|
||||
JD8_SetScreenPalette(pal);
|
||||
Jd8::Palette pal = Jd8::loadPalette("gfx/final.gif");
|
||||
Jd8::setScreenPalette(pal);
|
||||
// `pal` passa a ser propietat de main_palette — no l'alliberem.
|
||||
|
||||
phase_ = Phase::Rolling;
|
||||
phase_ = Phase::ROLLING;
|
||||
contador_ = 1;
|
||||
contador_acc_ms_ = 0;
|
||||
}
|
||||
|
||||
void CreditsScene::render() {
|
||||
JD8_ClearScreen(BG_INDEX);
|
||||
Jd8::clearScreen(BG_INDEX);
|
||||
|
||||
// Columna 1: scroll vertical del bloc (0,0,80,200) pujant des de
|
||||
// y=200 fins que el contador supera 2750.
|
||||
if (contador_ < 2750) {
|
||||
JD8_BlitCKCut(115, 200 - (contador_ / 6), vaddr2_, 0, 0, 80, 200, 0);
|
||||
Jd8::blitCKCut(115, 200 - (contador_ / 6), vaddr2_, 0, 0, 80, 200, 0);
|
||||
}
|
||||
|
||||
// Columna 2: scroll vertical del bloc (85,0,120,140), arrenca
|
||||
// a contador 1200 i s'atura (fix en y=20) a partir de 2250.
|
||||
if ((contador_ > 1200) && (contador_ < 2280)) {
|
||||
JD8_BlitCKCut(100, 200 - ((contador_ - 1200) / 6), vaddr2_, 85, 0, 120, 140, 0);
|
||||
Jd8::blitCKCut(100, 200 - ((contador_ - 1200) / 6), vaddr2_, 85, 0, 120, 140, 0);
|
||||
} else if (contador_ >= 2250) {
|
||||
JD8_BlitCK(100, 20, vaddr2_, 85, 0, 120, 140, 0);
|
||||
Jd8::blitCK(100, 20, vaddr2_, 85, 0, 120, 140, 0);
|
||||
}
|
||||
|
||||
// Fons: 4 capes parallax + cotxe només si l'usuari ha aconseguit
|
||||
// tots els diamants (final "bo"). Altrament fons estàtic.
|
||||
if (info::ctx.diamants == 16) {
|
||||
JD8_BlitCKScroll(50, vaddr3_, ((contador_ >> 3) % 320) + 1, 0, 50, 255);
|
||||
JD8_BlitCKScroll(50, vaddr3_, ((contador_ >> 2) % 320) + 1, 50, 50, 255);
|
||||
JD8_BlitCKScroll(50, vaddr3_, ((contador_ >> 1) % 320) + 1, 100, 50, 255);
|
||||
JD8_BlitCKScroll(50, vaddr3_, (contador_ % 320) + 1, 150, 50, 255);
|
||||
if (Info::ctx.diamants == 16) {
|
||||
Jd8::blitCKScroll(50, vaddr3_, ((contador_ >> 3) % 320) + 1, 0, 50, 255);
|
||||
Jd8::blitCKScroll(50, vaddr3_, ((contador_ >> 2) % 320) + 1, 50, 50, 255);
|
||||
Jd8::blitCKScroll(50, vaddr3_, ((contador_ >> 1) % 320) + 1, 100, 50, 255);
|
||||
Jd8::blitCKScroll(50, vaddr3_, (contador_ % 320) + 1, 150, 50, 255);
|
||||
|
||||
const CocheFrame& cf = COCHE_FRAMES[coche_.frame()];
|
||||
JD8_BlitCK(100, 50, vaddr2_, cf.x, cf.y, 106, 48, 255);
|
||||
Jd8::blitCK(100, 50, vaddr2_, cf.x, cf.y, 106, 48, 255);
|
||||
} else {
|
||||
JD8_BlitCK(0, 50, vaddr3_, 0, 0, 320, 50, 255);
|
||||
JD8_BlitCK(0, 50, vaddr3_, 0, 50, 320, 50, 255);
|
||||
Jd8::blitCK(0, 50, vaddr3_, 0, 0, 320, 50, 255);
|
||||
Jd8::blitCK(0, 50, vaddr3_, 0, 50, 320, 50, 255);
|
||||
}
|
||||
|
||||
// Barres de marc que cobreixen els extrems del scroll vertical.
|
||||
JD8_FillSquare(0, 50, BG_INDEX);
|
||||
JD8_FillSquare(100, 10, BG_INDEX);
|
||||
Jd8::fillSquare(0, 50, BG_INDEX);
|
||||
Jd8::fillSquare(100, 10, BG_INDEX);
|
||||
}
|
||||
|
||||
void CreditsScene::writeTrickIni() {
|
||||
FILE* ini = std::fopen("trick.ini", "wb");
|
||||
if (ini) {
|
||||
if (ini != nullptr) {
|
||||
std::fwrite("1", 1, 1, ini);
|
||||
std::fclose(ini);
|
||||
}
|
||||
info::ctx.nou_personatge = true;
|
||||
Info::ctx.nou_personatge = true;
|
||||
}
|
||||
|
||||
void CreditsScene::tick(int delta_ms) {
|
||||
switch (phase_) {
|
||||
case Phase::Rolling: {
|
||||
case Phase::ROLLING: {
|
||||
// Avancem el contador en passos discrets de 20 ms, igual
|
||||
// que feia JG_ShouldUpdate(20) al vell doCredits.
|
||||
// que feia Jg::shouldUpdate(20) al vell doCredits.
|
||||
contador_acc_ms_ += delta_ms;
|
||||
while (contador_acc_ms_ >= TICK_MS) {
|
||||
contador_acc_ms_ -= TICK_MS;
|
||||
@@ -122,25 +121,25 @@ namespace scenes {
|
||||
coche_.tick(delta_ms);
|
||||
render();
|
||||
|
||||
if (JI_AnyKey() || contador_ >= CONTADOR_MAX) {
|
||||
if (Ji::anyKey() || contador_ >= CONTADOR_MAX) {
|
||||
writeTrickIni();
|
||||
fade_.startFadeOut();
|
||||
phase_ = Phase::FadingOut;
|
||||
phase_ = Phase::FADING_OUT;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Phase::FadingOut:
|
||||
case Phase::FADING_OUT:
|
||||
fade_.tick(delta_ms);
|
||||
if (fade_.done()) {
|
||||
info::ctx.num_piramide = 255;
|
||||
phase_ = Phase::Done;
|
||||
Info::ctx.num_piramide = 255;
|
||||
phase_ = Phase::DONE;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::Done:
|
||||
case Phase::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -1,12 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
#include "scenes/frame_animator.hpp"
|
||||
#include "scenes/palette_fade.hpp"
|
||||
#include "scenes/scene.hpp"
|
||||
#include "scenes/surface_handle.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
namespace scenes {
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
#include "game/scenes/frame_animator.hpp"
|
||||
#include "game/scenes/palette_fade.hpp"
|
||||
#include "game/scenes/scene.hpp"
|
||||
#include "game/scenes/surface_handle.hpp"
|
||||
|
||||
namespace Scenes {
|
||||
|
||||
// Crèdits finals del joc. Reemplaça `ModuleSequence::doCredits()`.
|
||||
//
|
||||
@@ -14,10 +16,10 @@ namespace scenes {
|
||||
// 1. Carrega gfx/final.gif (sprites de crèdits) i gfx/finals.gif (fons).
|
||||
// 2. Mostra els crèdits amb scroll vertical de 2 columnes durant
|
||||
// ~62 segons (contador 0..3100 × 20 ms).
|
||||
// 3. Si `info::ctx.diamants == 16`, pinta addicionalment un parallax
|
||||
// 3. Si `Info::ctx.diamants == 16`, pinta addicionalment un parallax
|
||||
// de 4 capes amb cotxe animat (8 frames). Si no, 2 blits fixos.
|
||||
// 4. Al acabar (per tecla o per contador), crea el fitxer `trick.ini`
|
||||
// i activa `info::ctx.nou_personatge`.
|
||||
// i activa `Info::ctx.nou_personatge`.
|
||||
// 5. Fade-out de paleta. Torna a la intro (num_piramide = 255).
|
||||
//
|
||||
// Registrada al SceneRegistry amb state_key = 8.
|
||||
@@ -28,24 +30,24 @@ namespace scenes {
|
||||
|
||||
void onEnter() override;
|
||||
void tick(int delta_ms) override;
|
||||
bool done() const override { return phase_ == Phase::Done; }
|
||||
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::DONE; }
|
||||
|
||||
private:
|
||||
enum class Phase { Rolling,
|
||||
FadingOut,
|
||||
Done };
|
||||
enum class Phase : std::uint8_t { ROLLING,
|
||||
FADING_OUT,
|
||||
DONE };
|
||||
|
||||
void render();
|
||||
void writeTrickIni();
|
||||
static void writeTrickIni();
|
||||
|
||||
SurfaceHandle vaddr2_; // gfx/final.gif (sprites i coches)
|
||||
SurfaceHandle vaddr3_; // gfx/finals.gif (fons / parallax)
|
||||
PaletteFade fade_;
|
||||
FrameAnimator coche_{8, 60, true}; // 8 frames × 60 ms (~3 × 20 ms tick vell)
|
||||
|
||||
Phase phase_{Phase::Rolling};
|
||||
Phase phase_{Phase::ROLLING};
|
||||
int contador_{1};
|
||||
int contador_acc_ms_{0};
|
||||
};
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "scenes/frame_animator.hpp"
|
||||
#include "game/scenes/frame_animator.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
FrameAnimator::FrameAnimator(int num_frames, int frame_ms, bool loop)
|
||||
: num_frames_(std::max(1, num_frames)),
|
||||
@@ -10,7 +10,9 @@ namespace scenes {
|
||||
loop_(loop) {}
|
||||
|
||||
void FrameAnimator::tick(int delta_ms) {
|
||||
if (finished_) return;
|
||||
if (finished_) {
|
||||
return;
|
||||
}
|
||||
elapsed_ms_ += delta_ms;
|
||||
while (elapsed_ms_ >= frame_ms_) {
|
||||
elapsed_ms_ -= frame_ms_;
|
||||
@@ -33,4 +35,4 @@ namespace scenes {
|
||||
finished_ = false;
|
||||
}
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
// Cicla per un conjunt de frames numerats (0..num_frames-1) avançant un
|
||||
// frame cada `frame_ms` mil·lisegons. No carrega ni dibuixa cap sprite —
|
||||
@@ -15,9 +15,9 @@ namespace scenes {
|
||||
|
||||
void tick(int delta_ms);
|
||||
|
||||
int frame() const { return current_frame_; }
|
||||
bool done() const { return !loop_ && finished_; }
|
||||
int numFrames() const { return num_frames_; }
|
||||
[[nodiscard]] auto frame() const -> int { return current_frame_; }
|
||||
[[nodiscard]] auto done() const -> bool { return !loop_ && finished_; }
|
||||
[[nodiscard]] auto numFrames() const -> int { return num_frames_; }
|
||||
|
||||
void reset();
|
||||
void setFrameMs(int frame_ms) { frame_ms_ = frame_ms; }
|
||||
@@ -31,4 +31,4 @@ namespace scenes {
|
||||
bool finished_{false};
|
||||
};
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -0,0 +1,226 @@
|
||||
#include "game/scenes/intro_new_logo_scene.hpp"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
#include "core/jail/jinput.hpp"
|
||||
#include "game/info.hpp"
|
||||
#include "game/scenes/scene_utils.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Coordenades mesurades del wordmark "Jailgames" dins gfx/logo_new.gif.
|
||||
// Si canvies el logo, aquí i al GIF són els únics llocs a tocar.
|
||||
constexpr int LOGO_SRC_X = 60;
|
||||
constexpr int LOGO_SRC_Y = 158;
|
||||
constexpr int LOGO_DST_Y = 78;
|
||||
constexpr int LOGO_HEIGHT = 28;
|
||||
constexpr int LETTER_WIDTHS[9] = {16, 39, 50, 69, 92, 115, 146, 169, 188};
|
||||
// El logo vell es pintava a dst_x == LOGO_SRC_X = 60, cosa que el deixava
|
||||
// 6 px fora de centre (320 − 188) / 2 = 66. Ho corregim amb un shift
|
||||
// comú aplicat tant al blit del logo com als CURSOR_X de sota.
|
||||
constexpr int LOGO_DST_X = (320 - LETTER_WIDTHS[8]) / 2; // 66
|
||||
constexpr int CENTER_SHIFT = LOGO_DST_X - LOGO_SRC_X; // +6
|
||||
constexpr int CURSOR_X[9] = {77 + CENTER_SHIFT, 100 + CENTER_SHIFT, 111 + CENTER_SHIFT, 130 + CENTER_SHIFT, 153 + CENTER_SHIFT, 176 + CENTER_SHIFT, 207 + CENTER_SHIFT, 230 + CENTER_SHIFT, 249 + CENTER_SHIFT};
|
||||
constexpr int CURSOR_W = 12;
|
||||
constexpr int CURSOR_H = 3;
|
||||
constexpr int CURSOR_Y = LOGO_DST_Y + LOGO_HEIGHT - CURSOR_H; // y = 103
|
||||
constexpr Uint8 CURSOR_COLOR = 17; // mateix index verd que les lletres
|
||||
|
||||
// Timings (ms) — idèntics als de doIntroNewLogo vell.
|
||||
constexpr int INITIAL_MS = 1000;
|
||||
constexpr int REVEAL_FRAME_MS = 150;
|
||||
constexpr int FULL_LOGO_MS = 200;
|
||||
constexpr int PALETTE_CYCLE_STEP_MS = 20;
|
||||
constexpr int FINAL_WAIT_MS = 20;
|
||||
constexpr int PALETTE_CYCLE_STEPS = 256;
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace Scenes {
|
||||
|
||||
IntroNewLogoScene::IntroNewLogoScene() = default;
|
||||
|
||||
// No alliberem `pal_`: Jd8::setScreenPalette n'ha pres ownership i el
|
||||
// proper SetScreenPalette / FadeToPal el lliurarà. Alliberar-lo ací
|
||||
// provocaria double free.
|
||||
IntroNewLogoScene::~IntroNewLogoScene() = default;
|
||||
|
||||
void IntroNewLogoScene::onEnter() {
|
||||
playMusic("music/menu.ogg");
|
||||
|
||||
gfx_ = SurfaceHandle("gfx/logo_new.gif");
|
||||
pal_ = Jd8::loadPalette("gfx/logo_new.gif");
|
||||
Jd8::setScreenPalette(pal_);
|
||||
|
||||
// Surface auxiliar omplida amb el color del cursor — permet pintar
|
||||
// el "subratllat" amb un blit normal.
|
||||
cursor_surf_.adopt(Jd8::newSurface());
|
||||
std::memset(cursor_surf_.get(), CURSOR_COLOR, 64000);
|
||||
|
||||
Jd8::clearScreen(0);
|
||||
|
||||
phase_ = Phase::INITIAL;
|
||||
phase_acc_ms_ = 0;
|
||||
reveal_letter_ = 0;
|
||||
reveal_cursor_visible_ = true;
|
||||
palette_step_ = 0;
|
||||
}
|
||||
|
||||
void IntroNewLogoScene::render() {
|
||||
switch (phase_) {
|
||||
case Phase::INITIAL:
|
||||
Jd8::clearScreen(0);
|
||||
break;
|
||||
|
||||
case Phase::REVEALING: {
|
||||
Jd8::clearScreen(0);
|
||||
Jd8::blit(LOGO_DST_X, LOGO_DST_Y, gfx_, LOGO_SRC_X, LOGO_SRC_Y, LETTER_WIDTHS[reveal_letter_], LOGO_HEIGHT);
|
||||
if (reveal_cursor_visible_) {
|
||||
Jd8::blit(CURSOR_X[reveal_letter_], CURSOR_Y, cursor_surf_, 0, 0, CURSOR_W, CURSOR_H);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Phase::FULL_LOGO_FLASH:
|
||||
Jd8::clearScreen(0);
|
||||
Jd8::blit(LOGO_DST_X, LOGO_DST_Y, gfx_, LOGO_SRC_X, LOGO_SRC_Y, LETTER_WIDTHS[8], LOGO_HEIGHT);
|
||||
Jd8::blit(CURSOR_X[8], CURSOR_Y, cursor_surf_, 0, 0, CURSOR_W, CURSOR_H);
|
||||
break;
|
||||
|
||||
case Phase::PALETTE_CYCLE:
|
||||
case Phase::FINAL_WAIT:
|
||||
// Logo complet sense cursor — els pixels del cursor
|
||||
// ciclarien de color durant el cicle de paleta.
|
||||
Jd8::clearScreen(0);
|
||||
Jd8::blit(LOGO_DST_X, LOGO_DST_Y, gfx_, LOGO_SRC_X, LOGO_SRC_Y, LETTER_WIDTHS[8], LOGO_HEIGHT);
|
||||
break;
|
||||
|
||||
case Phase::SPRITES:
|
||||
case Phase::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void IntroNewLogoScene::advancePaletteCycle() {
|
||||
// Replica exacta del ciclo de paleta del doIntroNewLogo vell sobre
|
||||
// els índexs 16..31 (grup del verd brillant del logo).
|
||||
for (int i = 16; i < 32; i++) {
|
||||
if (i == 17) {
|
||||
if (pal_[i].r < 255) {
|
||||
pal_[i].r++;
|
||||
}
|
||||
if (pal_[i].g < 255) {
|
||||
pal_[i].g++;
|
||||
}
|
||||
if (pal_[i].b < 255) {
|
||||
pal_[i].b++;
|
||||
}
|
||||
}
|
||||
if (pal_[i].b < pal_[i].g) {
|
||||
pal_[i].b++;
|
||||
}
|
||||
if (pal_[i].b > pal_[i].g) {
|
||||
pal_[i].b--;
|
||||
}
|
||||
if (pal_[i].r < pal_[i].g) {
|
||||
pal_[i].r++;
|
||||
}
|
||||
if (pal_[i].r > pal_[i].g) {
|
||||
pal_[i].r--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void IntroNewLogoScene::advanceRevealing(int delta_ms) {
|
||||
phase_acc_ms_ += delta_ms;
|
||||
render();
|
||||
if (phase_acc_ms_ < REVEAL_FRAME_MS) {
|
||||
return;
|
||||
}
|
||||
phase_acc_ms_ = 0;
|
||||
reveal_cursor_visible_ = !reveal_cursor_visible_;
|
||||
if (!reveal_cursor_visible_) {
|
||||
return;
|
||||
}
|
||||
++reveal_letter_;
|
||||
if (reveal_letter_ >= 9) {
|
||||
phase_ = Phase::FULL_LOGO_FLASH;
|
||||
reveal_letter_ = 8;
|
||||
}
|
||||
}
|
||||
|
||||
void IntroNewLogoScene::advancePaletteStep(int delta_ms) {
|
||||
phase_acc_ms_ += delta_ms;
|
||||
while (phase_acc_ms_ >= PALETTE_CYCLE_STEP_MS && palette_step_ < PALETTE_CYCLE_STEPS) {
|
||||
phase_acc_ms_ -= PALETTE_CYCLE_STEP_MS;
|
||||
advancePaletteCycle();
|
||||
++palette_step_;
|
||||
}
|
||||
render();
|
||||
if (palette_step_ >= PALETTE_CYCLE_STEPS) {
|
||||
phase_ = Phase::FINAL_WAIT;
|
||||
phase_acc_ms_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void IntroNewLogoScene::advanceSpritesPhase(int delta_ms) {
|
||||
if (!sprites_scene_) {
|
||||
sprites_scene_ = std::make_unique<IntroSpritesScene>(std::move(gfx_));
|
||||
sprites_scene_->onEnter();
|
||||
}
|
||||
sprites_scene_->tick(delta_ms);
|
||||
if (sprites_scene_->done()) {
|
||||
Info::ctx.num_piramide = 0;
|
||||
phase_ = Phase::DONE;
|
||||
}
|
||||
}
|
||||
|
||||
void IntroNewLogoScene::tick(int delta_ms) {
|
||||
// Qualsevol tecla durant el revelat o el ciclo de paleta salta
|
||||
// TOTA la intro (inclou saltar la fase de sprites). Durant SPRITES
|
||||
// deixem que la sub-escena gestione el seu propi skip.
|
||||
if (phase_ != Phase::SPRITES && phase_ != Phase::DONE && Ji::anyKey()) {
|
||||
Info::ctx.num_piramide = 0;
|
||||
phase_ = Phase::DONE;
|
||||
return;
|
||||
}
|
||||
switch (phase_) {
|
||||
case Phase::INITIAL:
|
||||
phase_acc_ms_ += delta_ms;
|
||||
render();
|
||||
if (phase_acc_ms_ >= INITIAL_MS) {
|
||||
phase_ = Phase::REVEALING;
|
||||
phase_acc_ms_ = 0;
|
||||
}
|
||||
break;
|
||||
case Phase::REVEALING:
|
||||
advanceRevealing(delta_ms);
|
||||
break;
|
||||
case Phase::FULL_LOGO_FLASH:
|
||||
phase_acc_ms_ += delta_ms;
|
||||
render();
|
||||
if (phase_acc_ms_ >= FULL_LOGO_MS) {
|
||||
phase_ = Phase::PALETTE_CYCLE;
|
||||
phase_acc_ms_ = 0;
|
||||
}
|
||||
break;
|
||||
case Phase::PALETTE_CYCLE:
|
||||
advancePaletteStep(delta_ms);
|
||||
break;
|
||||
case Phase::FINAL_WAIT:
|
||||
phase_acc_ms_ += delta_ms;
|
||||
render();
|
||||
if (phase_acc_ms_ >= FINAL_WAIT_MS) {
|
||||
phase_ = Phase::SPRITES;
|
||||
}
|
||||
break;
|
||||
case Phase::SPRITES:
|
||||
advanceSpritesPhase(delta_ms);
|
||||
break;
|
||||
case Phase::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Scenes
|
||||
+21
-16
@@ -1,13 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
#include "scenes/intro_sprites_scene.hpp"
|
||||
#include "scenes/scene.hpp"
|
||||
#include "scenes/surface_handle.hpp"
|
||||
#include "game/scenes/intro_sprites_scene.hpp"
|
||||
#include "game/scenes/scene.hpp"
|
||||
#include "game/scenes/surface_handle.hpp"
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
// Intro "moderna" del logo Jailgames amb revelat lletra-a-lletra +
|
||||
// ciclo de paleta final. Reemplaça `ModuleSequence::doIntroNewLogo()`.
|
||||
@@ -36,32 +37,36 @@ namespace scenes {
|
||||
|
||||
void onEnter() override;
|
||||
void tick(int delta_ms) override;
|
||||
bool done() const override { return phase_ == Phase::Done; }
|
||||
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::DONE; }
|
||||
|
||||
private:
|
||||
enum class Phase {
|
||||
Initial, // pantalla negra 1000 ms
|
||||
Revealing, // 9 × 2 frames × 150 ms cada un
|
||||
FullLogoFlash, // logo complet + cursor, 200 ms
|
||||
PaletteCycle, // 256 passos × 20 ms modificant paleta
|
||||
FinalWait, // 20 ms final
|
||||
Sprites, // tick delegat a IntroSpritesScene fins que acaba
|
||||
Done,
|
||||
enum class Phase : std::uint8_t {
|
||||
INITIAL, // pantalla negra 1000 ms
|
||||
REVEALING, // 9 × 2 frames × 150 ms cada un
|
||||
FULL_LOGO_FLASH, // logo complet + cursor, 200 ms
|
||||
PALETTE_CYCLE, // 256 passos × 20 ms modificant paleta
|
||||
FINAL_WAIT, // 20 ms final
|
||||
SPRITES, // tick delegat a IntroSpritesScene fins que acaba
|
||||
DONE,
|
||||
};
|
||||
|
||||
void render();
|
||||
void advancePaletteCycle();
|
||||
// Helpers per a `tick()` — extrets per reduir complexitat cognitiva.
|
||||
void advanceRevealing(int delta_ms);
|
||||
void advancePaletteStep(int delta_ms);
|
||||
void advanceSpritesPhase(int delta_ms);
|
||||
|
||||
SurfaceHandle gfx_;
|
||||
SurfaceHandle cursor_surf_;
|
||||
JD8_Palette pal_{nullptr}; // propietat transferida a main_palette via SetScreenPalette
|
||||
Jd8::Palette pal_{nullptr}; // propietat transferida a main_palette via SetScreenPalette
|
||||
std::unique_ptr<IntroSpritesScene> sprites_scene_;
|
||||
|
||||
Phase phase_{Phase::Initial};
|
||||
Phase phase_{Phase::INITIAL};
|
||||
int phase_acc_ms_{0};
|
||||
int reveal_letter_{0};
|
||||
bool reveal_cursor_visible_{true};
|
||||
int palette_step_{0};
|
||||
};
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -1,13 +1,13 @@
|
||||
#include "scenes/intro_scene.hpp"
|
||||
#include "game/scenes/intro_scene.hpp"
|
||||
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
#include "core/jail/jinput.hpp"
|
||||
#include "game/info.hpp"
|
||||
#include "scenes/scene_utils.hpp"
|
||||
#include "game/scenes/scene_utils.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Timings idèntics als del vell `doIntro()`: el JG_SetUpdateTicks(1000)
|
||||
// Timings idèntics als del vell `doIntro()`: el Jg::setUpdateTicks(1000)
|
||||
// inicial, (100) per a les 3 primeres lletres (J, A, I), (200) per a
|
||||
// "JAIL" i el seu clear, (100) per a les 4 lletres centrals
|
||||
// (G, A, M, E) i (200) per a la resta fins al cicle de paleta.
|
||||
@@ -27,21 +27,21 @@ namespace {
|
||||
};
|
||||
|
||||
constexpr RevealStep REVEAL_STEPS[] = {
|
||||
{100, 27, 68, false, false}, // J
|
||||
{100, 53, 96, false, false}, // JA
|
||||
{100, 66, 109, false, false}, // JAI
|
||||
{200, 92, 136, false, false}, // JAIL
|
||||
{200, 92, -1, true, false}, // JAIL (clear, sense avió — parpelleig)
|
||||
{100, 118, 160, false, false}, // JAILG
|
||||
{100, 145, 188, false, false}, // JAILGA
|
||||
{100, 178, 221, false, false}, // JAILGAM
|
||||
{100, 205, 248, false, false}, // JAILGAME
|
||||
{200, 0, 274, false, true}, // JAILGAMES (wordmark complet) + avió
|
||||
{200, 0, -1, true, true}, // JAILGAMES (clear, sense avió)
|
||||
{200, 0, 274, false, true}, // JAILGAMES + avió (sense clear)
|
||||
{200, 0, -1, true, true}, // JAILGAMES (clear, sense avió)
|
||||
{200, 0, 274, false, true}, // JAILGAMES + avió (sense clear)
|
||||
{200, 0, -1, true, true}, // JAILGAMES (clear, sense avió)
|
||||
{.duration_ms = 100, .body_w = 27, .plane_x = 68, .clear = false, .wordmark = false}, // J
|
||||
{.duration_ms = 100, .body_w = 53, .plane_x = 96, .clear = false, .wordmark = false}, // JA
|
||||
{.duration_ms = 100, .body_w = 66, .plane_x = 109, .clear = false, .wordmark = false}, // JAI
|
||||
{.duration_ms = 200, .body_w = 92, .plane_x = 136, .clear = false, .wordmark = false}, // JAIL
|
||||
{.duration_ms = 200, .body_w = 92, .plane_x = -1, .clear = true, .wordmark = false}, // JAIL (clear, sense avió — parpelleig)
|
||||
{.duration_ms = 100, .body_w = 118, .plane_x = 160, .clear = false, .wordmark = false}, // JAILG
|
||||
{.duration_ms = 100, .body_w = 145, .plane_x = 188, .clear = false, .wordmark = false}, // JAILGA
|
||||
{.duration_ms = 100, .body_w = 178, .plane_x = 221, .clear = false, .wordmark = false}, // JAILGAM
|
||||
{.duration_ms = 100, .body_w = 205, .plane_x = 248, .clear = false, .wordmark = false}, // JAILGAME
|
||||
{.duration_ms = 200, .body_w = 0, .plane_x = 274, .clear = false, .wordmark = true}, // JAILGAMES (wordmark complet) + avió
|
||||
{.duration_ms = 200, .body_w = 0, .plane_x = -1, .clear = true, .wordmark = true}, // JAILGAMES (clear, sense avió)
|
||||
{.duration_ms = 200, .body_w = 0, .plane_x = 274, .clear = false, .wordmark = true}, // JAILGAMES + avió (sense clear)
|
||||
{.duration_ms = 200, .body_w = 0, .plane_x = -1, .clear = true, .wordmark = true}, // JAILGAMES (clear, sense avió)
|
||||
{.duration_ms = 200, .body_w = 0, .plane_x = 274, .clear = false, .wordmark = true}, // JAILGAMES + avió (sense clear)
|
||||
{.duration_ms = 200, .body_w = 0, .plane_x = -1, .clear = true, .wordmark = true}, // JAILGAMES (clear, sense avió)
|
||||
};
|
||||
constexpr int REVEAL_COUNT = sizeof(REVEAL_STEPS) / sizeof(REVEAL_STEPS[0]);
|
||||
|
||||
@@ -49,32 +49,31 @@ namespace {
|
||||
// blit únic del wordmark "JAILGAMES" complet (231×45 al destí 43,78).
|
||||
// IntroScene només s'activa quan use_new_logo == false, així que la
|
||||
// branca use_new_logo d'aquell helper aquí no es necessita.
|
||||
void drawWordmark(JD8_Surface gfx) {
|
||||
JD8_Blit(43, 78, gfx, 43, 155, 231, 45);
|
||||
void drawWordmark(const Uint8* gfx) {
|
||||
Jd8::blit(43, 78, gfx, 43, 155, 231, 45);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
IntroScene::IntroScene() = default;
|
||||
|
||||
IntroScene::~IntroScene() {
|
||||
// No alliberem `pal_`: JD8_SetScreenPalette n'ha pres ownership i el
|
||||
// proper SetScreenPalette / FadeToPal la lliurarà. Alliberar-la ací
|
||||
// provocaria double free.
|
||||
}
|
||||
// No alliberem `pal_`: Jd8::setScreenPalette n'ha pres ownership i el
|
||||
// proper SetScreenPalette / FadeToPal la lliurarà. Alliberar-la ací
|
||||
// provocaria double free.
|
||||
IntroScene::~IntroScene() = default;
|
||||
|
||||
void IntroScene::onEnter() {
|
||||
playMusic("music/menu.ogg");
|
||||
|
||||
gfx_ = SurfaceHandle("gfx/logo.gif");
|
||||
pal_ = JD8_LoadPalette("gfx/logo.gif");
|
||||
JD8_SetScreenPalette(pal_);
|
||||
pal_ = Jd8::loadPalette("gfx/logo.gif");
|
||||
Jd8::setScreenPalette(pal_);
|
||||
|
||||
JD8_ClearScreen(0);
|
||||
Jd8::clearScreen(0);
|
||||
|
||||
phase_ = Phase::InitialWait;
|
||||
phase_ = Phase::INITIAL_WAIT;
|
||||
phase_acc_ms_ = 0;
|
||||
reveal_index_ = 0;
|
||||
palette_step_ = 0;
|
||||
@@ -82,35 +81,37 @@ namespace scenes {
|
||||
|
||||
void IntroScene::render() {
|
||||
switch (phase_) {
|
||||
case Phase::InitialWait:
|
||||
JD8_ClearScreen(0);
|
||||
case Phase::INITIAL_WAIT:
|
||||
Jd8::clearScreen(0);
|
||||
break;
|
||||
|
||||
case Phase::Reveal: {
|
||||
case Phase::REVEAL: {
|
||||
const RevealStep& s = REVEAL_STEPS[reveal_index_];
|
||||
if (s.clear) JD8_ClearScreen(0);
|
||||
if (s.clear) {
|
||||
Jd8::clearScreen(0);
|
||||
}
|
||||
if (s.wordmark) {
|
||||
drawWordmark(gfx_);
|
||||
} else if (s.body_w > 0) {
|
||||
JD8_Blit(43, 78, gfx_, 43, 155, s.body_w, 45);
|
||||
Jd8::blit(43, 78, gfx_, 43, 155, s.body_w, 45);
|
||||
}
|
||||
if (s.plane_x >= 0) {
|
||||
JD8_Blit(s.plane_x, 78, gfx_, 274, 155, 27, 45);
|
||||
Jd8::blit(s.plane_x, 78, gfx_, 274, 155, 27, 45);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Phase::PaletteCycle:
|
||||
case Phase::FinalWait:
|
||||
case Phase::PALETTE_CYCLE:
|
||||
case Phase::FINAL_WAIT:
|
||||
// Wordmark complet fix mentre cicla la paleta — l'últim
|
||||
// pas del revelat (PAS 15) deixa la pantalla en aquest mateix
|
||||
// estat, i el vell doIntro no redibuixava durant el cicle.
|
||||
JD8_ClearScreen(0);
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx_);
|
||||
break;
|
||||
|
||||
case Phase::Sprites:
|
||||
case Phase::Done:
|
||||
case Phase::SPRITES:
|
||||
case Phase::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -121,52 +122,66 @@ namespace scenes {
|
||||
// els altres convergeixen cap al mateix gris mitjà.
|
||||
for (int i = 16; i < 32; i++) {
|
||||
if (i == 17) {
|
||||
if (pal_[i].r < 255) pal_[i].r++;
|
||||
if (pal_[i].g < 255) pal_[i].g++;
|
||||
if (pal_[i].b < 255) pal_[i].b++;
|
||||
if (pal_[i].r < 255) {
|
||||
pal_[i].r++;
|
||||
}
|
||||
if (pal_[i].g < 255) {
|
||||
pal_[i].g++;
|
||||
}
|
||||
if (pal_[i].b < 255) {
|
||||
pal_[i].b++;
|
||||
}
|
||||
}
|
||||
if (pal_[i].b < pal_[i].g) {
|
||||
pal_[i].b++;
|
||||
}
|
||||
if (pal_[i].b > pal_[i].g) {
|
||||
pal_[i].b--;
|
||||
}
|
||||
if (pal_[i].r < pal_[i].g) {
|
||||
pal_[i].r++;
|
||||
}
|
||||
if (pal_[i].r > pal_[i].g) {
|
||||
pal_[i].r--;
|
||||
}
|
||||
if (pal_[i].b < pal_[i].g) pal_[i].b++;
|
||||
if (pal_[i].b > pal_[i].g) pal_[i].b--;
|
||||
if (pal_[i].r < pal_[i].g) pal_[i].r++;
|
||||
if (pal_[i].r > pal_[i].g) pal_[i].r--;
|
||||
}
|
||||
}
|
||||
|
||||
void IntroScene::tick(int delta_ms) {
|
||||
// Qualsevol tecla durant revelat/paleta salta TOTA la intro
|
||||
// (inclou saltar la fase de sprites). Durant Sprites deixem que
|
||||
// (inclou saltar la fase de sprites). Durant SPRITES deixem que
|
||||
// la sub-escena gestione el seu propi skip internament, que a més
|
||||
// respecta la fase "final" no skippable de la variant 0.
|
||||
if (phase_ != Phase::Sprites && phase_ != Phase::Done && JI_AnyKey()) {
|
||||
info::ctx.num_piramide = 0;
|
||||
phase_ = Phase::Done;
|
||||
if (phase_ != Phase::SPRITES && phase_ != Phase::DONE && Ji::anyKey()) {
|
||||
Info::ctx.num_piramide = 0;
|
||||
phase_ = Phase::DONE;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (phase_) {
|
||||
case Phase::InitialWait:
|
||||
case Phase::INITIAL_WAIT:
|
||||
phase_acc_ms_ += delta_ms;
|
||||
render();
|
||||
if (phase_acc_ms_ >= INITIAL_MS) {
|
||||
phase_ = Phase::Reveal;
|
||||
phase_ = Phase::REVEAL;
|
||||
phase_acc_ms_ = 0;
|
||||
reveal_index_ = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::Reveal:
|
||||
case Phase::REVEAL:
|
||||
phase_acc_ms_ += delta_ms;
|
||||
render();
|
||||
if (phase_acc_ms_ >= REVEAL_STEPS[reveal_index_].duration_ms) {
|
||||
phase_acc_ms_ = 0;
|
||||
++reveal_index_;
|
||||
if (reveal_index_ >= REVEAL_COUNT) {
|
||||
phase_ = Phase::PaletteCycle;
|
||||
phase_ = Phase::PALETTE_CYCLE;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::PaletteCycle:
|
||||
case Phase::PALETTE_CYCLE:
|
||||
phase_acc_ms_ += delta_ms;
|
||||
// Avancem tants passos com permet el delta, per evitar
|
||||
// saltar-ne si el frame ha vingut lent.
|
||||
@@ -178,20 +193,20 @@ namespace scenes {
|
||||
}
|
||||
render();
|
||||
if (palette_step_ >= PALETTE_CYCLE_STEPS) {
|
||||
phase_ = Phase::FinalWait;
|
||||
phase_ = Phase::FINAL_WAIT;
|
||||
phase_acc_ms_ = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::FinalWait:
|
||||
case Phase::FINAL_WAIT:
|
||||
phase_acc_ms_ += delta_ms;
|
||||
render();
|
||||
if (phase_acc_ms_ >= FINAL_WAIT_MS) {
|
||||
phase_ = Phase::Sprites;
|
||||
phase_ = Phase::SPRITES;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::Sprites:
|
||||
case Phase::SPRITES:
|
||||
// Sub-escena construïda al vol al primer tick d'aquesta fase.
|
||||
// Transferim el gfx_ per move — la sub-escena se n'ocupa
|
||||
// fins que es destruix. Una vegada feta, els ticks delegats
|
||||
@@ -205,14 +220,14 @@ namespace scenes {
|
||||
// Equivalent al vell `Go()` post-switch: passem al menú.
|
||||
// Sense açò el while del fiber tornaria a crear IntroScene
|
||||
// infinitament amb num_piramide encara a 255.
|
||||
info::ctx.num_piramide = 0;
|
||||
phase_ = Phase::Done;
|
||||
Info::ctx.num_piramide = 0;
|
||||
phase_ = Phase::DONE;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::Done:
|
||||
case Phase::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -1,13 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
#include "scenes/intro_sprites_scene.hpp"
|
||||
#include "scenes/scene.hpp"
|
||||
#include "scenes/surface_handle.hpp"
|
||||
#include "game/scenes/intro_sprites_scene.hpp"
|
||||
#include "game/scenes/scene.hpp"
|
||||
#include "game/scenes/surface_handle.hpp"
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
// Intro "legacy" del wordmark JAILGAMES lletra a lletra + cicle de paleta.
|
||||
// Reemplaça `ModuleSequence::doIntro()`. S'activa quan
|
||||
@@ -37,29 +38,29 @@ namespace scenes {
|
||||
|
||||
void onEnter() override;
|
||||
void tick(int delta_ms) override;
|
||||
bool done() const override { return phase_ == Phase::Done; }
|
||||
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::DONE; }
|
||||
|
||||
private:
|
||||
enum class Phase {
|
||||
InitialWait, // 1000 ms pantalla negra
|
||||
Reveal, // 15 passos del wordmark
|
||||
PaletteCycle, // 256 × 20 ms mutant pal[16..31]
|
||||
FinalWait, // 200 ms abans de la sub-escena de sprites
|
||||
Sprites, // tick delegat a IntroSpritesScene fins que acaba
|
||||
Done,
|
||||
enum class Phase : std::uint8_t {
|
||||
INITIAL_WAIT, // 1000 ms pantalla negra
|
||||
REVEAL, // 15 passos del wordmark
|
||||
PALETTE_CYCLE, // 256 × 20 ms mutant pal[16..31]
|
||||
FINAL_WAIT, // 200 ms abans de la sub-escena de sprites
|
||||
SPRITES, // tick delegat a IntroSpritesScene fins que acaba
|
||||
DONE,
|
||||
};
|
||||
|
||||
void render();
|
||||
void advancePaletteCycle();
|
||||
|
||||
SurfaceHandle gfx_;
|
||||
JD8_Palette pal_{nullptr}; // propietat transferida a main_palette via SetScreenPalette
|
||||
Jd8::Palette pal_{nullptr}; // propietat transferida a main_palette via SetScreenPalette
|
||||
std::unique_ptr<IntroSpritesScene> sprites_scene_;
|
||||
|
||||
Phase phase_{Phase::InitialWait};
|
||||
Phase phase_{Phase::INITIAL_WAIT};
|
||||
int phase_acc_ms_{0};
|
||||
int reveal_index_{0};
|
||||
int palette_step_{0};
|
||||
};
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -0,0 +1,370 @@
|
||||
#include "game/scenes/intro_sprites_scene.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
#include "core/jail/jinput.hpp"
|
||||
#include "game/options.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// Duració d'un pas. El vell doIntroSprites feia Jg::setUpdateTicks(20);
|
||||
// cada iteració del seu for (i) consumia un tick de 20 ms.
|
||||
constexpr int TICK_MS = 20;
|
||||
|
||||
// Taules de frames. Ubicacions de cada sprite dins el gfx de la intro
|
||||
// (gfx/logo.gif o gfx/logo_new.gif — el layout de sprites és el mateix).
|
||||
// Cada sprite ocupa 15×15 px, disposats horitzontalment per fila.
|
||||
// Els valors són els offsets x (la y la posa l'invocador al src_y).
|
||||
// Derivats dels `fr_ani_N[i] = ...` del vell doIntroSprites.
|
||||
constexpr Uint16 FR1[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180}; // camina dreta (y=0)
|
||||
constexpr Uint16 FR2[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180}; // camina esquerra (y=15)
|
||||
constexpr Uint16 FR3[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150}; // trau mapa dreta (y=30)
|
||||
constexpr Uint16 FR4[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150}; // trau mapa esquerra (y=45)
|
||||
constexpr Uint16 FR5[] = {165, 180, 195, 210, 225, 240, 255, 270, 285, 300, 300, 285, 270, 255, 240, 225, 210, 195, 180, 165}; // bot de susto (y=45, mirror)
|
||||
constexpr Uint16 FR6[] = {0, 15, 30, 45, 60, 75, 90, 105}; // momia (y=60)
|
||||
constexpr Uint16 FR7[] = {75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240, 255, 270, // paper (y=75, idx 0..13)
|
||||
0,
|
||||
15,
|
||||
30,
|
||||
45,
|
||||
60,
|
||||
75,
|
||||
90,
|
||||
105,
|
||||
120,
|
||||
135,
|
||||
150,
|
||||
165,
|
||||
180,
|
||||
195,
|
||||
210}; // sombra (y=105, idx 14..28)
|
||||
constexpr Uint16 FR8[] = {15, 30, 45, 60}; // pedra (y=75)
|
||||
constexpr Uint16 FR9[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225}; // prota ball (y=120)
|
||||
constexpr Uint16 FR10[] = {0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225}; // momia ball (y=135)
|
||||
constexpr Uint16 FR11[] = {15, 30, 45, 60, 75, 60}; // altaveu (y=90, [5]=[3] pel loop de 4)
|
||||
|
||||
constexpr Uint16 CREU = 75; // src_y de la creu (overlay)
|
||||
constexpr Uint16 INTERROGANT = 90; // src_y del signe d'interrogant
|
||||
|
||||
// Equivalent de la funció `drawIntroWordmark` de modulesequence.cpp.
|
||||
// Branqueja segons use_new_logo perquè la mateixa sub-escena es
|
||||
// reutilitza des de IntroScene (logo vell) i IntroNewLogoScene (logo
|
||||
// nou) amb arxius diferents però mateix layout de sprites.
|
||||
void drawWordmark(const Uint8* gfx) {
|
||||
if (Options::game.use_new_logo) {
|
||||
// Centrat: (320 − 188) / 2 = 66 (IntroNewLogoScene usa la mateixa x).
|
||||
Jd8::blit(66, 78, gfx, 60, 158, 188, 28);
|
||||
} else {
|
||||
Jd8::blit(43, 78, gfx, 43, 155, 231, 45);
|
||||
}
|
||||
}
|
||||
|
||||
using RenderFn = void (*)(const Uint8*, int);
|
||||
|
||||
// Una fase — rang [start_i..end_i] inclusive (direcció implícita per
|
||||
// signe), funció de render, i flag d'skippable. Totes les fases actuals
|
||||
// són skippables; el flag es conserva per si alguna futura ha de ser
|
||||
// no interrompuda (p.ex. un logo fatídic que cal veure sencer).
|
||||
struct SpritePhase {
|
||||
int start_i;
|
||||
int end_i;
|
||||
RenderFn render;
|
||||
bool skippable;
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// Variant 0 — Interrogant / Momia
|
||||
// =========================================================================
|
||||
|
||||
void v0WalkRight(const Uint8* gfx, int i) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(i, 150, gfx, FR1[(i / 5) % 13], 0, 15, 15, 0);
|
||||
}
|
||||
|
||||
void v0PullMapRight(const Uint8* gfx, int i) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(200, 150, gfx, FR3[std::min(i / 5, 10)], 30, 15, 15, 0);
|
||||
}
|
||||
|
||||
void v0WalkLeftTo80(const Uint8* gfx, int i) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(i, 150, gfx, FR2[(i / 5) % 13], 15, 15, 15, 0);
|
||||
}
|
||||
|
||||
void v0PullMapLeft(const Uint8* gfx, int i) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(80, 150, gfx, FR4[std::min(i / 5, 10)], 45, 15, 15, 0);
|
||||
}
|
||||
|
||||
void v0MomiaLeft(const Uint8* gfx, int i) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(i, 150, gfx, FR6[(i / 5) % 8], 60, 15, 15, 0);
|
||||
Jd8::blitCK(80, 150, gfx, FR4[10], 45, 15, 15, 0);
|
||||
}
|
||||
|
||||
void v0Turn(const Uint8* gfx, int /*i*/) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(80, 150, gfx, FR1[1], 0, 15, 15, 0);
|
||||
Jd8::blitCK(95, 150, gfx, FR6[4], 60, 15, 15, 0);
|
||||
Jd8::blitCK(80, 133, gfx, 0, INTERROGANT, 15, 15, 0);
|
||||
}
|
||||
|
||||
void v0Jump1(const Uint8* gfx, int i) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(80, 150 - ((i % 50) / 5), gfx, FR5[std::min(i / 5, 19)], 45, 15, 15, 0);
|
||||
Jd8::blitCK(95, 150, gfx, FR6[4], 60, 15, 15, 0);
|
||||
}
|
||||
|
||||
void v0Jump2(const Uint8* gfx, int i) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(80, 140 + ((i % 50) / 5), gfx, FR5[std::min(i / 5, 19)], 45, 15, 15, 0);
|
||||
Jd8::blitCK(95, 150, gfx, FR6[4], 60, 15, 15, 0);
|
||||
}
|
||||
|
||||
void v0WalkFinal(const Uint8* gfx, int i) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(i, 150, gfx, FR2[(i / 5) % 13], 15, 15, 15, 0);
|
||||
Jd8::blitCK(95, 150, gfx, FR6[4], 60, 15, 15, 0);
|
||||
}
|
||||
|
||||
void v0Final(const Uint8* gfx, int /*i*/) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(95, 150, gfx, FR6[4], 60, 15, 15, 0);
|
||||
Jd8::blitCK(95, 133, gfx, 0, INTERROGANT, 15, 15, 0);
|
||||
}
|
||||
|
||||
constexpr SpritePhase VARIANT_0[] = {
|
||||
{.start_i = 0, .end_i = 200, .render = v0WalkRight, .skippable = true},
|
||||
{.start_i = 0, .end_i = 200, .render = v0PullMapRight, .skippable = true},
|
||||
{.start_i = 200, .end_i = 0, .render = v0PullMapRight, .skippable = true}, // guarda el mapa (reprodueix inversament)
|
||||
{.start_i = 200, .end_i = 80, .render = v0WalkLeftTo80, .skippable = true},
|
||||
{.start_i = 0, .end_i = 200, .render = v0PullMapLeft, .skippable = true},
|
||||
{.start_i = 300, .end_i = 95, .render = v0MomiaLeft, .skippable = true},
|
||||
{.start_i = 0, .end_i = 50, .render = v0Turn, .skippable = true},
|
||||
{.start_i = 0, .end_i = 49, .render = v0Jump1, .skippable = true},
|
||||
{.start_i = 50, .end_i = 99, .render = v0Jump2, .skippable = true},
|
||||
{.start_i = 80, .end_i = 0, .render = v0WalkFinal, .skippable = true},
|
||||
{.start_i = 0, .end_i = 150, .render = v0Final, .skippable = true},
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// Variant 1 — Creu / Pedra
|
||||
// =========================================================================
|
||||
|
||||
void v1WalkRight(const Uint8* gfx, int i) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
|
||||
Jd8::blitCK(i, 150, gfx, FR1[(i / 5) % 13], 0, 15, 15, 255);
|
||||
}
|
||||
|
||||
void v1PullMap(const Uint8* gfx, int i) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
|
||||
Jd8::blitCK(200, 150, gfx, FR3[std::min(i / 5, 10)], 30, 15, 15, 255);
|
||||
}
|
||||
|
||||
void v1Interrogant(const Uint8* gfx, int /*i*/) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
|
||||
Jd8::blitCK(200, 134, gfx, 0, INTERROGANT, 15, 15, 255);
|
||||
Jd8::blitCK(200, 150, gfx, FR3[10], 30, 15, 15, 255);
|
||||
}
|
||||
|
||||
void v1DropMap(const Uint8* gfx, int i) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
|
||||
const int IDX = std::min(i / 5, 28);
|
||||
// FR7 té 29 frames dividits en dos grups: paper (idx 0..13, src_y=75)
|
||||
// i sombra (idx 14..28, src_y=105). El vell feia una branca al bucle.
|
||||
if (IDX <= 13) {
|
||||
Jd8::blitCK(200, 150, gfx, FR7[IDX], 75, 15, 15, 255);
|
||||
} else {
|
||||
Jd8::blitCK(200, 150, gfx, FR7[IDX], 105, 15, 15, 255);
|
||||
}
|
||||
}
|
||||
|
||||
void v1StoneFall(const Uint8* gfx, int i) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
|
||||
Jd8::blitCK(200, 150, gfx, FR7[28], 105, 15, 15, 255);
|
||||
Jd8::blitCK(200, i * 2, gfx, FR8[0], 75, 15, 15, 255);
|
||||
}
|
||||
|
||||
void v1StoneBreak(const Uint8* gfx, int i) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
|
||||
Jd8::blitCK(200, 150, gfx, FR8[i / 10], 75, 15, 15, 255);
|
||||
}
|
||||
|
||||
void v1Final(const Uint8* gfx, int /*i*/) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(200, 155, gfx, 0, CREU, 15, 15, 255);
|
||||
Jd8::blitCK(200, 150, gfx, FR8[1], 75, 15, 15, 255);
|
||||
Jd8::blitCK(185, 150, gfx, FR8[2], 75, 15, 15, 255);
|
||||
Jd8::blitCK(215, 150, gfx, FR8[3], 75, 15, 15, 255);
|
||||
}
|
||||
|
||||
constexpr SpritePhase VARIANT_1[] = {
|
||||
{.start_i = 0, .end_i = 200, .render = v1WalkRight, .skippable = true},
|
||||
{.start_i = 0, .end_i = 300, .render = v1PullMap, .skippable = true},
|
||||
{.start_i = 0, .end_i = 100, .render = v1Interrogant, .skippable = true},
|
||||
{.start_i = 0, .end_i = 200, .render = v1DropMap, .skippable = true},
|
||||
{.start_i = 0, .end_i = 75, .render = v1StoneFall, .skippable = true},
|
||||
{.start_i = 0, .end_i = 19, .render = v1StoneBreak, .skippable = true},
|
||||
{.start_i = 0, .end_i = 200, .render = v1Final, .skippable = true},
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// Variant 2 — Ball de carnaval
|
||||
// =========================================================================
|
||||
|
||||
void v2Approach(const Uint8* gfx, int i) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(i, 150, gfx, FR1[(i / 5) % 13], 0, 15, 15, 255);
|
||||
Jd8::blitCK(304 - i, 150, gfx, FR6[(i / 10) % 8], 60, 15, 15, 255);
|
||||
}
|
||||
|
||||
void v2Still(const Uint8* gfx, int /*i*/) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(145, 150, gfx, FR1[1], 0, 15, 15, 255);
|
||||
Jd8::blitCK(160, 150, gfx, FR6[1], 60, 15, 15, 255);
|
||||
}
|
||||
|
||||
void v2Horn(const Uint8* gfx, int i) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(125, 150, gfx, FR11[(i / 10) % 2], 90, 15, 15, 255);
|
||||
Jd8::blitCK(145, 150, gfx, FR1[1], 0, 15, 15, 255);
|
||||
Jd8::blitCK(160, 150, gfx, FR6[1], 60, 15, 15, 255);
|
||||
}
|
||||
|
||||
void v2Ball(const Uint8* gfx, int i) {
|
||||
Jd8::clearScreen(0);
|
||||
drawWordmark(gfx);
|
||||
Jd8::blitCK(145, 150, gfx, FR9[(i / 10) % 16], 120, 15, 15, 255);
|
||||
Jd8::blitCK(160, 150, gfx, FR10[(i / 10) % 16], 135, 15, 15, 255);
|
||||
Jd8::blitCK(125, 150, gfx, FR11[((i / 5) % 4) + 2], 90, 15, 15, 255);
|
||||
}
|
||||
|
||||
constexpr SpritePhase VARIANT_2[] = {
|
||||
{.start_i = 0, .end_i = 145, .render = v2Approach, .skippable = true},
|
||||
{.start_i = 0, .end_i = 100, .render = v2Still, .skippable = true},
|
||||
{.start_i = 0, .end_i = 50, .render = v2Horn, .skippable = true},
|
||||
{.start_i = 0, .end_i = 800, .render = v2Ball, .skippable = true},
|
||||
};
|
||||
|
||||
// =========================================================================
|
||||
// Dispatch per variant
|
||||
// =========================================================================
|
||||
|
||||
auto variantTable(int variant) -> const SpritePhase* {
|
||||
switch (variant) {
|
||||
case 0:
|
||||
return VARIANT_0;
|
||||
case 1:
|
||||
return VARIANT_1;
|
||||
case 2:
|
||||
return VARIANT_2;
|
||||
default:
|
||||
return VARIANT_0;
|
||||
}
|
||||
}
|
||||
|
||||
auto variantLength(int variant) -> int {
|
||||
switch (variant) {
|
||||
case 0:
|
||||
return sizeof(VARIANT_0) / sizeof(VARIANT_0[0]);
|
||||
case 1:
|
||||
return sizeof(VARIANT_1) / sizeof(VARIANT_1[0]);
|
||||
case 2:
|
||||
return sizeof(VARIANT_2) / sizeof(VARIANT_2[0]);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
auto phaseStepCount(const SpritePhase& p) -> int {
|
||||
return std::abs(p.end_i - p.start_i) + 1;
|
||||
}
|
||||
|
||||
auto phaseCurrentI(const SpritePhase& p, int step) -> int {
|
||||
return p.end_i >= p.start_i ? p.start_i + step : p.start_i - step;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace Scenes {
|
||||
|
||||
IntroSpritesScene::IntroSpritesScene(SurfaceHandle&& gfx)
|
||||
: gfx_(std::move(gfx)) {}
|
||||
|
||||
void IntroSpritesScene::onEnter() {
|
||||
// El vell doIntroSprites feia `rand() % 3` al principi. El seed ve
|
||||
// establert per `srand(time(0))` al boot del joc (info.cpp / main),
|
||||
// així que la variant canvia entre execucions.
|
||||
variant_ = std::rand() % 3;
|
||||
phase_ = 0;
|
||||
phase_step_ = 0;
|
||||
step_acc_ms_ = 0;
|
||||
done_ = false;
|
||||
|
||||
// Renderitzem ja el primer frame (step 0 de la primera fase) perquè
|
||||
// el Jd8::flip del mini-loop del fiber el pinte al primer cicle.
|
||||
const SpritePhase* phases = variantTable(variant_);
|
||||
phases[0].render(gfx_.get(), phaseCurrentI(phases[0], 0));
|
||||
}
|
||||
|
||||
void IntroSpritesScene::tick(int delta_ms) {
|
||||
if (done_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const SpritePhase* phases = variantTable(variant_);
|
||||
const int NUM_PHASES = variantLength(variant_);
|
||||
|
||||
// Skip per tecla. Durant la fase marcada com a no skippable (només
|
||||
// v0Final al vell codi) s'ignora — preserva la semàntica del vell
|
||||
// bucle final de la variant 0 que no cridava wait_frame_or_skip.
|
||||
if (phases[phase_].skippable && Ji::anyKey()) {
|
||||
done_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
step_acc_ms_ += delta_ms;
|
||||
while (step_acc_ms_ >= TICK_MS && !done_) {
|
||||
step_acc_ms_ -= TICK_MS;
|
||||
++phase_step_;
|
||||
if (phase_step_ >= phaseStepCount(phases[phase_])) {
|
||||
++phase_;
|
||||
phase_step_ = 0;
|
||||
if (phase_ >= NUM_PHASES) {
|
||||
done_ = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
phases[phase_].render(gfx_.get(), phaseCurrentI(phases[phase_], phase_step_));
|
||||
}
|
||||
|
||||
} // namespace Scenes
|
||||
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "scenes/scene.hpp"
|
||||
#include "scenes/surface_handle.hpp"
|
||||
#include "game/scenes/scene.hpp"
|
||||
#include "game/scenes/surface_handle.hpp"
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
// Sub-escena de sprites de la intro (prota + momia + mapa + etc).
|
||||
// Reemplaça `ModuleSequence::doIntroSprites()`. No es registra al
|
||||
@@ -28,7 +28,7 @@ namespace scenes {
|
||||
|
||||
void onEnter() override;
|
||||
void tick(int delta_ms) override;
|
||||
bool done() const override { return done_; }
|
||||
[[nodiscard]] auto done() const -> bool override { return done_; }
|
||||
|
||||
private:
|
||||
SurfaceHandle gfx_;
|
||||
@@ -39,4 +39,4 @@ namespace scenes {
|
||||
bool done_{false};
|
||||
};
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "game/scenes/menu_scene.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
#include "core/jail/jinput.hpp"
|
||||
#include "game/info.hpp"
|
||||
|
||||
namespace Scenes {
|
||||
|
||||
void MenuScene::onEnter() {
|
||||
fondo_ = SurfaceHandle("gfx/menu.gif");
|
||||
gfx_ = SurfaceHandle("gfx/menu2.gif");
|
||||
|
||||
// Pintat inicial (congelat durant el fade-in de paleta). El loop
|
||||
// d'animació repintarà tot des de zero en el primer tick de SHOWING.
|
||||
Jd8::blit(fondo_);
|
||||
Jd8::blitCK(100, 25, gfx_, 0, 74, 124, 68, 255); // logo
|
||||
Jd8::blitCK(130, 100, gfx_, 0, 0, 80, 74, 255); // camell (frame 0)
|
||||
Jd8::blitCK(0, 150, gfx_, 0, 150, 320, 50, 255); // base "jdes"
|
||||
|
||||
Jd8::Palette pal = Jd8::loadPalette("gfx/menu2.gif");
|
||||
fade_.startFadeTo(pal);
|
||||
delete[] pal;
|
||||
|
||||
phase_ = Phase::FADING_IN;
|
||||
}
|
||||
|
||||
void MenuScene::render() {
|
||||
// Cel estàtic (els primers 100 pixels verticals)
|
||||
Jd8::blit(0, 0, fondo_, 0, 0, 320, 100);
|
||||
|
||||
// Fondo mòvil (horitzó) amb wrap a 320
|
||||
Jd8::blitCK(horitzo_, 100, fondo_, 0, 100, 320 - horitzo_, 100, 255);
|
||||
Jd8::blitCK(0, 100, fondo_, 320 - horitzo_, 100, horitzo_, 100, 255);
|
||||
|
||||
// Logo i camell animat
|
||||
Jd8::blitCK(100, 25, gfx_, 0, 74, 124, 68, 255);
|
||||
Jd8::blitCK(130, 100, gfx_, camello_.frame() * 80, 0, 80, 74, 255);
|
||||
|
||||
// Palmeres mòvils amb wrap a 320
|
||||
Jd8::blitCK(palmeres_, 150, gfx_, 0, 150, 320 - palmeres_, 50, 255);
|
||||
Jd8::blitCK(0, 150, gfx_, 320 - palmeres_, 150, palmeres_, 50, 255);
|
||||
|
||||
// "jdes" estàtic (davant dels scrollers) i versió a la cantonada
|
||||
Jd8::blitCK(87, 167, gfx_, 127, 124, 150, 24, 255);
|
||||
Jd8::blitCK(303, 193, gfx_, 305, 143, 15, 5, 255);
|
||||
|
||||
// "Polsa tecla" parpallejant. Al vell `contador % 100 > 30` amb
|
||||
// updateTicks=20 ms, el cicle són 2000 ms amb un llindar de 600 ms:
|
||||
// amagat els primers 600 ms, visible els següents 1400 ms.
|
||||
const bool BLINK_ON = (blink_ms_ % 2000) > 600;
|
||||
if (BLINK_ON) {
|
||||
Jd8::blitCK(98, 130, gfx_, 161, 92, 127, 9, 255);
|
||||
if (Info::ctx.nou_personatge) {
|
||||
Jd8::blitCK(68, 141, gfx_, 128, 105, 189, 9, 255);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MenuScene::tick(int delta_ms) {
|
||||
switch (phase_) {
|
||||
case Phase::FADING_IN:
|
||||
fade_.tick(delta_ms);
|
||||
if (fade_.done()) {
|
||||
phase_ = Phase::SHOWING;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::SHOWING: {
|
||||
// Palmeres: 1 pixel cada 80 ms (= cada 4 ticks × 20 ms originals)
|
||||
palmeres_acc_ms_ += delta_ms;
|
||||
while (palmeres_acc_ms_ >= 80) {
|
||||
palmeres_acc_ms_ -= 80;
|
||||
if (--palmeres_ < 0) {
|
||||
palmeres_ = 319;
|
||||
}
|
||||
}
|
||||
|
||||
// Horitzó: 1 pixel cada 320 ms (= cada 16 ticks × 20 ms)
|
||||
horitzo_acc_ms_ += delta_ms;
|
||||
while (horitzo_acc_ms_ >= 320) {
|
||||
horitzo_acc_ms_ -= 320;
|
||||
if (--horitzo_ < 0) {
|
||||
horitzo_ = 319;
|
||||
}
|
||||
}
|
||||
|
||||
camello_.tick(delta_ms);
|
||||
|
||||
blink_ms_ += delta_ms;
|
||||
if (blink_ms_ >= 2000) {
|
||||
blink_ms_ %= 2000;
|
||||
}
|
||||
|
||||
render();
|
||||
|
||||
// Qualsevol tecla tanca el menú. Llegim 'P' explícitament abans
|
||||
// de reiniciar el flag de input perquè `Info::ctx.pepe_activat`
|
||||
// reflecteixca si l'usuari estava polsant P al moment d'eixir.
|
||||
if (Ji::anyKey() || Ji::keyPressed(SDL_SCANCODE_P)) {
|
||||
Info::ctx.pepe_activat = Ji::keyPressed(SDL_SCANCODE_P);
|
||||
Ji::disableKeyboard(60);
|
||||
Info::ctx.num_piramide = 1;
|
||||
fade_.startFadeOut();
|
||||
phase_ = Phase::FADING_OUT;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Phase::FADING_OUT:
|
||||
fade_.tick(delta_ms);
|
||||
if (fade_.done()) {
|
||||
phase_ = Phase::DONE;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Scenes
|
||||
@@ -1,11 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "scenes/frame_animator.hpp"
|
||||
#include "scenes/palette_fade.hpp"
|
||||
#include "scenes/scene.hpp"
|
||||
#include "scenes/surface_handle.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
namespace scenes {
|
||||
#include "game/scenes/frame_animator.hpp"
|
||||
#include "game/scenes/palette_fade.hpp"
|
||||
#include "game/scenes/scene.hpp"
|
||||
#include "game/scenes/surface_handle.hpp"
|
||||
|
||||
namespace Scenes {
|
||||
|
||||
// Menú del títol. Reemplaça `ModuleSequence::doMenu()`.
|
||||
//
|
||||
@@ -18,7 +20,7 @@ namespace scenes {
|
||||
// i el text "polsa tecla" parpallejant cada 2 s (visible 1.4 s,
|
||||
// amagat 0.6 s, igual que el `contador % 100 > 30` original).
|
||||
// 4. Quan l'usuari polsa qualsevol tecla — o 'P' per a activar Pepe —
|
||||
// llegim `info::ctx.pepe_activat`, disparem fade-out i marquem
|
||||
// llegim `Info::ctx.pepe_activat`, disparem fade-out i marquem
|
||||
// num_piramide=1 (vas a doSlides).
|
||||
//
|
||||
// Registrat al SceneRegistry amb state_key = 0.
|
||||
@@ -26,13 +28,13 @@ namespace scenes {
|
||||
public:
|
||||
void onEnter() override;
|
||||
void tick(int delta_ms) override;
|
||||
bool done() const override { return phase_ == Phase::Done; }
|
||||
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::DONE; }
|
||||
|
||||
private:
|
||||
enum class Phase { FadingIn,
|
||||
Showing,
|
||||
FadingOut,
|
||||
Done };
|
||||
enum class Phase : std::uint8_t { FADING_IN,
|
||||
SHOWING,
|
||||
FADING_OUT,
|
||||
DONE };
|
||||
|
||||
void render();
|
||||
|
||||
@@ -41,7 +43,7 @@ namespace scenes {
|
||||
PaletteFade fade_;
|
||||
FrameAnimator camello_{4, 160, true};
|
||||
|
||||
Phase phase_{Phase::FadingIn};
|
||||
Phase phase_{Phase::FADING_IN};
|
||||
|
||||
// Scrollers horizontals. Mouen 1 pixel per pas.
|
||||
int palmeres_{0};
|
||||
@@ -53,4 +55,4 @@ namespace scenes {
|
||||
int blink_ms_{0};
|
||||
};
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -1,42 +1,44 @@
|
||||
#include "scenes/mort_scene.hpp"
|
||||
#include "game/scenes/mort_scene.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
#include "core/jail/jinput.hpp"
|
||||
#include "game/info.hpp"
|
||||
#include "scenes/scene_utils.hpp"
|
||||
#include "game/scenes/scene_utils.hpp"
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
void MortScene::onEnter() {
|
||||
playMusic("music/mort.ogg");
|
||||
JI_DisableKeyboard(60);
|
||||
info::ctx.vida = 5;
|
||||
Ji::disableKeyboard(60);
|
||||
Info::ctx.vida = 5;
|
||||
|
||||
gfx_ = SurfaceHandle("gfx/gameover.gif");
|
||||
JD8_ClearScreen(0);
|
||||
JD8_Blit(gfx_);
|
||||
Jd8::clearScreen(0);
|
||||
Jd8::blit(gfx_);
|
||||
|
||||
// PaletteFade en fa una còpia interna via memcpy, així que alliberem
|
||||
// la paleta temporal immediatament.
|
||||
JD8_Palette pal = JD8_LoadPalette("gfx/gameover.gif");
|
||||
Jd8::Palette pal = Jd8::loadPalette("gfx/gameover.gif");
|
||||
fade_.startFadeTo(pal);
|
||||
delete[] pal;
|
||||
|
||||
phase_ = Phase::FadingIn;
|
||||
phase_ = Phase::FADING_IN;
|
||||
remaining_ms_ = 10000;
|
||||
}
|
||||
|
||||
void MortScene::tick(int delta_ms) {
|
||||
switch (phase_) {
|
||||
case Phase::FadingIn:
|
||||
case Phase::FADING_IN:
|
||||
fade_.tick(delta_ms);
|
||||
if (fade_.done()) phase_ = Phase::Showing;
|
||||
if (fade_.done()) {
|
||||
phase_ = Phase::SHOWING;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::Showing:
|
||||
if (JI_AnyKey()) {
|
||||
case Phase::SHOWING:
|
||||
if (Ji::anyKey()) {
|
||||
remaining_ms_ = 0;
|
||||
} else {
|
||||
remaining_ms_ -= delta_ms;
|
||||
@@ -45,20 +47,22 @@ namespace scenes {
|
||||
// Arrenca música del següent mòdul abans del fade out,
|
||||
// igual que la versió vella feia al final de doMort().
|
||||
playMusic("music/menu.ogg");
|
||||
info::ctx.num_piramide = 0;
|
||||
Info::ctx.num_piramide = 0;
|
||||
fade_.startFadeOut();
|
||||
phase_ = Phase::FadingOut;
|
||||
phase_ = Phase::FADING_OUT;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::FadingOut:
|
||||
case Phase::FADING_OUT:
|
||||
fade_.tick(delta_ms);
|
||||
if (fade_.done()) phase_ = Phase::Done;
|
||||
if (fade_.done()) {
|
||||
phase_ = Phase::DONE;
|
||||
}
|
||||
break;
|
||||
|
||||
case Phase::Done:
|
||||
case Phase::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -1,10 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "scenes/palette_fade.hpp"
|
||||
#include "scenes/scene.hpp"
|
||||
#include "scenes/surface_handle.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
namespace scenes {
|
||||
#include "game/scenes/palette_fade.hpp"
|
||||
#include "game/scenes/scene.hpp"
|
||||
#include "game/scenes/surface_handle.hpp"
|
||||
|
||||
namespace Scenes {
|
||||
|
||||
// Pantalla de "game over". Reemplaça `ModuleSequence::doMort()`.
|
||||
//
|
||||
@@ -18,18 +20,18 @@ namespace scenes {
|
||||
public:
|
||||
void onEnter() override;
|
||||
void tick(int delta_ms) override;
|
||||
bool done() const override { return phase_ == Phase::Done; }
|
||||
[[nodiscard]] auto done() const -> bool override { return phase_ == Phase::DONE; }
|
||||
|
||||
private:
|
||||
enum class Phase { FadingIn,
|
||||
Showing,
|
||||
FadingOut,
|
||||
Done };
|
||||
enum class Phase : std::uint8_t { FADING_IN,
|
||||
SHOWING,
|
||||
FADING_OUT,
|
||||
DONE };
|
||||
|
||||
SurfaceHandle gfx_;
|
||||
PaletteFade fade_;
|
||||
Phase phase_{Phase::FadingIn};
|
||||
Phase phase_{Phase::FADING_IN};
|
||||
int remaining_ms_{10000}; // 1000 ticks × 10 ms/tick del doMort original
|
||||
};
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -1,28 +1,30 @@
|
||||
#include "scenes/palette_fade.hpp"
|
||||
#include "game/scenes/palette_fade.hpp"
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
void PaletteFade::startFadeOut() {
|
||||
JD8_FadeStartOut();
|
||||
Jd8::fadeStartOut();
|
||||
active_ = true;
|
||||
}
|
||||
|
||||
void PaletteFade::startFadeTo(JD8_Palette target) {
|
||||
JD8_FadeStartToPal(target);
|
||||
void PaletteFade::startFadeTo(const Color* target) {
|
||||
Jd8::fadeStartToPal(target);
|
||||
active_ = true;
|
||||
}
|
||||
|
||||
void PaletteFade::tick(int /*delta_ms*/) {
|
||||
if (!active_) return;
|
||||
if (!active_) {
|
||||
return;
|
||||
}
|
||||
// El fade té 32 passos interns. Amb un tick per frame (~16ms)
|
||||
// dura ~512ms — el mateix temps que la versió bloquejant original.
|
||||
// Si en el futur volem fer-lo genuinament time-based (p.ex. "fade
|
||||
// de 500ms exactes independent del framerate") podem convertir la
|
||||
// màquina d'estats de jdraw8 a time-based ací sense tocar cap altre
|
||||
// call site.
|
||||
if (JD8_FadeTickStep()) {
|
||||
if (Jd8::fadeTickStep()) {
|
||||
active_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
#include "core/jail/jdraw8.hpp"
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
// Embolcall fi damunt de la màquina d'estats de fade de jdraw8
|
||||
// (`JD8_FadeStart*` / `JD8_FadeTickStep`). Exposa una API time-based
|
||||
// (`JD8_FadeStart*` / `Jd8::fadeTickStep`). Exposa una API time-based
|
||||
// però internament avança un pas del fade per cada crida a `tick()`.
|
||||
// La raó de tindre-ho com a classe a banda: que una escena no puga
|
||||
// cridar accidentalment a `JD8_FadeOut`/`JD8_FadeToPal` (els shims
|
||||
@@ -16,15 +16,15 @@ namespace scenes {
|
||||
PaletteFade() = default;
|
||||
|
||||
void startFadeOut();
|
||||
void startFadeTo(JD8_Palette target);
|
||||
void startFadeTo(const Color* target);
|
||||
|
||||
void tick(int delta_ms);
|
||||
|
||||
bool active() const { return active_; }
|
||||
bool done() const { return !active_; }
|
||||
[[nodiscard]] auto active() const -> bool { return active_; }
|
||||
[[nodiscard]] auto done() const -> bool { return !active_; }
|
||||
|
||||
private:
|
||||
bool active_{false};
|
||||
};
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -6,7 +6,7 @@
|
||||
// cert, i llavors consulta `nextState()` per decidir la següent.
|
||||
//
|
||||
// Contracte:
|
||||
// - `tick(delta_ms)` no pot bloquejar ni cridar JD8_Flip — el caller
|
||||
// - `tick(delta_ms)` no pot bloquejar ni cridar Jd8::flip — el caller
|
||||
// s'encarrega de fer el flip després del tick.
|
||||
// - `done()` es consulta just després de cada tick.
|
||||
// - Els assets són propietat de l'escena (normalment via SurfaceHandle)
|
||||
@@ -14,7 +14,7 @@
|
||||
// - `onEnter()` es crida una vegada just abans del primer tick. És el
|
||||
// moment bo per a arrancar música, disparar un fade-in, etc.
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
class Scene {
|
||||
public:
|
||||
@@ -24,14 +24,14 @@ namespace scenes {
|
||||
|
||||
virtual void tick(int delta_ms) = 0;
|
||||
|
||||
virtual bool done() const = 0;
|
||||
[[nodiscard]] virtual auto done() const -> bool = 0;
|
||||
|
||||
// Valor retornat al caller quan l'escena acaba — equivalent al int
|
||||
// que retornaven les velles funcions `Go()` de ModuleSequence:
|
||||
// 1 = continuar amb la següent escena segons info::ctx
|
||||
// 1 = continuar amb la següent escena segons Info::ctx
|
||||
// 0 = entrar al gameplay (ModuleGame)
|
||||
// -1 = eixir del joc
|
||||
virtual int nextState() const { return 1; }
|
||||
[[nodiscard]] virtual auto nextState() const -> int { return 1; }
|
||||
};
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -0,0 +1,22 @@
|
||||
#include "game/scenes/scene_registry.hpp"
|
||||
|
||||
namespace Scenes {
|
||||
|
||||
auto SceneRegistry::instance() -> SceneRegistry& {
|
||||
static SceneRegistry instance_;
|
||||
return instance_;
|
||||
}
|
||||
|
||||
void SceneRegistry::registerScene(int state_key, Factory factory) {
|
||||
factories_[state_key] = std::move(factory);
|
||||
}
|
||||
|
||||
auto SceneRegistry::tryCreate(int state_key) const -> std::unique_ptr<Scene> {
|
||||
const auto IT = factories_.find(state_key);
|
||||
if (IT == factories_.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return IT->second();
|
||||
}
|
||||
|
||||
} // namespace Scenes
|
||||
@@ -4,11 +4,11 @@
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "scenes/scene.hpp"
|
||||
#include "game/scenes/scene.hpp"
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
// Mapa de `state_key` (actualment = `info::ctx.num_piramide`) a factory
|
||||
// Mapa de `state_key` (actualment = `Info::ctx.num_piramide`) a factory
|
||||
// d'escena. Permet que el dispatch de `gameFiberEntry` provi primer una
|
||||
// Scene nova i caiga al vell `ModuleSequence::Go()` si encara no està
|
||||
// migrada.
|
||||
@@ -21,17 +21,17 @@ namespace scenes {
|
||||
public:
|
||||
using Factory = std::function<std::unique_ptr<Scene>()>;
|
||||
|
||||
static SceneRegistry& instance();
|
||||
static auto instance() -> SceneRegistry&;
|
||||
|
||||
void registerScene(int state_key, Factory factory);
|
||||
|
||||
// Retorna `nullptr` si no hi ha cap escena registrada per a aquest
|
||||
// state. El caller hauria de caure al path legacy en aquest cas.
|
||||
std::unique_ptr<Scene> tryCreate(int state_key) const;
|
||||
auto tryCreate(int state_key) const -> std::unique_ptr<Scene>;
|
||||
|
||||
private:
|
||||
SceneRegistry() = default;
|
||||
std::unordered_map<int, Factory> factories_;
|
||||
};
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -1,13 +1,13 @@
|
||||
#include "scenes/scene_utils.hpp"
|
||||
#include "game/scenes/scene_utils.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
namespace {
|
||||
std::string basename(const char* path) {
|
||||
auto basename(const char* path) -> std::string {
|
||||
std::string s = path;
|
||||
auto pos = s.find_last_of("/\\");
|
||||
return pos == std::string::npos ? s : s.substr(pos + 1);
|
||||
@@ -15,8 +15,10 @@ namespace scenes {
|
||||
} // namespace
|
||||
|
||||
void playMusic(const char* filename, int loop) {
|
||||
if (!filename) return;
|
||||
if (filename == nullptr) {
|
||||
return;
|
||||
}
|
||||
Audio::get()->playMusic(basename(filename), loop);
|
||||
}
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
@@ -3,11 +3,11 @@
|
||||
// Helpers compartits per les escenes. Aquest header és petit i creix
|
||||
// quan una abstracció comú apareix en dos o més escenes.
|
||||
|
||||
namespace scenes {
|
||||
namespace Scenes {
|
||||
|
||||
// Carrega un OGG de `data/` i arranca'l com a música de fons. Substituïx
|
||||
// el `play_music()` repetit en tots els doX() del vell modulesequence.
|
||||
// `loop`: -1 = infinit (per defecte), 0 = una sola vegada, N = N+1 passades.
|
||||
void playMusic(const char* filename, int loop = -1);
|
||||
|
||||
} // namespace scenes
|
||||
} // namespace Scenes
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user