Compare commits
85 Commits
cc16908b86
..
v0.8.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 491992a4d7 | |||
| e5b727216c | |||
| f03e337b9a | |||
| 99e99e7e08 | |||
| b93761eb1e | |||
| 4f5421191d | |||
| 71ed9dc24f | |||
| 1a0cc504c4 | |||
| 86775d4642 | |||
| b936f410ce | |||
| ddcd2076a1 | |||
| 9345facaed | |||
| 885caa6bc3 | |||
| a77bbe4420 | |||
| 61a4886e62 | |||
| 164f58c883 | |||
| fbfacb825b | |||
| 5e4d2cf993 | |||
| 97d3749269 | |||
| 0dcecf9a3c | |||
| c75e6406cd | |||
| 0254b44369 | |||
| ff11567471 | |||
| 06e383fe2c | |||
| dc5b31087a | |||
| 9e745dc3fc | |||
| 14b10c663e | |||
| f64c72f9a6 | |||
| 610eaf257e | |||
| b511740d93 | |||
| b0643b6f62 | |||
| 7e8d79222c | |||
| 14295ce859 | |||
| 5ad433e63a | |||
| 61e40e88f4 | |||
| 410955de3c | |||
| 9c0502eefb | |||
| 9b3da3a6e7 | |||
| bc41169176 | |||
| b3a1afce06 | |||
| 4b6dc8a47a | |||
| 3dadd5fc1a | |||
| bea844d51e | |||
| 5fb6c68df4 | |||
| 866a057704 | |||
| da8eab330d | |||
| 39bda0775e | |||
| ed4d3a3915 | |||
| 6447932212 | |||
| 9f278772bb | |||
| 2d073b6055 | |||
| 99b18d208d | |||
| 1321566910 | |||
| cefafe99e4 | |||
| daa7eaf811 | |||
| 3dcf5c3a99 | |||
| 99d0f62ab5 | |||
| 85050c8da4 | |||
| 120c5502fd | |||
| 64a6599e81 | |||
| a4b567588f | |||
| 2e74fea2d5 | |||
| c4933875dd | |||
| 10a54aef91 | |||
| 34be79192c | |||
| fcf13591be | |||
| 3e8f2f35bf | |||
| e5a91825b1 | |||
| b3271b17a2 | |||
| d4117e3505 | |||
| 73c7e4ea76 | |||
| 23cc5ce68d | |||
| e42059e486 | |||
| 00f40d194b | |||
| 31f348328e | |||
| 8c48a9a772 | |||
| bacfbe6eac | |||
| 63d08aef46 | |||
| 87f818ef96 | |||
| 7eafe21623 | |||
| 22827c28fa | |||
| 8c21345f14 | |||
| 56d7d4af52 | |||
| 71c43ec6fe | |||
| 443b461974 |
@@ -9,6 +9,9 @@ Checks:
|
|||||||
- -bugprone-easily-swappable-parameters
|
- -bugprone-easily-swappable-parameters
|
||||||
- -bugprone-narrowing-conversions
|
- -bugprone-narrowing-conversions
|
||||||
- -modernize-avoid-c-arrays
|
- -modernize-avoid-c-arrays
|
||||||
|
# No forçar reemplaç de bucles "normals" per std::any_of/std::all_of.
|
||||||
|
# Equivalent a `--suppress=useStlAlgorithm` que ja tenim a cppcheck.
|
||||||
|
- -readability-use-anyofallof
|
||||||
# performance-noexcept-move-constructor crashea clang-tidy (LLVM 19.1)
|
# performance-noexcept-move-constructor crashea clang-tidy (LLVM 19.1)
|
||||||
# con recursión infinita en ExceptionSpecAnalyzer::analyzeRecord cuando
|
# con recursión infinita en ExceptionSpecAnalyzer::analyzeRecord cuando
|
||||||
# analiza ciertas instanciaciones de std::set. No es un falso positivo
|
# analiza ciertas instanciaciones de std::set. No es un falso positivo
|
||||||
|
|||||||
+5
-2
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
project(orni VERSION 0.7.2 LANGUAGES CXX)
|
project(orni VERSION 0.8.1 LANGUAGES CXX)
|
||||||
|
|
||||||
# Info del projecte (font de veritat per a project.h)
|
# Info del projecte (font de veritat per a project.h)
|
||||||
set(PROJECT_LONG_NAME "Orni Attack")
|
set(PROJECT_LONG_NAME "Orni Attack")
|
||||||
@@ -110,7 +110,10 @@ add_executable(pack_resources EXCLUDE_FROM_ALL
|
|||||||
tools/pack_resources/pack_resources.cpp
|
tools/pack_resources/pack_resources.cpp
|
||||||
source/core/resources/resource_pack.cpp
|
source/core/resources/resource_pack.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source")
|
target_include_directories(pack_resources PRIVATE
|
||||||
|
"${CMAKE_SOURCE_DIR}/source"
|
||||||
|
"${CMAKE_BINARY_DIR}"
|
||||||
|
)
|
||||||
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
|
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
|
|
||||||
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
|
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
|
||||||
|
|||||||
@@ -84,9 +84,18 @@ else
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: all debug release _windows-release _macos-release _linux-release \
|
.PHONY: all debug release _windows-release _macos-release _linux-release \
|
||||||
run run-debug clean rebuild show-version pack \
|
run run-debug clean rebuild show-version pack controllerdb \
|
||||||
format format-check tidy tidy-fix cppcheck hooks-install help
|
format format-check tidy tidy-fix cppcheck hooks-install help
|
||||||
|
|
||||||
|
# Còpia del gamecontrollerdb.txt (si existeix) al directori de build, perquè
|
||||||
|
# director.cpp el resolgui via resource_base = directori de l'executable.
|
||||||
|
# Silenciós si el fitxer no existeix (l'usuari encara no ha fet `make controllerdb`).
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
CP_CONTROLLERDB = @powershell -Command "if (Test-Path 'gamecontrollerdb.txt') { Copy-Item 'gamecontrollerdb.txt' -Destination '$(BUILDDIR)' -Force }"
|
||||||
|
else
|
||||||
|
CP_CONTROLLERDB = @if [ -f gamecontrollerdb.txt ]; then cp gamecontrollerdb.txt $(BUILDDIR)/; fi
|
||||||
|
endif
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# COMPILACIÓ
|
# COMPILACIÓ
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
@@ -98,10 +107,12 @@ endif
|
|||||||
all:
|
all:
|
||||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Release $(CMAKE_DEFS)
|
||||||
@cmake --build $(BUILDDIR) -j$(JOBS)
|
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||||
|
$(CP_CONTROLLERDB)
|
||||||
|
|
||||||
debug:
|
debug:
|
||||||
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
|
@cmake -S . -B $(BUILDDIR) $(CMAKE_GEN) -DCMAKE_BUILD_TYPE=Debug $(CMAKE_DEFS)
|
||||||
@cmake --build $(BUILDDIR) -j$(JOBS)
|
@cmake --build $(BUILDDIR) -j$(JOBS)
|
||||||
|
$(CP_CONTROLLERDB)
|
||||||
|
|
||||||
run: all
|
run: all
|
||||||
@./$(BUILDDIR)/$(PROJECT)
|
@./$(BUILDDIR)/$(PROJECT)
|
||||||
@@ -138,6 +149,7 @@ _linux-release:
|
|||||||
|
|
||||||
# Còpia de fitxers
|
# Còpia de fitxers
|
||||||
cp $(BUILDDIR)/resources.pack "$(RELEASE_FOLDER)"
|
cp $(BUILDDIR)/resources.pack "$(RELEASE_FOLDER)"
|
||||||
|
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)"
|
||||||
cp README.md "$(RELEASE_FOLDER)"
|
cp README.md "$(RELEASE_FOLDER)"
|
||||||
@[ -f LICENSE ] && cp LICENSE "$(RELEASE_FOLDER)" || true
|
@[ -f LICENSE ] && cp LICENSE "$(RELEASE_FOLDER)" || true
|
||||||
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
|
cp "$(TARGET_FILE)" "$(RELEASE_FILE)"
|
||||||
@@ -166,6 +178,7 @@ _windows-release:
|
|||||||
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
|
@powershell -Command "if (-not (Test-Path '$(RELEASE_FOLDER)')) {New-Item '$(RELEASE_FOLDER)' -ItemType Directory}"
|
||||||
|
|
||||||
@powershell -Command "Copy-Item -Path '$(BUILDDIR)/resources.pack' -Destination '$(RELEASE_FOLDER)'"
|
@powershell -Command "Copy-Item -Path '$(BUILDDIR)/resources.pack' -Destination '$(RELEASE_FOLDER)'"
|
||||||
|
@powershell -Command "Copy-Item 'gamecontrollerdb.txt' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)' }"
|
@powershell -Command "if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' -Destination '$(RELEASE_FOLDER)' }"
|
||||||
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
|
@powershell -Command "Copy-Item 'README.md' -Destination '$(RELEASE_FOLDER)'"
|
||||||
@powershell -Command "if (Test-Path 'release\windows\dll') { Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)' }"
|
@powershell -Command "if (Test-Path 'release\windows\dll') { Copy-Item 'release\windows\dll\*.dll' -Destination '$(RELEASE_FOLDER)' }"
|
||||||
@@ -206,6 +219,7 @@ _macos-release:
|
|||||||
|
|
||||||
# Còpia de recursos i metadades del bundle
|
# Còpia de recursos i metadades del bundle
|
||||||
cp $(BUILDDIR)/arm/resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
cp $(BUILDDIR)/arm/resources.pack "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
|
cp gamecontrollerdb.txt "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
|
cp -R release/macos/frameworks/SDL3.xcframework/macos-arm64_x86_64/SDL3.framework "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Frameworks"
|
||||||
cp release/icons/icon.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
cp release/icons/icon.icns "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents/Resources"
|
||||||
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
|
cp release/macos/Info.plist "$(RELEASE_FOLDER)/$(APP_NAME).app/Contents"
|
||||||
@@ -274,6 +288,19 @@ pack:
|
|||||||
@cmake --build $(BUILDDIR) --target pack_resources
|
@cmake --build $(BUILDDIR) --target pack_resources
|
||||||
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
|
@./$(BUILDDIR)/pack_resources data $(BUILDDIR)/resources.pack
|
||||||
|
|
||||||
|
# ==============================================================================
|
||||||
|
# DESCÀRREGA DE GAMECONTROLLERDB
|
||||||
|
# ==============================================================================
|
||||||
|
# Descarrega l'última versió de gamecontrollerdb.txt (mappings de gamepads
|
||||||
|
# mantinguts per la comunitat) a l'arrel del projecte. SDL el carrega via
|
||||||
|
# filesystem real (no dins resources.pack) i s'ha de copiar al costat del binari
|
||||||
|
# en cada build (gestionat per CP_CONTROLLERDB a `all`/`debug` i pels release targets).
|
||||||
|
controllerdb:
|
||||||
|
@echo "Descargando gamecontrollerdb.txt..."
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt \
|
||||||
|
-o gamecontrollerdb.txt
|
||||||
|
@echo "gamecontrollerdb.txt actualizado"
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# CODE QUALITY (delegats a cmake)
|
# CODE QUALITY (delegats a cmake)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: bullet
|
||||||
|
|
||||||
|
# Shape de la bala. El bounding_radius del .shp dóna el hitbox base (~3 px);
|
||||||
|
# scale el modula visualment i pel hitbox.
|
||||||
|
shape:
|
||||||
|
path: bullet/basic.shp
|
||||||
|
scale: 1.0
|
||||||
|
collision_factor: 1.0
|
||||||
|
|
||||||
|
# Cinemàtica pura: la bala no col·lisiona físicament al PhysicsWorld
|
||||||
|
# (body_.radius = 0 al spawn), però sí participa al gameplay via
|
||||||
|
# checkCollisionSwept. La mass i l'impact_momentum_factor es fan servir
|
||||||
|
# només per calcular l'impuls que rep l'enemic en impactar.
|
||||||
|
physics:
|
||||||
|
mass: 0.5
|
||||||
|
restitution: 0.0 # irrelevant (no rebota)
|
||||||
|
linear_damping: 0.0 # movement rectilini uniforme
|
||||||
|
angular_damping: 0.0
|
||||||
|
impact_momentum_factor: 3.0 # factor de transferència de moment bala→enemic
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [155, 255, 175] # verd laser
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
name: bullet_double
|
||||||
|
|
||||||
|
# Variant de bala "anular" (dos cercles concèntrics, aspecte d'aura de plasma).
|
||||||
|
# Pensada per a contra-atacs d'enemic (ex: orb dispara una bullet_double al
|
||||||
|
# jugador quan rep un impacte). Mateixa física que la bala bàsica del player;
|
||||||
|
# canvien la forma (cercle doble) i el color per llegir-se com a tret enemic
|
||||||
|
# distintiu (groc verdós vs. el verd laser del player o el roig de bullet_long).
|
||||||
|
shape:
|
||||||
|
path: bullet/double.shp
|
||||||
|
scale: 1.5
|
||||||
|
collision_factor: 1.0
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 0.5
|
||||||
|
restitution: 0.0
|
||||||
|
linear_damping: 0.0
|
||||||
|
angular_damping: 0.0
|
||||||
|
impact_momentum_factor: 4.0
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [200, 255, 80] # groc verdós (chartreuse) — contra-atac de l'orb
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
name: bullet_long
|
||||||
|
|
||||||
|
# Variant de bala més llarga, pensada per a bales d'enemic: més visible per al
|
||||||
|
# jugador i amb prou marge per reaccionar. La velocitat NO viu aquí: es passa
|
||||||
|
# a Bullet::fire() i la decideix qui dispara (l'AiTickAction).
|
||||||
|
shape:
|
||||||
|
path: bullet/long.shp
|
||||||
|
scale: 1.0
|
||||||
|
collision_factor: 0.5
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 0.5
|
||||||
|
restitution: 0.0
|
||||||
|
linear_damping: 0.0
|
||||||
|
angular_damping: 0.0
|
||||||
|
impact_momentum_factor: 3.0
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [255, 100, 100] # roig clar — diferencia visualment del verd laser del player
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
name: orb
|
||||||
|
ai_type: orb # Validat contra el directori; mapeja a EnemyType::ORB.
|
||||||
|
|
||||||
|
# Shape circular pròpia (anell exterior + anell interior + 6 radis + nucli),
|
||||||
|
# pensada per llegir-se com a "reactor / orb" amb més detall que els enemics
|
||||||
|
# petits.
|
||||||
|
shape:
|
||||||
|
path: enemy/orb.shp
|
||||||
|
scale: 1.0
|
||||||
|
collision_factor: 1.0
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 50.0 # Molt pesat: una bala el frena un poc però no el "envia a passejar".
|
||||||
|
speed: 50.0 # Avança decidit cap al ship (no és lent passiu, és amenaça constant).
|
||||||
|
rotation_delta_min: 0.3
|
||||||
|
rotation_delta_max: 1.5
|
||||||
|
restitution: 1.0
|
||||||
|
linear_damping: 0.0
|
||||||
|
angular_damping: 0.0
|
||||||
|
|
||||||
|
ai:
|
||||||
|
# Persecució contínua del ship més proper. chase_strength alt (1.0 = ~1s
|
||||||
|
# per realinear-se) perquè, encara que una bala l'empentja lateralment,
|
||||||
|
# ràpidament torna a posar la seua proa cap al jugador.
|
||||||
|
movement:
|
||||||
|
type: chase
|
||||||
|
chase_strength: 1.0
|
||||||
|
|
||||||
|
animation:
|
||||||
|
pulse:
|
||||||
|
trigger_prob_per_second: 0.01
|
||||||
|
duration_min: 1.0
|
||||||
|
duration_max: 3.0
|
||||||
|
amplitude_min: 0.08
|
||||||
|
amplitude_max: 0.20
|
||||||
|
frequency_min: 1.5
|
||||||
|
frequency_max: 3.0
|
||||||
|
rotation_accel:
|
||||||
|
trigger_prob_per_second: 0.02
|
||||||
|
duration_min: 3.0
|
||||||
|
duration_max: 8.0
|
||||||
|
multiplier_min: 0.3
|
||||||
|
multiplier_max: 4.0
|
||||||
|
|
||||||
|
wounded:
|
||||||
|
duration: 1.5 # Una mica més llarg que els altres (és un boss).
|
||||||
|
blink_hz: 10.0
|
||||||
|
|
||||||
|
spawn:
|
||||||
|
invulnerability_duration: 3.0
|
||||||
|
invulnerability_brightness_start: 0.3
|
||||||
|
invulnerability_brightness_end: 0.7
|
||||||
|
invulnerability_scale_start: 0.0
|
||||||
|
invulnerability_scale_end: 1.0
|
||||||
|
safety_distance: 54.0 # 1.5× del normal (alineat amb scale 1.5).
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [255, 140, 110] # taronja rosat (coral) — distintiu del boss orb.
|
||||||
|
wounded: [255, 220, 60]
|
||||||
|
|
||||||
|
score: 500 # 5x un enemic normal: aguanta 10x més.
|
||||||
|
|
||||||
|
# Estrenant el sistema HP: 10 unitats. Cada bala fa decrease_health + flash
|
||||||
|
# + create_debris_partial (xip a 0.3x) + create_fireworks_small (espurna).
|
||||||
|
# Al 10è hit (HP=0), on_no_health encadena destroy directe — sense passar
|
||||||
|
# per wounded (com Star). 10 HP ja és prou dificultat sense afegir un hit
|
||||||
|
# extra.
|
||||||
|
health: 10
|
||||||
|
|
||||||
|
events:
|
||||||
|
on_hit:
|
||||||
|
- action: fire_bullet # contra-atac: dispara bullet_double dirigida al jugador
|
||||||
|
bullet: bullet_double
|
||||||
|
bullet_speed: 200.0
|
||||||
|
aim_mode: aimed
|
||||||
|
- action: decrease_health # primer: si arriba a 0 dispara on_no_health
|
||||||
|
#- action: flash # feedback visual de damage parcial
|
||||||
|
- action: create_debris_partial # xip a 0.3x mida (sense ser letal)
|
||||||
|
#- action: create_fireworks_small # espurna a cada hit (12 punts, lent)
|
||||||
|
- action: apply_impulse # empenta el cos (skip si will_die)
|
||||||
|
on_no_health:
|
||||||
|
- action: destroy # mort directa, sense wounded
|
||||||
|
on_destroy:
|
||||||
|
- action: add_score
|
||||||
|
- action: create_debris # explosió completa
|
||||||
|
- action: create_fireworks
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
name: pentagon
|
||||||
|
ai_type: pentagon # Validat contra el directori; mapeja a EnemyType::PENTAGON.
|
||||||
|
|
||||||
|
shape:
|
||||||
|
path: enemy/pentagon.shp
|
||||||
|
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||||
|
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 5.0
|
||||||
|
speed: 35.0 # px/s (esquivador lent)
|
||||||
|
rotation_delta_min: 0.75 # rad/s — rotació visual mínima
|
||||||
|
rotation_delta_max: 3.75 # rad/s — rotació visual màxima
|
||||||
|
restitution: 1.0 # rebot elàstic perfecte contra parets
|
||||||
|
linear_damping: 0.0 # manté velocitat (sense fricció)
|
||||||
|
angular_damping: 0.0
|
||||||
|
|
||||||
|
behavior:
|
||||||
|
# Pentagon: zigzag esquivador (canvi de direcció probabilístic per segon).
|
||||||
|
angle_change_max: 1.0 # rad — magnitud del canvi de direcció
|
||||||
|
zigzag_prob_per_second: 0.8
|
||||||
|
|
||||||
|
animation:
|
||||||
|
pulse: # respiració d'escala aleatòria
|
||||||
|
trigger_prob_per_second: 0.01
|
||||||
|
duration_min: 1.0
|
||||||
|
duration_max: 3.0
|
||||||
|
amplitude_min: 0.08
|
||||||
|
amplitude_max: 0.20
|
||||||
|
frequency_min: 1.5
|
||||||
|
frequency_max: 3.0
|
||||||
|
rotation_accel: # acceleració/desacceleració de rotació visual
|
||||||
|
trigger_prob_per_second: 0.02
|
||||||
|
duration_min: 3.0
|
||||||
|
duration_max: 8.0
|
||||||
|
multiplier_min: 0.3
|
||||||
|
multiplier_max: 4.0
|
||||||
|
|
||||||
|
wounded:
|
||||||
|
duration: 1.0 # segons en estat ferit abans d'explotar
|
||||||
|
blink_hz: 10.0 # parpelleig color normal ↔ wounded
|
||||||
|
|
||||||
|
spawn:
|
||||||
|
invulnerability_duration: 3.0
|
||||||
|
invulnerability_brightness_start: 0.3
|
||||||
|
invulnerability_brightness_end: 0.7
|
||||||
|
invulnerability_scale_start: 0.0
|
||||||
|
invulnerability_scale_end: 1.0
|
||||||
|
safety_distance: 36.0 # px mínim respecte al player al spawn
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [0, 255, 255] # Cyan pur "esquivador"
|
||||||
|
wounded: [255, 220, 60] # Daurat (parpelleig al rebre impacte)
|
||||||
|
|
||||||
|
score: 100
|
||||||
|
|
||||||
|
events:
|
||||||
|
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
|
||||||
|
# decrease_health primer perquè si la mort cau aquí (segon hit durant wounded),
|
||||||
|
# el dispatcher salta la resta del chain (incloent apply_impulse) sobre el
|
||||||
|
# cos ja destruït.
|
||||||
|
on_hit:
|
||||||
|
- action: decrease_health
|
||||||
|
- action: apply_impulse
|
||||||
|
on_no_health:
|
||||||
|
- action: set_hurt
|
||||||
|
on_hurt_end:
|
||||||
|
- action: destroy
|
||||||
|
on_destroy:
|
||||||
|
- action: add_score
|
||||||
|
- action: create_debris
|
||||||
|
- action: create_fireworks
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
name: pinwheel
|
||||||
|
ai_type: pinwheel # Validat contra el directori; mapeja a EnemyType::PINWHEEL.
|
||||||
|
|
||||||
|
shape:
|
||||||
|
path: enemy/pinwheel.shp
|
||||||
|
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||||
|
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 4.0 # Més lleuger — àgil
|
||||||
|
speed: 50.0 # px/s (el més ràpid)
|
||||||
|
rotation_delta_min: 3.0 # rad/s — rotació base elevada
|
||||||
|
rotation_delta_max: 6.0
|
||||||
|
restitution: 1.0
|
||||||
|
linear_damping: 0.0
|
||||||
|
angular_damping: 0.0
|
||||||
|
|
||||||
|
behavior:
|
||||||
|
# Pinwheel: movement rectilíniauniforme + boost de rotació visual prop de la nau.
|
||||||
|
rotation_proximity_multiplier: 3.0 # Multiplicador de rotació quan és prop de la nau
|
||||||
|
proximity_distance: 100.0 # Llindar de distància (px)
|
||||||
|
|
||||||
|
animation:
|
||||||
|
pulse:
|
||||||
|
trigger_prob_per_second: 0.01
|
||||||
|
duration_min: 1.0
|
||||||
|
duration_max: 3.0
|
||||||
|
amplitude_min: 0.08
|
||||||
|
amplitude_max: 0.20
|
||||||
|
frequency_min: 1.5
|
||||||
|
frequency_max: 3.0
|
||||||
|
rotation_accel:
|
||||||
|
trigger_prob_per_second: 0.02
|
||||||
|
duration_min: 3.0
|
||||||
|
duration_max: 8.0
|
||||||
|
multiplier_min: 0.3
|
||||||
|
multiplier_max: 4.0
|
||||||
|
|
||||||
|
wounded:
|
||||||
|
duration: 1.0
|
||||||
|
blink_hz: 10.0
|
||||||
|
|
||||||
|
spawn:
|
||||||
|
invulnerability_duration: 3.0
|
||||||
|
invulnerability_brightness_start: 0.3
|
||||||
|
invulnerability_brightness_end: 0.7
|
||||||
|
invulnerability_scale_start: 0.0
|
||||||
|
invulnerability_scale_end: 1.0
|
||||||
|
safety_distance: 36.0
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [255, 0, 255] # Magenta pur "agressiu"
|
||||||
|
wounded: [255, 220, 60]
|
||||||
|
|
||||||
|
score: 200
|
||||||
|
|
||||||
|
events:
|
||||||
|
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
|
||||||
|
on_hit:
|
||||||
|
- action: decrease_health
|
||||||
|
- action: apply_impulse
|
||||||
|
on_no_health:
|
||||||
|
- action: set_hurt
|
||||||
|
on_hurt_end:
|
||||||
|
- action: destroy
|
||||||
|
on_destroy:
|
||||||
|
- action: add_score
|
||||||
|
- action: create_debris
|
||||||
|
- action: create_fireworks
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: player_ship
|
||||||
|
|
||||||
|
# Shape de la nau. Resolt per ShapeLoader (busca a "shapes/<path>").
|
||||||
|
# Nota: el segon jugador rep un override del shape ("ship/wedge.shp") al ctor.
|
||||||
|
# Quan s'introdueixin variants reals de nau, es crearà un YAML separat
|
||||||
|
# per cada model.
|
||||||
|
#
|
||||||
|
# scale: multiplicador visual i de hitbox sobre la mida nativa del .shp (1.0 = mida del fitxer).
|
||||||
|
# collision_factor: ajust opcional del hitbox respecte el cercle circumscrit
|
||||||
|
# automàtic de la shape; tocar només si el feel del hitbox
|
||||||
|
# no quadra amb la silueta visual (default 1.0).
|
||||||
|
shape:
|
||||||
|
path: ship/arrow.shp
|
||||||
|
scale: 1.0
|
||||||
|
collision_factor: 1.0
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 10.0
|
||||||
|
restitution: 0.6
|
||||||
|
linear_damping: 1.5
|
||||||
|
angular_damping: 0.0
|
||||||
|
rotation_speed: 3.14 # rad/s (~180 deg/s, input-driven sense inercia)
|
||||||
|
acceleration: 400.0 # px/s^2 multiplicat per la massa quan THRUST
|
||||||
|
max_velocity: 180.0 # px/s (clamp post-integració per preservar feel arcade)
|
||||||
|
# Factor de transferència del moment lineal de la nau a l'enemic en el
|
||||||
|
# frame exacte que mor per col·lisió (afegit per damunt del rebot natural).
|
||||||
|
death_impact_factor: 0.3
|
||||||
|
|
||||||
|
invulnerability:
|
||||||
|
duration: 3.0 # segons d'invulnerabilitat post-respawn
|
||||||
|
blink_visible: 0.1 # segons visible per cicle de parpelleig
|
||||||
|
blink_invisible: 0.1 # segons invisible per cicle de parpelleig
|
||||||
|
|
||||||
|
hurt:
|
||||||
|
duration: 15.0 # segons en estat "ferit" abans de tornar a normal
|
||||||
|
blink_hz: 10.0 # freqüència parpelleig color normal <-> color hurt
|
||||||
|
|
||||||
|
# Empenta visual: la nau s'escala lleugerament amb la velocitat.
|
||||||
|
# Manté la sensació del Pascal original (0..MAX_VEL → 1.0..~1.5).
|
||||||
|
visual_thrust:
|
||||||
|
push_divisor: 33.33
|
||||||
|
scale_divisor: 12.0
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [255, 255, 255] # blanc neutre
|
||||||
|
hurt: [255, 220, 60] # daurat (estat ferit)
|
||||||
|
|
||||||
|
weapon:
|
||||||
|
bullet_speed: 700.0 # velocitat escalar de la bullet (px/s)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
name: square
|
||||||
|
ai_type: square # Validat contra el directori; mapeja a EnemyType::SQUARE.
|
||||||
|
|
||||||
|
shape:
|
||||||
|
path: enemy/square.shp
|
||||||
|
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||||
|
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 8.0 # Més pesat — "tanc"
|
||||||
|
speed: 40.0 # px/s (velocitat mitjana)
|
||||||
|
rotation_delta_min: 0.3 # rad/s — rotació lenta
|
||||||
|
rotation_delta_max: 1.5
|
||||||
|
restitution: 1.0
|
||||||
|
linear_damping: 0.0
|
||||||
|
angular_damping: 0.0
|
||||||
|
|
||||||
|
ai:
|
||||||
|
# Square: persecució contínua del ship més proper (steering suau, "tanc lent").
|
||||||
|
movement:
|
||||||
|
type: chase
|
||||||
|
chase_strength: 0.5 # Força/segon de la LERP cap a la direcció ideal (1.0 = ~1s per realinear)
|
||||||
|
|
||||||
|
animation:
|
||||||
|
pulse:
|
||||||
|
trigger_prob_per_second: 0.01
|
||||||
|
duration_min: 1.0
|
||||||
|
duration_max: 3.0
|
||||||
|
amplitude_min: 0.08
|
||||||
|
amplitude_max: 0.20
|
||||||
|
frequency_min: 1.5
|
||||||
|
frequency_max: 3.0
|
||||||
|
rotation_accel:
|
||||||
|
trigger_prob_per_second: 0.02
|
||||||
|
duration_min: 3.0
|
||||||
|
duration_max: 8.0
|
||||||
|
multiplier_min: 0.3
|
||||||
|
multiplier_max: 4.0
|
||||||
|
|
||||||
|
wounded:
|
||||||
|
duration: 1.0
|
||||||
|
blink_hz: 10.0
|
||||||
|
|
||||||
|
spawn:
|
||||||
|
invulnerability_duration: 3.0
|
||||||
|
invulnerability_brightness_start: 0.3
|
||||||
|
invulnerability_brightness_end: 0.7
|
||||||
|
invulnerability_scale_start: 0.0
|
||||||
|
invulnerability_scale_end: 1.0
|
||||||
|
safety_distance: 36.0
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [255, 0, 0] # Roig pur "tanc"
|
||||||
|
wounded: [255, 220, 60]
|
||||||
|
|
||||||
|
score: 150
|
||||||
|
|
||||||
|
events:
|
||||||
|
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
|
||||||
|
on_hit:
|
||||||
|
- action: decrease_health
|
||||||
|
- action: apply_impulse
|
||||||
|
on_no_health:
|
||||||
|
- action: set_hurt
|
||||||
|
on_hurt_end:
|
||||||
|
- action: destroy
|
||||||
|
on_destroy:
|
||||||
|
- action: add_score
|
||||||
|
- action: create_debris
|
||||||
|
- action: create_fireworks
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
name: star
|
||||||
|
ai_type: star # Validat contra el directori; mapeja a EnemyType::STAR.
|
||||||
|
|
||||||
|
shape:
|
||||||
|
path: enemy/star.shp
|
||||||
|
scale: 0.7 # Lleugerament més petit que els altres enemics per diferenciar visualment.
|
||||||
|
collision_factor: 1.0
|
||||||
|
|
||||||
|
physics:
|
||||||
|
mass: 5.0
|
||||||
|
speed: 35.0 # Mateixos paràmetres que pentagon (esquivador lent).
|
||||||
|
rotation_delta_min: 0.75
|
||||||
|
rotation_delta_max: 3.75
|
||||||
|
restitution: 1.0
|
||||||
|
linear_damping: 0.0
|
||||||
|
angular_damping: 0.0
|
||||||
|
|
||||||
|
ai:
|
||||||
|
# Movement: zigzag esquivador (com Pentagon).
|
||||||
|
movement:
|
||||||
|
type: zigzag
|
||||||
|
angle_change_max: 1.0
|
||||||
|
zigzag_prob_per_second: 0.8
|
||||||
|
# Accions periòdiques: cada ~2.5s dispara una bala apuntada al ship més proper.
|
||||||
|
tick:
|
||||||
|
- action: shoot
|
||||||
|
interval: 2.5
|
||||||
|
aim_mode: aimed # apunta al ship més proper (atan2)
|
||||||
|
jitter_rad: 0.0 # sense soroll: tret perfecte
|
||||||
|
bullet: bullet_long # variant més visible per al jugador
|
||||||
|
bullet_speed: 150.0 # px/s — prou lenta per reaccionar
|
||||||
|
|
||||||
|
animation:
|
||||||
|
pulse:
|
||||||
|
trigger_prob_per_second: 0.01
|
||||||
|
duration_min: 1.0
|
||||||
|
duration_max: 3.0
|
||||||
|
amplitude_min: 0.08
|
||||||
|
amplitude_max: 0.20
|
||||||
|
frequency_min: 1.5
|
||||||
|
frequency_max: 3.0
|
||||||
|
rotation_accel:
|
||||||
|
trigger_prob_per_second: 0.02
|
||||||
|
duration_min: 3.0
|
||||||
|
duration_max: 8.0
|
||||||
|
multiplier_min: 0.3
|
||||||
|
multiplier_max: 4.0
|
||||||
|
|
||||||
|
wounded:
|
||||||
|
duration: 1.0
|
||||||
|
blink_hz: 10.0
|
||||||
|
|
||||||
|
spawn:
|
||||||
|
invulnerability_duration: 3.0
|
||||||
|
invulnerability_brightness_start: 0.3
|
||||||
|
invulnerability_brightness_end: 0.7
|
||||||
|
invulnerability_scale_start: 0.0
|
||||||
|
invulnerability_scale_end: 1.0
|
||||||
|
safety_distance: 36.0
|
||||||
|
|
||||||
|
colors:
|
||||||
|
normal: [255, 255, 0] # Groc estrella
|
||||||
|
wounded: [255, 220, 60]
|
||||||
|
|
||||||
|
score: 100
|
||||||
|
|
||||||
|
events:
|
||||||
|
# STAR: mor al primer impacte, sense passar per wounded.
|
||||||
|
# HP=1 (default): decrement → on_no_health → destroy directe (sense wounded).
|
||||||
|
on_hit:
|
||||||
|
- action: decrease_health
|
||||||
|
on_no_health:
|
||||||
|
- action: destroy
|
||||||
|
on_destroy:
|
||||||
|
- action: add_score
|
||||||
|
- action: create_debris
|
||||||
|
- action: create_fireworks
|
||||||
@@ -14,6 +14,8 @@ notification:
|
|||||||
postfx_on: "POSTPROCESSAT ACTIU"
|
postfx_on: "POSTPROCESSAT ACTIU"
|
||||||
postfx_off: "POSTPROCESSAT INACTIU"
|
postfx_off: "POSTPROCESSAT INACTIU"
|
||||||
locale_switched: "IDIOMA: {lang}"
|
locale_switched: "IDIOMA: {lang}"
|
||||||
|
gamepad_connected: "{name} CONNECTAT"
|
||||||
|
gamepad_disconnected: "{name} DESCONNECTAT"
|
||||||
|
|
||||||
language:
|
language:
|
||||||
ca: "CATALA"
|
ca: "CATALA"
|
||||||
@@ -50,6 +52,61 @@ service_menu:
|
|||||||
title: "MENU DE SERVEI"
|
title: "MENU DE SERVEI"
|
||||||
video: "VIDEO"
|
video: "VIDEO"
|
||||||
audio: "AUDIO"
|
audio: "AUDIO"
|
||||||
|
options: "OPCIONS"
|
||||||
|
system: "SISTEMA"
|
||||||
controls: "CONTROLS"
|
controls: "CONTROLS"
|
||||||
back: "ENRERE"
|
back: "ENRERE"
|
||||||
exit: "EIXIR DEL JOC"
|
exit: "EIXIR DEL JOC"
|
||||||
|
# Items del submenu VIDEO
|
||||||
|
video_zoom: "ZOOM"
|
||||||
|
video_fullscreen: "PANTALLA COMPLETA"
|
||||||
|
video_vsync: "VSYNC"
|
||||||
|
video_aa: "ANTIALIAS"
|
||||||
|
video_postfx: "POSTPROCESSAT"
|
||||||
|
video_resolution: "RESOLUCIO"
|
||||||
|
# Items del submenu OPCIONS
|
||||||
|
options_language: "IDIOMA"
|
||||||
|
options_show_info: "MOSTRAR INFO"
|
||||||
|
# Items del submenu AUDIO
|
||||||
|
audio_master: "AUDIO"
|
||||||
|
audio_master_volume: "VOLUM GENERAL"
|
||||||
|
audio_music: "MUSICA"
|
||||||
|
audio_music_volume: "VOLUM MUSICA"
|
||||||
|
audio_sound: "EFECTES"
|
||||||
|
audio_sound_volume: "VOLUM EFECTES"
|
||||||
|
# Items del submenu SISTEMA
|
||||||
|
system_restart: "REINICIAR"
|
||||||
|
# Pagines de confirmacio (estructura: titol + NO/SI)
|
||||||
|
confirm_restart: "ESTAS SEGUR DE REINICIAR?"
|
||||||
|
confirm_exit: "ESTAS SEGUR DE EIXIR?"
|
||||||
|
confirm_no: "NO"
|
||||||
|
confirm_yes: "SI"
|
||||||
|
# Valors comuns
|
||||||
|
value_on: "ACTIU"
|
||||||
|
value_off: "INACTIU"
|
||||||
|
# Items del submenu CONTROLS
|
||||||
|
controls_pad_p1: "MANDO JUGADOR 1"
|
||||||
|
controls_pad_p2: "MANDO JUGADOR 2"
|
||||||
|
controls_no_pad: "SENSE MANDO"
|
||||||
|
controls_define_keyboard_p1: "REDEFINIR TECLES P1"
|
||||||
|
controls_define_keyboard_p2: "REDEFINIR TECLES P2"
|
||||||
|
controls_define_gamepad_p1: "REDEFINIR BOTONS P1"
|
||||||
|
controls_define_gamepad_p2: "REDEFINIR BOTONS P2"
|
||||||
|
|
||||||
|
# Overlay modal de redefinicio (DefineInputs)
|
||||||
|
define:
|
||||||
|
title_keyboard_p1: "REDEFINIR TECLES P1"
|
||||||
|
title_keyboard_p2: "REDEFINIR TECLES P2"
|
||||||
|
title_gamepad_p1: "REDEFINIR BOTONS P1"
|
||||||
|
title_gamepad_p2: "REDEFINIR BOTONS P2"
|
||||||
|
press_key: "PREMEU UNA TECLA"
|
||||||
|
press_button: "PREMEU UN BOTO"
|
||||||
|
complete: "CONFIGURACIO COMPLETA"
|
||||||
|
no_gamepad: "CAP MANDO ASSIGNAT AL JUGADOR"
|
||||||
|
action:
|
||||||
|
left: "ESQUERRA"
|
||||||
|
right: "DRETA"
|
||||||
|
fire: "DISPARAR"
|
||||||
|
accelerate: "ACCELERAR"
|
||||||
|
start: "START"
|
||||||
|
menu: "MENU"
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ notification:
|
|||||||
postfx_on: "POSTPROCESS ON"
|
postfx_on: "POSTPROCESS ON"
|
||||||
postfx_off: "POSTPROCESS OFF"
|
postfx_off: "POSTPROCESS OFF"
|
||||||
locale_switched: "LANGUAGE: {lang}"
|
locale_switched: "LANGUAGE: {lang}"
|
||||||
|
gamepad_connected: "{name} CONNECTED"
|
||||||
|
gamepad_disconnected: "{name} DISCONNECTED"
|
||||||
|
|
||||||
language:
|
language:
|
||||||
ca: "CATALAN"
|
ca: "CATALAN"
|
||||||
@@ -49,6 +51,61 @@ service_menu:
|
|||||||
title: "SERVICE MENU"
|
title: "SERVICE MENU"
|
||||||
video: "VIDEO"
|
video: "VIDEO"
|
||||||
audio: "AUDIO"
|
audio: "AUDIO"
|
||||||
|
options: "OPTIONS"
|
||||||
|
system: "SYSTEM"
|
||||||
controls: "CONTROLS"
|
controls: "CONTROLS"
|
||||||
back: "BACK"
|
back: "BACK"
|
||||||
exit: "EXIT GAME"
|
exit: "EXIT GAME"
|
||||||
|
# Items of VIDEO submenu
|
||||||
|
video_zoom: "ZOOM"
|
||||||
|
video_fullscreen: "FULLSCREEN"
|
||||||
|
video_vsync: "VSYNC"
|
||||||
|
video_aa: "ANTIALIAS"
|
||||||
|
video_postfx: "POSTPROCESS"
|
||||||
|
video_resolution: "RESOLUTION"
|
||||||
|
# Items of OPTIONS submenu
|
||||||
|
options_language: "LANGUAGE"
|
||||||
|
options_show_info: "SHOW INFO"
|
||||||
|
# Items of AUDIO submenu
|
||||||
|
audio_master: "AUDIO"
|
||||||
|
audio_master_volume: "MASTER VOLUME"
|
||||||
|
audio_music: "MUSIC"
|
||||||
|
audio_music_volume: "MUSIC VOLUME"
|
||||||
|
audio_sound: "SOUNDS"
|
||||||
|
audio_sound_volume: "SOUND VOLUME"
|
||||||
|
# Items of SYSTEM submenu
|
||||||
|
system_restart: "RESTART"
|
||||||
|
# Confirmation pages (structure: title + NO/YES)
|
||||||
|
confirm_restart: "REALLY RESTART?"
|
||||||
|
confirm_exit: "REALLY EXIT?"
|
||||||
|
confirm_no: "NO"
|
||||||
|
confirm_yes: "YES"
|
||||||
|
# Common values
|
||||||
|
value_on: "ON"
|
||||||
|
value_off: "OFF"
|
||||||
|
# Items of CONTROLS submenu
|
||||||
|
controls_pad_p1: "PLAYER 1 GAMEPAD"
|
||||||
|
controls_pad_p2: "PLAYER 2 GAMEPAD"
|
||||||
|
controls_no_pad: "NO GAMEPAD"
|
||||||
|
controls_define_keyboard_p1: "REDEFINE KEYS P1"
|
||||||
|
controls_define_keyboard_p2: "REDEFINE KEYS P2"
|
||||||
|
controls_define_gamepad_p1: "REDEFINE BUTTONS P1"
|
||||||
|
controls_define_gamepad_p2: "REDEFINE BUTTONS P2"
|
||||||
|
|
||||||
|
# Modal overlay for input redefinition (DefineInputs)
|
||||||
|
define:
|
||||||
|
title_keyboard_p1: "REDEFINE KEYS P1"
|
||||||
|
title_keyboard_p2: "REDEFINE KEYS P2"
|
||||||
|
title_gamepad_p1: "REDEFINE BUTTONS P1"
|
||||||
|
title_gamepad_p2: "REDEFINE BUTTONS P2"
|
||||||
|
press_key: "PRESS A KEY"
|
||||||
|
press_button: "PRESS A BUTTON"
|
||||||
|
complete: "CONFIGURATION COMPLETE"
|
||||||
|
no_gamepad: "NO GAMEPAD ASSIGNED TO PLAYER"
|
||||||
|
action:
|
||||||
|
left: "LEFT"
|
||||||
|
right: "RIGHT"
|
||||||
|
fire: "FIRE"
|
||||||
|
accelerate: "ACCELERATE"
|
||||||
|
start: "START"
|
||||||
|
menu: "MENU"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# bullet.shp - Projectil (octàgon, radi=3)
|
# bullet/basic.shp - Projectil (octàgon, radi=3)
|
||||||
|
|
||||||
name: bullet
|
name: basic
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# bullet/double.shp - Bala anular (dos cercles concèntrics)
|
||||||
|
# © 2026 JailDesigner
|
||||||
|
#
|
||||||
|
# Dos octàgons concèntrics al centre (0,0):
|
||||||
|
# - Exterior: radi 4 (lleugerament més gran que la bala estàndard, radi 3)
|
||||||
|
# - Interior: radi 2 (lleugerament més petit que la bala estàndard)
|
||||||
|
# Aspecte d'anell / aura de plasma. Bounding radius natiu = 4.
|
||||||
|
|
||||||
|
name: double
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
# Cercle exterior (octàgon, radi 4)
|
||||||
|
polyline: 0,-4 2.83,-2.83 4,0 2.83,2.83 0,4 -2.83,2.83 -4,0 -2.83,-2.83 0,-4
|
||||||
|
|
||||||
|
# Cercle interior (octàgon, radi 2)
|
||||||
|
polyline: 0,-2 1.41,-1.41 2,0 1.41,1.41 0,2 -1.41,1.41 -2,0 -1.41,-1.41 0,-2
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# bullet/long.shp - Bala allargada vertical (dos mig-octàgons + dos costats)
|
||||||
|
# © 2026 JailDesigner
|
||||||
|
#
|
||||||
|
# Càpsula orientada al llarg de l'eix Y: la bala viatja segons el seu angle
|
||||||
|
# de moviment (angle=0 = Y negatiu), i així s'estira en la direcció de vol.
|
||||||
|
# Es dibuixen només els segments exteriors per evitar veure la unió interna
|
||||||
|
# dels dos cercles; el resultat visual són dos "mig-octàgons" separats per
|
||||||
|
# un petit gap al centre, units pels dos costats verticals.
|
||||||
|
#
|
||||||
|
# Geometria:
|
||||||
|
# Mig-octàgon superior (radi 3) centrat a (0, -3)
|
||||||
|
# Mig-octàgon inferior (radi 3) centrat a (0, 3)
|
||||||
|
# Punt extrem superior: (0, -6)
|
||||||
|
# Punt extrem inferior: (0, 6)
|
||||||
|
# Bounding radius natiu = 6 (extrem vertical a y=±6).
|
||||||
|
# collision_factor al YAML compensa el bounding doble (0.5 → hitbox ≈ 3).
|
||||||
|
|
||||||
|
name: long
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
# Mig-octàgon superior (5 vèrtexs: del cantó dret cap al punt extrem i a l'esquerre)
|
||||||
|
polyline: 3,-3 2.12,-5.12 0,-6 -2.12,-5.12 -3,-3
|
||||||
|
|
||||||
|
# Mig-octàgon inferior
|
||||||
|
polyline: 3,3 2.12,5.12 0,6 -2.12,5.12 -3,3
|
||||||
|
|
||||||
|
# Costat dret (uneix extrem inferior del mig superior amb extrem superior del mig inferior)
|
||||||
|
polyline: 3,-3 3,3
|
||||||
|
|
||||||
|
# Costat esquerre
|
||||||
|
polyline: -3,-3 -3,3
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# star.shp - Estrella per a starfield
|
# effect/starfield.shp - Estrella per a starfield
|
||||||
# © 2026 JailDesigner
|
# © 2026 JailDesigner
|
||||||
|
|
||||||
name: star
|
name: starfield
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# title_flash.shp - Sparkle 4-puntes amb costats còncaus (Atari-style)
|
# effect/title_flash.shp - Sparkle 4-puntes amb costats còncaus (Atari-style)
|
||||||
# 4 puntes als cardinals (radi 30) i valls còncaus als 45° (corba Bezier
|
# 4 puntes als cardinals (radi 30) i valls còncaus als 45° (corba Bezier
|
||||||
# quadràtica amb control point ±8). 5 punts per arc subdividint la corba.
|
# quadràtica amb control point ±8). 5 punts per arc subdividint la corba.
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# enemy/orb.shp - ORNI enemic gegant (orb circular, doble anell amb radis)
|
||||||
|
# © 2026 JailDesigner
|
||||||
|
#
|
||||||
|
# Forma "reactor / boss circular" — més detall que els enemics petits perquè
|
||||||
|
# es renderitza a escala 1.5x i ha de llegir-se com a amenaça gran.
|
||||||
|
# - Anell exterior: dodecàgon (12 vèrtexs) — aparença circular suau, radi 20.
|
||||||
|
# - Anell interior: hexàgon (6 vèrtexs, rotat 30°) — radi 10.
|
||||||
|
# - 6 radis curts que connecten l'anell interior amb l'exterior.
|
||||||
|
# - Petit "+" central com a nucli.
|
||||||
|
# Bounding radius natiu = 20 (alineat amb la resta d'enemics).
|
||||||
|
|
||||||
|
name: orb
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
# Anell exterior (dodecàgon, vèrtex apuntant amunt)
|
||||||
|
polyline: 0,-20 10,-17.32 17.32,-10 20,0 17.32,10 10,17.32 0,20 -10,17.32 -17.32,10 -20,0 -17.32,-10 -10,-17.32 0,-20
|
||||||
|
|
||||||
|
# Anell interior (hexàgon, vèrtex apuntant a la dreta — rotat 30° respecte l'exterior)
|
||||||
|
polyline: 5,-8.66 10,0 5,8.66 -5,8.66 -10,0 -5,-8.66 5,-8.66
|
||||||
|
|
||||||
|
# 6 radis: del vèrtex de l'hexàgon interior al vèrtex corresponent del dodecàgon exterior
|
||||||
|
line: 5,-8.66 10,-17.32
|
||||||
|
line: 10,0 20,0
|
||||||
|
line: 5,8.66 10,17.32
|
||||||
|
line: -5,8.66 -10,17.32
|
||||||
|
line: -10,0 -20,0
|
||||||
|
line: -5,-8.66 -10,-17.32
|
||||||
|
|
||||||
|
# Nucli central: petit "+" (2 segments creuats, radi 3)
|
||||||
|
line: -3,0 3,0
|
||||||
|
line: 0,-3 0,3
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# enemy_pentagon.shp - ORNI enemic (pentàgon doble concentric, radi exterior=20)
|
# enemy/pentagon.shp - ORNI enemic (pentàgon doble concentric, radi exterior=20)
|
||||||
|
|
||||||
name: enemy_pentagon
|
name: pentagon
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# enemy_pinwheel.shp - ORNI enemic (molinillo de 4 triangles)
|
# enemy/pinwheel.shp - ORNI enemic (molinillo de 4 triangles)
|
||||||
# © 2026 JailDesigner
|
# © 2026 JailDesigner
|
||||||
|
|
||||||
name: enemy_pinwheel
|
name: pinwheel
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# enemy_square.shp - ORNI enemic (rombe, radi=20) + ull amb pupil·la al centre
|
# enemy/square.shp - ORNI enemic (rombe, radi=20) + ull amb pupil·la al centre
|
||||||
|
|
||||||
name: enemy_square
|
name: square
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# enemy/star.shp - ORNI enemic (estrella de 5 puntes, només perímetre)
|
||||||
|
# © 2026 JailDesigner
|
||||||
|
#
|
||||||
|
# Pentagrama clàssic: 5 vèrtexs exteriors (radi 20) alternant amb 5 vèrtexs
|
||||||
|
# interiors (radi 7.64 = 20/φ² ≈ proporció àuria) per donar puntes esveltes.
|
||||||
|
# Vèrtex apuntant amunt (igual que enemy_pentagon).
|
||||||
|
#
|
||||||
|
# Sense línies interiors: una única polyline que recorre el perímetre.
|
||||||
|
# Bounding radius natiu ≈ 20 (alineat amb pentagon/square/pinwheel).
|
||||||
|
|
||||||
|
name: star
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
polyline: 0,-20 4.49,-6.18 19.02,-6.18 7.27,2.36 11.76,16.18 0,7.64 -11.76,16.18 -7.27,2.36 -19.02,-6.18 -4.49,-6.18 0,-20
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# char_lparen.shp - Símbol ( (parèntesi esquerre)
|
||||||
|
# Dimensions: 20×40 (blocky display)
|
||||||
|
|
||||||
|
name: char_lparen
|
||||||
|
scale: 1.0
|
||||||
|
center: 10, 20
|
||||||
|
|
||||||
|
# Arc cap a l'esquerra aproximat amb 4 trams rectes
|
||||||
|
polyline: 14,4 8,12 6,20 8,28 14,36
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# char_rparen.shp - Símbol ) (parèntesi dret)
|
||||||
|
# Dimensions: 20×40 (blocky display)
|
||||||
|
|
||||||
|
name: char_rparen
|
||||||
|
scale: 1.0
|
||||||
|
center: 10, 20
|
||||||
|
|
||||||
|
# Arc cap a la dreta aproximat amb 4 trams rectes
|
||||||
|
polyline: 6,4 12,12 14,20 12,28 6,36
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# char_slash.shp - Símbol / (barra)
|
||||||
|
# Dimensions: 20×40 (blocky display)
|
||||||
|
|
||||||
|
name: char_slash
|
||||||
|
scale: 1.0
|
||||||
|
center: 10, 20
|
||||||
|
|
||||||
|
# Línia diagonal de baix-esquerra a dalt-dreta
|
||||||
|
line: 4,36 16,4
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# ship.shp - Nau del jugador 1
|
|
||||||
# Triangle amb base còncava (punta de fletxa)
|
|
||||||
|
|
||||||
name: ship
|
|
||||||
scale: 1.0
|
|
||||||
center: 0, 0
|
|
||||||
|
|
||||||
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# ship/arrow.shp - Nau del jugador 1 (triangle amb base còncava, punta de fletxa)
|
||||||
|
|
||||||
|
name: arrow
|
||||||
|
scale: 1.0
|
||||||
|
center: 0, 0
|
||||||
|
|
||||||
|
polyline: 0,-12 8.49,8.49 0,4 -8.49,8.49 0,-12
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# ship2.shp - Nau del jugador 2 (interceptor amb ales)
|
# ship/interceptor.shp - Interceptor amb ales laterals pronunciades
|
||||||
# © 2026 JailDesigner
|
# © 2026 JailDesigner
|
||||||
|
|
||||||
name: ship2
|
name: interceptor
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
# ship2.shp - Nau del jugador 2
|
# ship/wedge.shp - Nau del jugador 2 (triangle amb cercle central)
|
||||||
# Triangle amb cercle central (distintiu visual)
|
|
||||||
|
|
||||||
name: ship2
|
name: wedge
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+158
-143
@@ -1,168 +1,183 @@
|
|||||||
# stages.yaml - Configuració de les 10 etapes d'Orni Attack
|
# stages.yaml - Configuració de les fases d'Orni Attack
|
||||||
# © 2026 JailDesigner
|
# © 2026 JailDesigner
|
||||||
|
#
|
||||||
|
# Format basat en onades (waves). Cada wave:
|
||||||
|
# - spawn: list d'enemics a generar, en ordre.
|
||||||
|
# - spawn_interval: segons entre spawns interns (default 0 = simultanis).
|
||||||
|
# - next: condició per avançar a la wave següent.
|
||||||
|
# - "all_dead" / "end" → quan tots els enemics de l'arena han mort.
|
||||||
|
# - { timeout: T } → quan han passat T segons des de l'inici de la wave.
|
||||||
|
# - { all_dead: true, timeout: T } → el que arribe abans (amuntegament si vas lent).
|
||||||
|
#
|
||||||
|
# Tipus d'enemic: pentagon, square (alias: cuadrado), pinwheel (alias: molinillo), star, orb.
|
||||||
|
|
||||||
metadata:
|
metadata:
|
||||||
version: "1.0"
|
version: "2.0"
|
||||||
total_stages: 10
|
total_stages: 10
|
||||||
description: "Progressive difficulty curve from novice to expert"
|
description: "Wave-based progression"
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
# STAGE 1: Tutorial - Mix de tots els tipus, velocitat lenta
|
# STAGE 1 — Tutorial: contacte amb pentagons i un cuadrado.
|
||||||
|
# (Test: també hi ha un orb a la primera onada per provar el contra-atac.)
|
||||||
- stage_id: 1
|
- stage_id: 1
|
||||||
total_enemies: 50
|
multipliers: { velocity: 0.85, rotation: 0.9, tracking: 0.3 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pentagon, pentagon, orb]
|
||||||
initial_delay: 0.3
|
spawn_interval: 0.6
|
||||||
spawn_interval: 0.4
|
next: all_dead
|
||||||
enemy_distribution:
|
- spawn: [pentagon, pentagon, square]
|
||||||
pentagon: 34
|
spawn_interval: 0.5
|
||||||
cuadrado: 33
|
next: all_dead
|
||||||
molinillo: 33
|
- spawn: [pentagon, pentagon, square, square]
|
||||||
difficulty_multipliers:
|
spawn_interval: 0.4
|
||||||
speed_multiplier: 0.7
|
next: end
|
||||||
rotation_multiplier: 0.8
|
|
||||||
tracking_strength: 0.0
|
|
||||||
|
|
||||||
# STAGE 2: Introduction to tracking enemies
|
# STAGE 2 — Apareixen molinillos.
|
||||||
- stage_id: 2
|
- stage_id: 2
|
||||||
total_enemies: 7
|
multipliers: { velocity: 0.95, rotation: 1.0, tracking: 0.4 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pentagon, pentagon, pentagon]
|
||||||
initial_delay: 1.5
|
spawn_interval: 0.5
|
||||||
spawn_interval: 2.5
|
next: all_dead
|
||||||
enemy_distribution:
|
- spawn: [pinwheel]
|
||||||
pentagon: 70
|
next: all_dead
|
||||||
cuadrado: 30
|
- spawn: [pentagon, square, pinwheel]
|
||||||
molinillo: 0
|
spawn_interval: 0.6
|
||||||
difficulty_multipliers:
|
next: all_dead
|
||||||
speed_multiplier: 0.85
|
- spawn: [pinwheel, pinwheel, pentagon]
|
||||||
rotation_multiplier: 0.9
|
spawn_interval: 0.5
|
||||||
tracking_strength: 0.3
|
next: end
|
||||||
|
|
||||||
# STAGE 3: All enemy types, normal speed
|
# STAGE 3 — Primer orb (HP=10).
|
||||||
- stage_id: 3
|
- stage_id: 3
|
||||||
total_enemies: 10
|
multipliers: { velocity: 1.0, rotation: 1.0, tracking: 0.5 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pentagon, pentagon, square]
|
||||||
initial_delay: 1.0
|
spawn_interval: 0.4
|
||||||
spawn_interval: 2.0
|
next: all_dead
|
||||||
enemy_distribution:
|
- spawn: [orb]
|
||||||
pentagon: 50
|
next: { all_dead: true, timeout: 12.0 }
|
||||||
cuadrado: 30
|
- spawn: [pinwheel, pinwheel]
|
||||||
molinillo: 20
|
spawn_interval: 0.5
|
||||||
difficulty_multipliers:
|
next: all_dead
|
||||||
speed_multiplier: 1.0
|
- spawn: [pentagon, square, pinwheel, pinwheel]
|
||||||
rotation_multiplier: 1.0
|
spawn_interval: 0.4
|
||||||
tracking_strength: 0.5
|
next: end
|
||||||
|
|
||||||
# STAGE 4: Increased count, faster enemies
|
# STAGE 4 — Pressió creixent: timeouts curts que poden encavalcar onades.
|
||||||
- stage_id: 4
|
- stage_id: 4
|
||||||
total_enemies: 12
|
multipliers: { velocity: 1.05, rotation: 1.1, tracking: 0.6 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pentagon, pentagon, pentagon]
|
||||||
initial_delay: 0.8
|
spawn_interval: 0.3
|
||||||
spawn_interval: 1.8
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
enemy_distribution:
|
- spawn: [square, square]
|
||||||
pentagon: 40
|
spawn_interval: 0.4
|
||||||
cuadrado: 35
|
next: { all_dead: true, timeout: 6.0 }
|
||||||
molinillo: 25
|
- spawn: [pinwheel, pinwheel, pinwheel]
|
||||||
difficulty_multipliers:
|
spawn_interval: 0.4
|
||||||
speed_multiplier: 1.1
|
next: all_dead
|
||||||
rotation_multiplier: 1.15
|
- spawn: [orb, pentagon, pentagon]
|
||||||
tracking_strength: 0.6
|
spawn_interval: 0.5
|
||||||
|
next: end
|
||||||
|
|
||||||
# STAGE 5: Maximum count reached
|
# STAGE 5 — Apareix la star (zigzag clon del pentagon).
|
||||||
- stage_id: 5
|
- stage_id: 5
|
||||||
total_enemies: 15
|
multipliers: { velocity: 1.1, rotation: 1.2, tracking: 0.7 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [star, star]
|
||||||
initial_delay: 0.5
|
spawn_interval: 0.4
|
||||||
spawn_interval: 1.5
|
next: all_dead
|
||||||
enemy_distribution:
|
- spawn: [pentagon, square, star]
|
||||||
pentagon: 35
|
spawn_interval: 0.4
|
||||||
cuadrado: 35
|
next: { all_dead: true, timeout: 6.0 }
|
||||||
molinillo: 30
|
- spawn: [pinwheel, pinwheel, star, star]
|
||||||
difficulty_multipliers:
|
spawn_interval: 0.4
|
||||||
speed_multiplier: 1.2
|
next: all_dead
|
||||||
rotation_multiplier: 1.25
|
- spawn: [orb, square, square]
|
||||||
tracking_strength: 0.7
|
spawn_interval: 0.5
|
||||||
|
next: end
|
||||||
|
|
||||||
# STAGE 6: Molinillo becomes dominant
|
# STAGE 6 — Densitat alta, mix amb timeouts agressius.
|
||||||
- stage_id: 6
|
- stage_id: 6
|
||||||
total_enemies: 15
|
multipliers: { velocity: 1.15, rotation: 1.25, tracking: 0.8 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pentagon, pinwheel, pentagon, pinwheel]
|
||||||
initial_delay: 0.3
|
spawn_interval: 0.3
|
||||||
spawn_interval: 1.3
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
enemy_distribution:
|
- spawn: [square, square, star]
|
||||||
pentagon: 30
|
spawn_interval: 0.4
|
||||||
cuadrado: 30
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
molinillo: 40
|
- spawn: [pinwheel, pinwheel, pinwheel]
|
||||||
difficulty_multipliers:
|
spawn_interval: 0.3
|
||||||
speed_multiplier: 1.3
|
next: all_dead
|
||||||
rotation_multiplier: 1.4
|
- spawn: [orb, pinwheel, pinwheel]
|
||||||
tracking_strength: 0.8
|
spawn_interval: 0.4
|
||||||
|
next: end
|
||||||
|
|
||||||
# STAGE 7: High intensity, fast spawns
|
# STAGE 7 — Tiradors i agressivitat.
|
||||||
- stage_id: 7
|
- stage_id: 7
|
||||||
total_enemies: 15
|
multipliers: { velocity: 1.25, rotation: 1.35, tracking: 0.9 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [square, square, square]
|
||||||
initial_delay: 0.2
|
spawn_interval: 0.5
|
||||||
spawn_interval: 1.0
|
next: { all_dead: true, timeout: 6.0 }
|
||||||
enemy_distribution:
|
- spawn: [pinwheel, pinwheel, pentagon, pentagon]
|
||||||
pentagon: 25
|
spawn_interval: 0.3
|
||||||
cuadrado: 30
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
molinillo: 45
|
- spawn: [star, star, star]
|
||||||
difficulty_multipliers:
|
spawn_interval: 0.4
|
||||||
speed_multiplier: 1.4
|
next: all_dead
|
||||||
rotation_multiplier: 1.5
|
- spawn: [orb, pinwheel, pinwheel, square]
|
||||||
tracking_strength: 0.9
|
spawn_interval: 0.5
|
||||||
|
next: end
|
||||||
|
|
||||||
# STAGE 8: Expert level, 50% molinillos
|
# STAGE 8 — Pressió constant.
|
||||||
- stage_id: 8
|
- stage_id: 8
|
||||||
total_enemies: 15
|
multipliers: { velocity: 1.35, rotation: 1.45, tracking: 1.0 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pinwheel, pinwheel, pinwheel]
|
||||||
initial_delay: 0.1
|
spawn_interval: 0.3
|
||||||
spawn_interval: 0.8
|
next: { all_dead: true, timeout: 4.0 }
|
||||||
enemy_distribution:
|
- spawn: [square, square, star, star]
|
||||||
pentagon: 20
|
spawn_interval: 0.3
|
||||||
cuadrado: 30
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
molinillo: 50
|
- spawn: [orb]
|
||||||
difficulty_multipliers:
|
next: { all_dead: true, timeout: 8.0 }
|
||||||
speed_multiplier: 1.5
|
- spawn: [pinwheel, pinwheel, square, star, pentagon]
|
||||||
rotation_multiplier: 1.6
|
spawn_interval: 0.3
|
||||||
tracking_strength: 1.0
|
next: end
|
||||||
|
|
||||||
# STAGE 9: Near-maximum difficulty
|
# STAGE 9 — Quasi-final.
|
||||||
- stage_id: 9
|
- stage_id: 9
|
||||||
total_enemies: 15
|
multipliers: { velocity: 1.5, rotation: 1.6, tracking: 1.1 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pinwheel, pinwheel, star, star]
|
||||||
initial_delay: 0.0
|
spawn_interval: 0.3
|
||||||
spawn_interval: 0.6
|
next: { all_dead: true, timeout: 4.0 }
|
||||||
enemy_distribution:
|
- spawn: [orb, square, square]
|
||||||
pentagon: 15
|
spawn_interval: 0.4
|
||||||
cuadrado: 25
|
next: { all_dead: true, timeout: 8.0 }
|
||||||
molinillo: 60
|
- spawn: [pinwheel, pinwheel, pinwheel, pinwheel]
|
||||||
difficulty_multipliers:
|
spawn_interval: 0.3
|
||||||
speed_multiplier: 1.6
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
rotation_multiplier: 1.7
|
- spawn: [orb, pinwheel, pinwheel, square, star]
|
||||||
tracking_strength: 1.1
|
spawn_interval: 0.4
|
||||||
|
next: end
|
||||||
|
|
||||||
# STAGE 10: Final challenge, 70% molinillos
|
# STAGE 10 — Repte final.
|
||||||
- stage_id: 10
|
- stage_id: 10
|
||||||
total_enemies: 15
|
multipliers: { velocity: 1.7, rotation: 1.8, tracking: 1.2 }
|
||||||
spawn_config:
|
waves:
|
||||||
mode: "progressive"
|
- spawn: [pinwheel, pinwheel, pinwheel, pinwheel]
|
||||||
initial_delay: 0.0
|
spawn_interval: 0.25
|
||||||
spawn_interval: 0.5
|
next: { all_dead: true, timeout: 4.0 }
|
||||||
enemy_distribution:
|
- spawn: [orb, square, star]
|
||||||
pentagon: 10
|
spawn_interval: 0.4
|
||||||
cuadrado: 20
|
next: { all_dead: true, timeout: 6.0 }
|
||||||
molinillo: 70
|
- spawn: [pinwheel, pinwheel, star, star, square]
|
||||||
difficulty_multipliers:
|
spawn_interval: 0.3
|
||||||
speed_multiplier: 1.8
|
next: { all_dead: true, timeout: 5.0 }
|
||||||
rotation_multiplier: 2.0
|
- spawn: [orb, orb, pinwheel, pinwheel, star]
|
||||||
tracking_strength: 1.2
|
spawn_interval: 0.4
|
||||||
|
next: end
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+36
-33
@@ -51,8 +51,6 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!music_enabled_) { return; }
|
|
||||||
|
|
||||||
auto* resource = AudioResource::getMusic(name);
|
auto* resource = AudioResource::getMusic(name);
|
||||||
if (resource == nullptr) { return; }
|
if (resource == nullptr) { return; }
|
||||||
|
|
||||||
@@ -62,7 +60,7 @@ void Audio::playMusic(const std::string& name, const int loop, const int crossfa
|
|||||||
|
|
||||||
// Reprodueix la música per punter (amb crossfade opcional)
|
// Reprodueix la música per punter (amb crossfade opcional)
|
||||||
void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms) {
|
void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms) {
|
||||||
if (!music_enabled_ || music == nullptr) { return; }
|
if (music == nullptr) { return; }
|
||||||
|
|
||||||
playMusicInternal(music, loop, crossfade_ms);
|
playMusicInternal(music, loop, crossfade_ms);
|
||||||
// Si el Ja::Music es va crear con filename (loadMusic con 3 arguments), el
|
// Si el Ja::Music es va crear con filename (loadMusic con 3 arguments), el
|
||||||
@@ -72,9 +70,12 @@ void Audio::playMusic(Ja::Music* music, const int loop, const int crossfade_ms)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Camí comú dels dos overloads: fa el dispatch crossfade vs stop+play i
|
// Camí comú dels dos overloads: fa el dispatch crossfade vs stop+play i
|
||||||
// actualitza el loop cachejat. Els callers s'encarreguen del gating
|
// actualitza el loop cachejat. Els callers s'encarreguen del same-track early
|
||||||
// (music_enabled_, nullptr, same-track early return) y del nom. L'estat el
|
// return i del nom. El gate de música deshabilitada NO atura la reproducció:
|
||||||
// manté Ja (Ja::playMusic posa PLAYING al Ja::Music* corresponent).
|
// effectiveVolume porta el volum efectiu a 0 i la pista continua sonant
|
||||||
|
// silenciada, per garantir que reactivar la música la torne a sentir sense
|
||||||
|
// haver de reiniciar la pista. L'estat el manté Ja (Ja::playMusic posa
|
||||||
|
// PLAYING al Ja::Music* corresponent).
|
||||||
void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossfade_ms) {
|
void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossfade_ms) {
|
||||||
const bool CURRENTLY_PLAYING = (getMusicState() == MusicState::PLAYING);
|
const bool CURRENTLY_PLAYING = (getMusicState() == MusicState::PLAYING);
|
||||||
if (crossfade_ms > 0 && CURRENTLY_PLAYING) {
|
if (crossfade_ms > 0 && CURRENTLY_PLAYING) {
|
||||||
@@ -91,41 +92,35 @@ void Audio::playMusicInternal(Ja::Music* music, const int loop, const int crossf
|
|||||||
|
|
||||||
// Pausa la música (l'estat el transiciona Engine::pauseMusic)
|
// Pausa la música (l'estat el transiciona Engine::pauseMusic)
|
||||||
void Audio::pauseMusic() {
|
void Audio::pauseMusic() {
|
||||||
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
if (getMusicState() == MusicState::PLAYING) {
|
||||||
engine_->pauseMusic();
|
engine_->pauseMusic();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
|
// Continua la música pausada (l'estat el transiciona Engine::resumeMusic)
|
||||||
void Audio::resumeMusic() {
|
void Audio::resumeMusic() {
|
||||||
if (music_enabled_ && getMusicState() == MusicState::PAUSED) {
|
if (getMusicState() == MusicState::PAUSED) {
|
||||||
engine_->resumeMusic();
|
engine_->resumeMusic();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atura la música (l'estat el transiciona Engine::stopMusic)
|
// Atura la música (l'estat el transiciona Engine::stopMusic)
|
||||||
void Audio::stopMusic() {
|
void Audio::stopMusic() {
|
||||||
if (music_enabled_) {
|
engine_->stopMusic();
|
||||||
engine_->stopMusic();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Audio::setMusicSpeed(float ratio) {
|
void Audio::setMusicSpeed(float ratio) {
|
||||||
if (music_enabled_) {
|
engine_->setMusicSpeed(ratio);
|
||||||
engine_->setMusicSpeed(ratio);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reprodueix un so per nom
|
// Reprodueix un so per nom
|
||||||
void Audio::playSound(const std::string& name, Group group) {
|
void Audio::playSound(const std::string& name, Group group) {
|
||||||
if (sound_enabled_) {
|
engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
|
||||||
engine_->playSound(AudioResource::getSound(name), 0, static_cast<int>(group));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reprodueix un so per punter directe
|
// Reprodueix un so per punter directe
|
||||||
void Audio::playSound(Ja::Sound* sound, Group group) {
|
void Audio::playSound(Ja::Sound* sound, Group group) {
|
||||||
if (sound_enabled_ && sound != nullptr) {
|
if (sound != nullptr) {
|
||||||
engine_->playSound(sound, 0, static_cast<int>(group));
|
engine_->playSound(sound, 0, static_cast<int>(group));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,7 +131,6 @@ void Audio::playSound(Ja::Sound* sound, Group group) {
|
|||||||
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
|
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
|
||||||
// la crida al ratio — sin efectes col·laterals.
|
// la crida al ratio — sin efectes col·laterals.
|
||||||
void Audio::playSound(const std::string& name, Group group, float speed) {
|
void Audio::playSound(const std::string& name, Group group, float speed) {
|
||||||
if (!sound_enabled_) { return; }
|
|
||||||
auto* sound = AudioResource::getSound(name);
|
auto* sound = AudioResource::getSound(name);
|
||||||
if (sound == nullptr) { return; }
|
if (sound == nullptr) { return; }
|
||||||
const int CH = engine_->playSound(sound, 0, static_cast<int>(group));
|
const int CH = engine_->playSound(sound, 0, static_cast<int>(group));
|
||||||
@@ -149,7 +143,6 @@ void Audio::playSound(const std::string& name, Group group, float speed) {
|
|||||||
// existeix o l'engine retorna -1 (sin de canals d'efecte plé), cau a playSound
|
// existeix o l'engine retorna -1 (sin de canals d'efecte plé), cau a playSound
|
||||||
// sec — l'usuari sent el so aún que la cua no s'apliqui.
|
// sec — l'usuari sent el so aún que la cua no s'apliqui.
|
||||||
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
|
void Audio::playSoundWithEcho(const std::string& name, const std::string& preset_name, Group group) {
|
||||||
if (!sound_enabled_) { return; }
|
|
||||||
auto* sound = AudioResource::getSound(name);
|
auto* sound = AudioResource::getSound(name);
|
||||||
if (sound == nullptr) { return; }
|
if (sound == nullptr) { return; }
|
||||||
|
|
||||||
@@ -168,7 +161,6 @@ void Audio::playSoundWithEcho(const std::string& name, const std::string& preset
|
|||||||
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
|
// Reprodueix un so processat per un reverb definit a sounds.yaml. Mateix
|
||||||
// fallback que playSoundWithEcho.
|
// fallback que playSoundWithEcho.
|
||||||
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
|
void Audio::playSoundWithReverb(const std::string& name, const std::string& preset_name, Group group) {
|
||||||
if (!sound_enabled_) { return; }
|
|
||||||
auto* sound = AudioResource::getSound(name);
|
auto* sound = AudioResource::getSound(name);
|
||||||
if (sound == nullptr) { return; }
|
if (sound == nullptr) { return; }
|
||||||
|
|
||||||
@@ -186,14 +178,12 @@ void Audio::playSoundWithReverb(const std::string& name, const std::string& pres
|
|||||||
|
|
||||||
// Atura tots los sons
|
// Atura tots los sons
|
||||||
void Audio::stopAllSounds() {
|
void Audio::stopAllSounds() {
|
||||||
if (sound_enabled_) {
|
engine_->stopChannel(-1);
|
||||||
engine_->stopChannel(-1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fa una fosa de sortida de la música
|
// Fa una fosa de sortida de la música
|
||||||
void Audio::fadeOutMusic(int milliseconds) {
|
void Audio::fadeOutMusic(int milliseconds) {
|
||||||
if (music_enabled_ && getMusicState() == MusicState::PLAYING) {
|
if (getMusicState() == MusicState::PLAYING) {
|
||||||
engine_->fadeOutMusic(milliseconds);
|
engine_->fadeOutMusic(milliseconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,14 +228,27 @@ auto Audio::effectiveVolume(float volume, bool channel_enabled) const -> float {
|
|||||||
return (enabled_ && channel_enabled) ? volume * config_.volume : 0.0F;
|
return (enabled_ && channel_enabled) ? volume * config_.volume : 0.0F;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estableix el volum dels sons (float 0.0..1.0)
|
// Estableix el volum dels sons (float 0.0..1.0). Actualitza el valor cachejat
|
||||||
|
// a config_ perquè els getters i les re-aplicacions internes (enableSound,
|
||||||
|
// setMasterVolume) puguin tornar al volum que l'usuari va triar.
|
||||||
void Audio::setSoundVolume(float sound_volume, Group group) {
|
void Audio::setSoundVolume(float sound_volume, Group group) {
|
||||||
engine_->setSoundVolume(effectiveVolume(sound_volume, sound_enabled_), static_cast<int>(group));
|
config_.sound_volume = std::clamp(sound_volume, MIN_VOLUME, MAX_VOLUME);
|
||||||
|
engine_->setSoundVolume(effectiveVolume(config_.sound_volume, sound_enabled_), static_cast<int>(group));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estableix el volum de la música (float 0.0..1.0)
|
// Estableix el volum de la música (float 0.0..1.0). Cf. setSoundVolume.
|
||||||
void Audio::setMusicVolume(float music_volume) {
|
void Audio::setMusicVolume(float music_volume) {
|
||||||
engine_->setMusicVolume(effectiveVolume(music_volume, music_enabled_));
|
config_.music_volume = std::clamp(music_volume, MIN_VOLUME, MAX_VOLUME);
|
||||||
|
engine_->setMusicVolume(effectiveVolume(config_.music_volume, music_enabled_));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estableix el volum master (multiplicador aplicat a sound + music). Re-aplica
|
||||||
|
// els canals perquè el canvi tingui efecte immediat sense esperar al següent
|
||||||
|
// setSoundVolume/setMusicVolume explícit.
|
||||||
|
void Audio::setMasterVolume(float master_volume) {
|
||||||
|
config_.volume = std::clamp(master_volume, MIN_VOLUME, MAX_VOLUME);
|
||||||
|
setSoundVolume(config_.sound_volume);
|
||||||
|
setMusicVolume(config_.music_volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aplica una nueva configuración (substitueix la config cachejada i reaplica enables/volums)
|
// Aplica una nueva configuración (substitueix la config cachejada i reaplica enables/volums)
|
||||||
@@ -256,12 +259,12 @@ void Audio::applySettings(const Config& config) {
|
|||||||
enable(config_.enabled);
|
enable(config_.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estableix l'estat general
|
// Estableix l'estat general. Re-aplica els volums actuals; effectiveVolume
|
||||||
|
// retalla a 0 quan enabled_ és false, sense perdre els valors guardats.
|
||||||
void Audio::enable(bool value) {
|
void Audio::enable(bool value) {
|
||||||
enabled_ = value;
|
enabled_ = value;
|
||||||
|
setSoundVolume(config_.sound_volume);
|
||||||
setSoundVolume(enabled_ ? config_.sound_volume : MIN_VOLUME);
|
setMusicVolume(config_.music_volume);
|
||||||
setMusicVolume(enabled_ ? config_.music_volume : MIN_VOLUME);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Estableix l'estat dels sons i reaplica el volum porque los canals actius
|
// Estableix l'estat dels sons i reaplica el volum porque los canals actius
|
||||||
|
|||||||
@@ -101,6 +101,14 @@ class Audio {
|
|||||||
// --- Control de volum (API interna: float 0.0..1.0) ---
|
// --- Control de volum (API interna: float 0.0..1.0) ---
|
||||||
void setSoundVolume(float volume, Group group = Group::ALL); // Ajusta el volum dels efectes
|
void setSoundVolume(float volume, Group group = Group::ALL); // Ajusta el volum dels efectes
|
||||||
void setMusicVolume(float volume); // Ajusta el volum de la música
|
void setMusicVolume(float volume); // Ajusta el volum de la música
|
||||||
|
void setMasterVolume(float volume); // Ajusta el master (re-aplica sound + music)
|
||||||
|
|
||||||
|
// Getters dels volums actuals (lectura de la config_ cachejada). Reflexen
|
||||||
|
// el valor que l'usuari ha triat l'última vegada, independent del gating
|
||||||
|
// d'enabled/channel.
|
||||||
|
[[nodiscard]] auto getMasterVolume() const -> float { return config_.volume; }
|
||||||
|
[[nodiscard]] auto getSoundVolume() const -> float { return config_.sound_volume; }
|
||||||
|
[[nodiscard]] auto getMusicVolume() const -> float { return config_.music_volume; }
|
||||||
|
|
||||||
// --- Helpers de conversió para la capa de presentació ---
|
// --- Helpers de conversió para la capa de presentació ---
|
||||||
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
|
// UI (menús, notificacions) manega enters 0..100; internament viu float 0..1.
|
||||||
|
|||||||
@@ -48,17 +48,30 @@ namespace Config {
|
|||||||
int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT};
|
int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT};
|
||||||
int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button
|
int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button
|
||||||
int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A button
|
int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A button
|
||||||
|
int button_start{SDL_GAMEPAD_BUTTON_START}; // Start button
|
||||||
|
int button_menu{SDL_GAMEPAD_BUTTON_BACK}; // Select/Back -> obre menu servei
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PlayerBindings {
|
struct PlayerBindings {
|
||||||
KeyboardBindings keyboard{};
|
KeyboardBindings keyboard{};
|
||||||
GamepadBindings gamepad{};
|
GamepadBindings gamepad{};
|
||||||
std::string gamepad_name; // Empty = auto-assign by index
|
std::string gamepad_name; // Empty = auto-assign by index
|
||||||
|
std::string gamepad_path; // Prioritari sobre name per distingir mateixos models
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AudioConfig {
|
||||||
|
bool enabled{true};
|
||||||
|
float volume{1.0F}; // Master 0..1
|
||||||
|
bool music_enabled{true};
|
||||||
|
float music_volume{1.0F};
|
||||||
|
bool sound_enabled{true};
|
||||||
|
float sound_volume{0.25F};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct EngineConfig {
|
struct EngineConfig {
|
||||||
WindowConfig window{};
|
WindowConfig window{};
|
||||||
RenderingConfig rendering{};
|
RenderingConfig rendering{};
|
||||||
|
AudioConfig audio{};
|
||||||
PlayerBindings player1{};
|
PlayerBindings player1{};
|
||||||
PlayerBindings player2{};
|
PlayerBindings player2{};
|
||||||
KeyboardBindings keyboard_controls{}; // Defaults globals per Input
|
KeyboardBindings keyboard_controls{}; // Defaults globals per Input
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
#include "core/defaults/physics.hpp"
|
#include "core/defaults/physics.hpp"
|
||||||
#include "core/defaults/playfield.hpp"
|
#include "core/defaults/playfield.hpp"
|
||||||
#include "core/defaults/rendering.hpp"
|
#include "core/defaults/rendering.hpp"
|
||||||
#include "core/defaults/ship.hpp"
|
|
||||||
#include "core/defaults/starfield_parallax.hpp"
|
#include "core/defaults/starfield_parallax.hpp"
|
||||||
#include "core/defaults/title.hpp"
|
#include "core/defaults/title.hpp"
|
||||||
#include "core/defaults/trail.hpp"
|
#include "core/defaults/trail.hpp"
|
||||||
|
|||||||
@@ -35,10 +35,11 @@ namespace Defaults::Music {
|
|||||||
namespace Defaults::Sound {
|
namespace Defaults::Sound {
|
||||||
|
|
||||||
constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras
|
constexpr const char* CONTINUE = "effects/continue.wav"; // Cuenta atras
|
||||||
constexpr const char* EXPLOSION = "effects/explosion.wav"; // Explosión
|
constexpr const char* ENEMY_EXPLOSION = "effects/enemy_explosion.wav"; // Explosió d'enemic (debris default)
|
||||||
constexpr const char* EXPLOSION2 = "effects/explosion2.wav"; // Explosión alternativa
|
constexpr const char* ENEMY_HIT = "effects/enemy_hit.wav"; // Impacte parcial a enemic (debris_partial — HP > 1)
|
||||||
|
constexpr const char* PLAYER_EXPLOSION = "effects/player_explosion.wav"; // Explosió de la nau del jugador
|
||||||
constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit
|
constexpr const char* FRIENDLY_FIRE_HIT = "effects/friendly_fire.wav"; // Friendly fire hit
|
||||||
constexpr const char* HIT = "effects/hit.wav"; // Enemic ferit (primer impacte → HURT)
|
constexpr const char* BULLET_ZAP = "effects/bullet_zap.wav"; // Bala desintegrant-se (qualsevol impacte o eixida de playarea)
|
||||||
constexpr const char* HURT = "effects/hurt.wav"; // Nau pròpia entra a HURT
|
constexpr const char* HURT = "effects/hurt.wav"; // Nau pròpia entra a HURT
|
||||||
constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD
|
constexpr const char* INIT_HUD = "effects/init_hud.wav"; // Para la animación del HUD
|
||||||
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
|
constexpr const char* LASER = "effects/laser_shoot.wav"; // Disparo
|
||||||
|
|||||||
@@ -1,101 +1,45 @@
|
|||||||
// enemies.hpp - Configuració per tipus d'enemic (Pentagon/Square/Molinillo), spawn i scoring
|
// enemies.hpp - Constants tècniques compartides per al sistema d'enemics.
|
||||||
// © 2026 JailDesigner
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Tots els paràmetres jugables (physics, animation, wounded, spawn,
|
||||||
|
// behavior, colors, scoring) viuen a data/entities/<type>/<type>.yaml i
|
||||||
|
// s'accedeixen via EnemyRegistry::get(EnemyType). Aquí només queda el
|
||||||
|
// que no és per personalitzar per tipus.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "core/defaults/entities.hpp"
|
namespace Defaults::Enemies::Spawn {
|
||||||
|
|
||||||
namespace Defaults::Enemies {
|
// Sostre de reintents al cercar una posició de spawn que respecti el
|
||||||
|
// safety_distance del tipus. No és un paràmetre jugable: és el llindar
|
||||||
|
// tècnic abans de caure a un fallback aleatori amb advertència.
|
||||||
|
constexpr int MAX_SPAWN_ATTEMPTS = 50;
|
||||||
|
|
||||||
// Cuerpo físico común (valores por defecto del constructor)
|
} // namespace Defaults::Enemies::Spawn
|
||||||
namespace Body {
|
|
||||||
constexpr float DEFAULT_MASS = 5.0F; // Más liviano que la nave (10.0)
|
|
||||||
constexpr float RESTITUTION = 1.0F; // Rebote elástico perfecto contra paredes
|
|
||||||
constexpr float LINEAR_DAMPING = 0.0F; // Sin fricción: mantienen velocidad
|
|
||||||
constexpr float ANGULAR_DAMPING = 0.0F;
|
|
||||||
} // namespace Body
|
|
||||||
|
|
||||||
// Pentagon (esquivador - zigzag evasion)
|
namespace Defaults::Enemies::Visual {
|
||||||
namespace Pentagon {
|
|
||||||
constexpr float SPEED = 35.0F; // px/s (slightly slower)
|
|
||||||
constexpr float MASS = 5.0F; // Masa estándar
|
|
||||||
constexpr float ANGLE_CHANGE_PROB = 0.20F; // 20% per wall hit (frequent zigzag)
|
|
||||||
constexpr float ANGLE_CHANGE_MAX = 1.0F; // Max random angle change (rad)
|
|
||||||
constexpr float ZIGZAG_PROB_PER_SECOND = 0.8F; // Probabilidad de zigzag por segundo
|
|
||||||
constexpr float ROTATION_DELTA_MIN = 0.75F; // Min visual rotation (rad/s) [+50%]
|
|
||||||
constexpr float ROTATION_DELTA_MAX = 3.75F; // Max visual rotation (rad/s) [+50%]
|
|
||||||
constexpr const char* SHAPE_FILE = "enemy_pentagon.shp";
|
|
||||||
} // namespace Pentagon
|
|
||||||
|
|
||||||
// Square (perseguidor - tracks player)
|
// Duració del "flash" que dispara l'acció FLASH (feedback per impacte
|
||||||
namespace Square {
|
// parcial en enemics HP>1). Curt: l'efecte ha de llegir-se com un cop,
|
||||||
constexpr float SPEED = 40.0F; // px/s (medium speed)
|
// no com una transició.
|
||||||
constexpr float MASS = 8.0F; // Más pesado, "tanque"
|
constexpr float FLASH_DURATION = 0.08F;
|
||||||
constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0)
|
|
||||||
constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates
|
|
||||||
constexpr float ROTATION_DELTA_MIN = 0.3F; // Slow rotation [+50%]
|
|
||||||
constexpr float ROTATION_DELTA_MAX = 1.5F; // [+50%]
|
|
||||||
constexpr const char* SHAPE_FILE = "enemy_square.shp";
|
|
||||||
} // namespace Square
|
|
||||||
|
|
||||||
// Molinillo (agressiu - fast straight lines, proximity spin-up)
|
} // namespace Defaults::Enemies::Visual
|
||||||
namespace Pinwheel {
|
|
||||||
constexpr float SPEED = 50.0F; // px/s (fastest)
|
|
||||||
constexpr float MASS = 4.0F; // Más liviano, ágil
|
|
||||||
constexpr float ANGLE_CHANGE_PROB = 0.05F; // 5% per wall hit (rare direction change)
|
|
||||||
constexpr float ANGLE_CHANGE_MAX = 0.3F; // Small angle adjustments
|
|
||||||
constexpr float ROTATION_DELTA_MIN = 3.0F; // Base rotation (rad/s) [+50%]
|
|
||||||
constexpr float ROTATION_DELTA_MAX = 6.0F; // [+50%]
|
|
||||||
constexpr float ROTATION_DELTA_PROXIMITY_MULTIPLIER = 3.0F; // Spin-up multiplier when near ship
|
|
||||||
constexpr float PROXIMITY_DISTANCE = 100.0F; // Distance threshold (px)
|
|
||||||
constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp";
|
|
||||||
} // namespace Pinwheel
|
|
||||||
|
|
||||||
// Animation parameters (shared)
|
namespace Defaults::Enemies::Debris {
|
||||||
namespace Animation {
|
|
||||||
// Palpitation
|
|
||||||
constexpr float PULSE_TRIGGER_PROB = 0.01F; // 1% chance per second
|
|
||||||
constexpr float PULSE_DURATION_MIN = 1.0F; // Min duration (seconds)
|
|
||||||
constexpr float PULSE_DURATION_MAX = 3.0F; // Max duration (seconds)
|
|
||||||
constexpr float PULSE_AMPLITUD_MIN = 0.08F; // Min scale variation
|
|
||||||
constexpr float PULSE_AMPLITUD_MAX = 0.20F; // Max scale variation
|
|
||||||
constexpr float PULSE_FREQ_MIN = 1.5F; // Min frequency (Hz)
|
|
||||||
constexpr float PULSE_FREQ_MAX = 3.0F; // Max frequency (Hz)
|
|
||||||
|
|
||||||
// Rotation acceleration
|
// Escala dels fragments per a l'acció CREATE_DEBRIS_PARTIAL (xip d'impacte
|
||||||
constexpr float ROTATION_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent]
|
// en enemics HP>1). 0.3 = trossos petits, com de "casc esquerdat".
|
||||||
constexpr float ROTATION_ACCEL_DURATION_MIN = 3.0F; // Min transition time
|
constexpr float PARTIAL_PIECE_SCALE = 0.3F;
|
||||||
constexpr float ROTATION_ACCEL_DURATION_MAX = 8.0F; // Max transition time
|
|
||||||
constexpr float ROTATION_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic]
|
|
||||||
constexpr float ROTATION_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic]
|
|
||||||
} // namespace Animation
|
|
||||||
|
|
||||||
// Wounded state (entre primer impacto y explosión)
|
} // namespace Defaults::Enemies::Debris
|
||||||
namespace Wounded {
|
|
||||||
constexpr float DURATION = 1.0F; // Segundos en estado herido antes de explotar
|
|
||||||
constexpr float BLINK_HZ = 10.0F; // Frecuencia de parpadeo color tipo ↔ dorado
|
|
||||||
} // namespace Wounded
|
|
||||||
|
|
||||||
// Spawn safety and invulnerability system
|
namespace Defaults::Enemies::Fireworks {
|
||||||
namespace Spawn {
|
|
||||||
// Safe spawn distance from player
|
|
||||||
constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F; // 3x ship radius
|
|
||||||
constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px
|
|
||||||
constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position
|
|
||||||
|
|
||||||
// Invulnerability system
|
// Paràmetres del firework "petit" per a l'acció CREATE_FIREWORKS_SMALL
|
||||||
constexpr float INVULNERABILITY_DURATION = 3.0F; // Seconds
|
// (feedback per impacte parcial en enemics HP>1). Pocs punts i baixa
|
||||||
constexpr float INVULNERABILITY_BRIGHTNESS_START = 0.3F; // Dim
|
// velocitat: una espurna breu, no una explosió.
|
||||||
constexpr float INVULNERABILITY_BRIGHTNESS_END = 0.7F; // Normal (same as Defaults::Brightness::ENEMIC)
|
constexpr int SMALL_N_POINTS = 20;
|
||||||
constexpr float INVULNERABILITY_SCALE_START = 0.0F; // Invisible
|
constexpr float SMALL_SPEED = 250.0F;
|
||||||
constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size
|
|
||||||
} // namespace Spawn
|
|
||||||
|
|
||||||
// Scoring system (puntuación per type de enemy)
|
} // namespace Defaults::Enemies::Fireworks
|
||||||
namespace Scoring {
|
|
||||||
constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s)
|
|
||||||
constexpr int SQUARE_SCORE = 150; // Square (perseguidor, 40 px/s)
|
|
||||||
constexpr int PINWHEEL_SCORE = 200; // Molinillo (agressiu, 50 px/s)
|
|
||||||
} // namespace Scoring
|
|
||||||
|
|
||||||
} // namespace Defaults::Enemies
|
|
||||||
|
|||||||
@@ -3,13 +3,28 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
namespace Defaults::Entities {
|
namespace Defaults::Entities {
|
||||||
|
|
||||||
constexpr int MAX_ORNIS = 15;
|
constexpr int MAX_ORNIS = 15;
|
||||||
constexpr int MAX_BULLETS = 50;
|
constexpr int MAX_BULLETS = 50; // per jugador (P1 + P2 = 2× aquest valor)
|
||||||
|
constexpr int MAX_ENEMY_BULLETS = 50; // pool reservat per a bales d'enemic
|
||||||
|
|
||||||
constexpr float SHIP_RADIUS = 12.0F;
|
// Total real de slots a l'array global bullets_: zona P1, zona P2 i zona enemic.
|
||||||
constexpr float ENEMY_RADIUS = 20.0F;
|
// Reservar zones impedeix que les bales d'enemic ocupin slots del jugador.
|
||||||
constexpr float BULLET_RADIUS = 3.0F;
|
constexpr int MAX_BULLETS_TOTAL = (MAX_BULLETS * 2) + MAX_ENEMY_BULLETS;
|
||||||
|
constexpr int ENEMY_BULLET_START_IDX = MAX_BULLETS * 2;
|
||||||
|
|
||||||
|
// Convenció d'owner_id per a Bullet::fire:
|
||||||
|
// 0..1 = players (P1, P2)
|
||||||
|
// ENEMY_OWNER_BASE + index = enemic concret (per identificar el seu autoimpacte)
|
||||||
|
// Una bala mata a qualsevol col·lisió excepte amb el seu propi creador.
|
||||||
|
constexpr uint8_t ENEMY_OWNER_BASE = 128;
|
||||||
|
|
||||||
|
// SHIP_RADIUS / ENEMY_RADIUS / BULLET_RADIUS han migrat: ara cada entitat
|
||||||
|
// calcula el seu collision_radius com a
|
||||||
|
// shape.bounding_radius × shape.scale × shape.collision_factor
|
||||||
|
// a partir del seu YAML (data/entities/<name>/<name>.yaml).
|
||||||
|
|
||||||
} // namespace Defaults::Entities
|
} // namespace Defaults::Entities
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ namespace Defaults::Game {
|
|||||||
// Friendly fire system
|
// Friendly fire system
|
||||||
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
|
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
|
||||||
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
|
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
|
||||||
constexpr float BULLET_SPEED = 700.0F; // Velocidad escalar (px/s). Pascal: 7 px/frame × 20 FPS
|
// BULLET_SPEED migrat a data/entities/player/player.yaml (weapon.bullet_speed).
|
||||||
|
|
||||||
// Transición LEVEL_START (mensajes aleatorios PRE-level)
|
// Transición LEVEL_START (mensajes aleatorios PRE-level)
|
||||||
constexpr float LEVEL_START_DURATION = 3.0F; // Duración total
|
constexpr float LEVEL_START_DURATION = 3.0F; // Duración total
|
||||||
|
|||||||
@@ -12,6 +12,17 @@ namespace Defaults::Hud {
|
|||||||
constexpr float SCOREBOARD_TEXT_SCALE = 0.85F;
|
constexpr float SCOREBOARD_TEXT_SCALE = 0.85F;
|
||||||
constexpr float SCOREBOARD_TEXT_SPACING = 0.0F;
|
constexpr float SCOREBOARD_TEXT_SPACING = 0.0F;
|
||||||
|
|
||||||
|
// Colors per segment del marcador. Jerarquia per funció (score/vides/nivell)
|
||||||
|
// + diferenciació de jugador (P1 blanc vs P2 rosa) sense xocar amb els
|
||||||
|
// colors d'enemics (cyan/roig). Amb alpha=255 el line_renderer usa el color
|
||||||
|
// directament sense caure al fallback verd (Rendering::DEFAULT_LINE_COLOR).
|
||||||
|
namespace Colors {
|
||||||
|
constexpr SDL_Color SCORE_P1 = {.r = 255, .g = 255, .b = 255, .a = 255}; // blanc
|
||||||
|
constexpr SDL_Color SCORE_P2 = {.r = 255, .g = 130, .b = 200, .a = 255}; // rosa magenta
|
||||||
|
constexpr SDL_Color LIVES = {.r = 255, .g = 180, .b = 60, .a = 255}; // ambre / or
|
||||||
|
constexpr SDL_Color LEVEL = {.r = 155, .g = 255, .b = 175, .a = 255}; // verd sistema
|
||||||
|
} // namespace Colors
|
||||||
|
|
||||||
// Animación de entrada del HUD (init_hud_animator).
|
// Animación de entrada del HUD (init_hud_animator).
|
||||||
namespace InitAnim {
|
namespace InitAnim {
|
||||||
// Spawn vertical de la nave: 50 px bajo la PLAYAREA (sale desde fuera).
|
// Spawn vertical de la nave: 50 px bajo la PLAYAREA (sale desde fuera).
|
||||||
@@ -33,7 +44,9 @@ namespace Defaults::Hud {
|
|||||||
namespace DebugOverlay {
|
namespace DebugOverlay {
|
||||||
constexpr float X = 30.0F;
|
constexpr float X = 30.0F;
|
||||||
constexpr float Y_FPS = 24.0F;
|
constexpr float Y_FPS = 24.0F;
|
||||||
constexpr float LINE_HEIGHT = 18.0F; // separació entre línies (scale 0.4 → ~16 px alt)
|
constexpr float FPS_LINE_HEIGHT = 28.0F; // separació després del FPS (scale 0.7 → ~28 px)
|
||||||
|
constexpr float LINE_HEIGHT = 18.0F; // separació entre línies (scale 0.4 → ~16 px alt)
|
||||||
|
constexpr float FPS_SCALE = 0.7F; // FPS més gran que la resta
|
||||||
constexpr float TEXT_SCALE = 0.4F;
|
constexpr float TEXT_SCALE = 0.4F;
|
||||||
constexpr float TEXT_SPACING = 2.0F;
|
constexpr float TEXT_SPACING = 2.0F;
|
||||||
constexpr float BRIGHTNESS = 1.0F;
|
constexpr float BRIGHTNESS = 1.0F;
|
||||||
|
|||||||
@@ -6,19 +6,18 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
// Paleta semántica por tipo de entidad. Si una entity declara color, lo
|
// Paleta semántica por tipo de entidad. Si una entity declara color, lo
|
||||||
// pasa al pipeline con alpha=255 (sentinela "color válido"); si no, se
|
// pasa al pipeline con alpha=255 (sentinela "color válido"); si no,
|
||||||
// usa el color global del oscilador (g_current_line_color).
|
// line_renderer::linea() cau a DEFAULT_LINE_COLOR (verd fòsfor fallback).
|
||||||
namespace Defaults::Palette {
|
namespace Defaults::Palette {
|
||||||
|
|
||||||
// Paleta neon: pujada lleugera dels canals secundaris per millorar la
|
// Paleta neon: pujada lleugera dels canals secundaris per millorar la
|
||||||
// brillantor perceptual sota el bloom (sense alterar la identitat de color).
|
// brillantor perceptual sota el bloom (sense alterar la identitat de color).
|
||||||
// El canal dominant es manté a 255 a cada color per maximitzar la saturació
|
// El canal dominant es manté a 255 a cada color per maximitzar la saturació
|
||||||
// visible quan el halo s'expandeix.
|
// visible quan el halo s'expandeix.
|
||||||
constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro
|
// Tots els colors d'entitats han migrat al seu YAML respectiu
|
||||||
constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser
|
// (data/entities/<name>/<name>.yaml, secció `colors`):
|
||||||
constexpr SDL_Color PENTAGON = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cyan pur "esquivador"
|
// - SHIP → player.yaml
|
||||||
constexpr SDL_Color SQUARE = {.r = 255, .g = 0, .b = 0, .a = 255}; // Roig pur "tank"
|
// - PENTAGON / SQUARE / PINWHEEL / WOUNDED → cada enemy.yaml
|
||||||
constexpr SDL_Color PINWHEEL = {.r = 255, .g = 0, .b = 255, .a = 255}; // Magenta pur "agressiu"
|
// - BULLET → bullet.yaml
|
||||||
constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido
|
|
||||||
|
|
||||||
} // namespace Defaults::Palette
|
} // namespace Defaults::Palette
|
||||||
|
|||||||
@@ -3,72 +3,62 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
namespace Defaults::Physics {
|
// NOTA: els paràmetres del player (rotation_speed, acceleration,
|
||||||
|
// max_velocity, death_impact_factor) viuen a data/entities/player/player.yaml.
|
||||||
|
// Els paràmetres específics de la bala (mass, restitution, damping,
|
||||||
|
// impact_momentum_factor) viuen a data/entities/bullet/bullet.yaml.
|
||||||
|
// Aquest fitxer només conté els paràmetres compartits del subsistema de
|
||||||
|
// debris (explosions visuals).
|
||||||
|
|
||||||
constexpr float ROTATION_SPEED = 3.14F; // rad/s (~180°/s)
|
namespace Defaults::Physics::Debris {
|
||||||
constexpr float ACCELERATION = 400.0F; // px/s²
|
|
||||||
constexpr float MAX_VELOCITY = 180.0F; // px/s
|
|
||||||
constexpr float FRICTION = 20.0F; // px/s²
|
|
||||||
|
|
||||||
// Bullet — impacto físico contra enemigo (impulse mass-aware).
|
constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
|
||||||
// Model: el impulse és el moment lineal de la bala (m·v) multiplicat per
|
constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s)
|
||||||
// un factor de transferència [0..1]. 1.0 = transfereix tot el moment
|
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
|
||||||
// (col·lisió perfectament inelàstica), 0.5 = transfereix la meitat.
|
constexpr float ROTATION_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
|
||||||
namespace Bullet {
|
constexpr float ROTATION_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
|
||||||
constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic
|
constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa
|
||||||
} // namespace Bullet
|
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION)
|
||||||
|
constexpr float SHRINK_RATE = 1.0F; // Reducció de mida (1.0 = encoge a 0 al final del min_lifetime)
|
||||||
|
|
||||||
// Ship → enemy: impuls explícit aplicat a l'enemic en el moment exacte
|
// Política de mort: passat el min_lifetime, el fragment mor quan la
|
||||||
// que la nau mor per col·lisió amb ell (afegit per damunt del rebot
|
// seva velocity cau per sota d'aquest llindar. Així els fragments
|
||||||
// natural de PhysicsWorld, que ja és present però subtil amb la
|
// ràpids no "popen" en moviment.
|
||||||
// damping de la nau).
|
constexpr float MIN_SPEED_TO_DIE = 5.0F; // px/s — al cuadrat per evitar sqrt en update
|
||||||
namespace Ship {
|
constexpr float MIN_SPEED_TO_DIE_SQ = MIN_SPEED_TO_DIE * MIN_SPEED_TO_DIE;
|
||||||
constexpr float DEATH_IMPACT_MOMENTUM_FACTOR = 0.3F;
|
|
||||||
} // namespace Ship
|
|
||||||
|
|
||||||
// Explosions (debris physics)
|
// Rebot contra els límits del PLAYAREA (mateix patró que enemics/ship).
|
||||||
namespace Debris {
|
// 0.7 = 70% de l'energia conservada al rebot.
|
||||||
constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
|
constexpr float RESTITUTION_BOUNDS = 0.7F;
|
||||||
constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s)
|
|
||||||
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
|
|
||||||
constexpr float ROTATION_MIN = 0.1F; // Rotación mínima (rad/s ~5.7°/s)
|
|
||||||
constexpr float ROTATION_MAX = 0.3F; // Rotación màxima (rad/s ~17.2°/s)
|
|
||||||
constexpr float TEMPS_VIDA = 2.0F; // Vida mínima garantida (s) — després pot morir per velocitat baixa
|
|
||||||
constexpr float TEMPS_VIDA_NAU = 3.0F; // Ship debris min lifetime (matches DEATH_DURATION)
|
|
||||||
constexpr float SHRINK_RATE = 1.0F; // Reducció de mida (1.0 = encoge a 0 al final del min_lifetime)
|
|
||||||
|
|
||||||
// Política de mort: passat el min_lifetime, el fragment mor quan la
|
// Herència de velocity angular (trayectorias curvas)
|
||||||
// seva velocity cau per sota d'aquest llindar. Així els fragments
|
constexpr float INHERITANCE_FACTOR_MIN = 0.7F; // Mínimo 70% del drotacio heredat
|
||||||
// ràpids no "popen" en moviment.
|
constexpr float INHERITANCE_FACTOR_MAX = 1.0F; // Màxim 100% del drotacio heredat
|
||||||
constexpr float MIN_SPEED_TO_DIE = 5.0F; // px/s — al cuadrat per evitar sqrt en update
|
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
|
||||||
constexpr float MIN_SPEED_TO_DIE_SQ = MIN_SPEED_TO_DIE * MIN_SPEED_TO_DIE;
|
|
||||||
|
|
||||||
// Rebot contra els límits del PLAYAREA (mateix patró que enemics/ship).
|
// Velocity heredada de la nau a l'explosió (80% del feel original).
|
||||||
// 0.7 = 70% de l'energia conservada al rebot.
|
constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F;
|
||||||
constexpr float RESTITUTION_BOUNDS = 0.7F;
|
|
||||||
|
|
||||||
// Herència de velocity angular (trayectorias curvas)
|
// Velocity heredada de l'enemic a l'explosió (palanca per a tuneo).
|
||||||
constexpr float INHERITANCE_FACTOR_MIN = 0.7F; // Mínimo 70% del drotacio heredat
|
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
|
||||||
constexpr float INHERITANCE_FACTOR_MAX = 1.0F; // Màxim 100% del drotacio heredat
|
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
|
||||||
constexpr float FRICCIO_ANGULAR = 0.5F; // Desacceleració angular (rad/s²)
|
|
||||||
|
|
||||||
// Velocity heredada de la nau a l'explosió (80% del feel original).
|
// Velocitat de la bala traspassada a cada fragment de debris al moment
|
||||||
constexpr float SHIP_VELOCITY_INHERITANCE = 0.8F;
|
// de l'impacte. Separat de la inèrcia del cos (velocitat_objecte): permet
|
||||||
|
// que els trossos volin "amb la força de la bala" encara que el cos pesi
|
||||||
|
// molt i amb prou feines es mogui. 0.4 a 700 px/s = ~280 px/s extra per
|
||||||
|
// fragment, molt visible sense ser excessiu.
|
||||||
|
constexpr float BULLET_IMPULSE_FACTOR = 0.4F;
|
||||||
|
|
||||||
// Velocity heredada de l'enemic a l'explosió (palanca per a tuneo).
|
// Tuneig específic de l'explosió d'enemic (overrides als defaults
|
||||||
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
|
// que es passen com a paràmetres opcionals a explode()).
|
||||||
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
|
constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més
|
||||||
|
constexpr float ENEMY_FRICTION = -30.0F; // Fricció més suau perquè s'estenguin més
|
||||||
|
constexpr int ENEMY_SEGMENT_MULTIPLIER = 1; // Sense còpies (5 cares = 5 trossos); >1 produeix grups sincronitzats
|
||||||
|
|
||||||
// Tuneig específic de l'explosió d'enemic (overrides als defaults
|
// Angular velocity sin for trajectory inheritance
|
||||||
// que es passen com a paràmetres opcionals a explode()).
|
// Excess above this threshold is converted to tangential linear velocity
|
||||||
constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més
|
// Prevents "vortex trap" problem with high-rotation enemies
|
||||||
constexpr float ENEMY_FRICTION = -30.0F; // Fricció més suau perquè s'estenguin més
|
constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s)
|
||||||
constexpr int ENEMY_SEGMENT_MULTIPLIER = 1; // Sense còpies (5 cares = 5 trossos); >1 produeix grups sincronitzats
|
|
||||||
|
|
||||||
// Angular velocity sin for trajectory inheritance
|
} // namespace Defaults::Physics::Debris
|
||||||
// Excess above this threshold is converted to tangential linear velocity
|
|
||||||
// Prevents "vortex trap" problem with high-rotation enemies
|
|
||||||
constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s)
|
|
||||||
} // namespace Debris
|
|
||||||
|
|
||||||
} // namespace Defaults::Physics
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
|
||||||
namespace Defaults::Rendering {
|
namespace Defaults::Rendering {
|
||||||
@@ -35,8 +34,12 @@ namespace Defaults::Rendering {
|
|||||||
constexpr int RENDER_HEIGHT_DEFAULT = 720;
|
constexpr int RENDER_HEIGHT_DEFAULT = 720;
|
||||||
|
|
||||||
constexpr auto isValidRenderResolution(int w, int h) -> bool {
|
constexpr auto isValidRenderResolution(int w, int h) -> bool {
|
||||||
return std::ranges::any_of(RESOLUTION_PRESETS,
|
for (const auto& preset : RESOLUTION_PRESETS) {
|
||||||
[w, h](const ResolutionPreset& preset) { return preset.w == w && preset.h == h; });
|
if (preset.w == w && preset.h == h) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Defaults::Rendering
|
} // namespace Defaults::Rendering
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// service_menu.hpp - Constants del menu de servei (F12)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
namespace Defaults::ServiceMenu {
|
||||||
|
|
||||||
|
// ---- Mides en coordenades logiques del joc (1280×720) ----
|
||||||
|
// BOX_WIDTH_MIN es el minim: si el titol o algun item no hi caben, el
|
||||||
|
// marc s'expandeix dinamicament amb animacio (cf. WIDTH_RATE).
|
||||||
|
constexpr int BOX_WIDTH_MIN = 460;
|
||||||
|
constexpr int GAP_Y = 22;
|
||||||
|
constexpr int TITLE_HEIGHT = 36; // scale 0.85 ≈ 34 px de text
|
||||||
|
constexpr int SUBTITLE_HEIGHT = 18; // scale 0.4 ≈ 16 px de text
|
||||||
|
constexpr int SEPARATOR_HEIGHT = 1;
|
||||||
|
constexpr int ITEM_HEIGHT = 38; // scale 0.55 ≈ 22 px de text + padding per al highlight
|
||||||
|
constexpr int ITEM_GAP_Y = 6;
|
||||||
|
|
||||||
|
// Brackets als 4 cantons (substitueixen la vora completa: estètica sci-fi).
|
||||||
|
constexpr int CORNER_ARM_H = 48;
|
||||||
|
constexpr int CORNER_ARM_V = 28;
|
||||||
|
constexpr int CORNER_THICKNESS = 2;
|
||||||
|
|
||||||
|
// ---- Animacio open/close (mateixos parametres que aee_arcade) ----
|
||||||
|
constexpr float OPEN_SPEED = 8.0F; // ~125 ms a obrir
|
||||||
|
constexpr float CLOSE_SPEED = 10.0F; // ~100 ms a tancar
|
||||||
|
constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada de la caixa
|
||||||
|
constexpr float WIDTH_RATE = 12.0F; // smoothing per a canvis d'ample entre pagines
|
||||||
|
|
||||||
|
// ---- Animacio del highlight (rectangle del cursor) ----
|
||||||
|
// Rate=18 dona settling ~0.17 s al 95% (ease-out exponencial).
|
||||||
|
constexpr float HIGHLIGHT_RATE = 18.0F;
|
||||||
|
constexpr int HIGHLIGHT_TICK_LEN = 10; // longitud dels ticks a cada cantonada
|
||||||
|
constexpr int HIGHLIGHT_THICKNESS = 1;
|
||||||
|
constexpr int HIGHLIGHT_PAD_X = 18; // padding lateral del rect respecte al text
|
||||||
|
constexpr int HIGHLIGHT_PAD_Y = 4; // padding vertical
|
||||||
|
constexpr int TEXT_INSET_X = 16; // marge intern del text dins del highlight (label esq / valor dre)
|
||||||
|
constexpr int MIN_LABEL_VALUE_GAP = 30; // mínim gap entre label i valor (per al càlcul d'ample dinàmic)
|
||||||
|
|
||||||
|
// ---- Colors RGBA ----
|
||||||
|
constexpr SDL_Color BG_COLOR{.r = 0, .g = 12, .b = 24, .a = 215};
|
||||||
|
constexpr SDL_Color CORNER_COLOR{.r = 120, .g = 220, .b = 255, .a = 255}; // cian neon
|
||||||
|
constexpr SDL_Color TITLE_COLOR{.r = 200, .g = 240, .b = 255, .a = 255};
|
||||||
|
constexpr SDL_Color SUBTITLE_COLOR{.r = 110, .g = 170, .b = 210, .a = 220}; // cian apagat
|
||||||
|
constexpr SDL_Color SEPARATOR_COLOR{.r = 60, .g = 120, .b = 180, .a = 180};
|
||||||
|
constexpr SDL_Color LABEL_COLOR{.r = 170, .g = 210, .b = 240, .a = 255};
|
||||||
|
constexpr SDL_Color CURSOR_COLOR{.r = 255, .g = 230, .b = 120, .a = 255}; // groc per al text sel·leccionat
|
||||||
|
constexpr SDL_Color HIGHLIGHT_OUTLINE{.r = 255, .g = 230, .b = 120, .a = 255}; // mateix groc, opac
|
||||||
|
constexpr SDL_Color HIGHLIGHT_FILL{.r = 255, .g = 230, .b = 120, .a = 36}; // wash translucid
|
||||||
|
|
||||||
|
// ---- Tipografia (VectorText). Scale 1.0 = caracter 20×40 px ----
|
||||||
|
constexpr float TITLE_SCALE = 0.85F; // mateixa escala que el HUD del scoreboard
|
||||||
|
constexpr float SUBTITLE_SCALE = 0.40F; // sota el titol, info decorativa (versio/hash)
|
||||||
|
constexpr float ITEM_SCALE = 0.55F; // mateixa escala que les notificacions
|
||||||
|
constexpr float TEXT_SPACING = 2.0F;
|
||||||
|
|
||||||
|
// ---- Sons UI (relatius a data/sounds/), portats d'aee_arcade ----
|
||||||
|
constexpr const char* SELECT_SOUND = "ui/menu_select.wav";
|
||||||
|
constexpr const char* ACCEPT_SOUND = "ui/menu_accept.wav";
|
||||||
|
|
||||||
|
} // namespace Defaults::ServiceMenu
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
// ship.hpp - Configuració de la nau (invulnerabilitat, parpelleig)
|
|
||||||
// © 2026 JailDesigner
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
namespace Defaults::Ship {
|
|
||||||
|
|
||||||
// Invulnerabilidad post-respawn
|
|
||||||
constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad
|
|
||||||
|
|
||||||
// Parpadeo visual durante invulnerabilidad
|
|
||||||
constexpr float BLINK_VISIBLE_TIME = 0.1F; // Tiempo visible (segundos)
|
|
||||||
constexpr float BLINK_INVISIBLE_TIME = 0.1F; // Tiempo invisible (segundos)
|
|
||||||
// Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s)
|
|
||||||
|
|
||||||
// Cuerpo físico
|
|
||||||
constexpr float MASS = 10.0F; // Masa de referencia para choques
|
|
||||||
constexpr float RESTITUTION = 0.6F; // Rebote moderado contra paredes
|
|
||||||
constexpr float LINEAR_DAMPING = 1.5F; // Fricción exponencial (s⁻¹)
|
|
||||||
constexpr float ANGULAR_DAMPING = 0.0F; // Rotación 100% por input (no inercial)
|
|
||||||
|
|
||||||
// Empuje visual: escala proporcional a la velocidad (0..200 px/s → 1.0..1.5)
|
|
||||||
// Mantiene la sensación del Pascal original.
|
|
||||||
constexpr float VISUAL_PUSH_DIVISOR = 33.33F; // SPEED / DIVISOR = empuje visual
|
|
||||||
constexpr float VISUAL_SCALE_DIVISOR = 12.0F; // SCALE = 1 + (PUSH / DIVISOR)
|
|
||||||
|
|
||||||
// Estat "ferit": entre primera col·lisió amb enemic i recuperació o segona col·lisió mortal.
|
|
||||||
namespace Hurt {
|
|
||||||
constexpr float DURATION = 15.0F; // Segons en estat ferit (provisional)
|
|
||||||
constexpr float BLINK_HZ = 10.0F; // Freqüència parpelleig color normal ↔ ferit
|
|
||||||
} // namespace Hurt
|
|
||||||
|
|
||||||
} // namespace Defaults::Ship
|
|
||||||
@@ -7,7 +7,7 @@ namespace Defaults::Trail {
|
|||||||
|
|
||||||
constexpr int POOL_SIZE = 200;
|
constexpr int POOL_SIZE = 200;
|
||||||
|
|
||||||
constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de Physics::MAX_VELOCITY (180)
|
constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de player.yaml::physics.max_velocity (180 px/s)
|
||||||
constexpr float EMIT_INTERVAL_S = 0.04F; // ~25 Hz nominal
|
constexpr float EMIT_INTERVAL_S = 0.04F; // ~25 Hz nominal
|
||||||
constexpr float EMIT_JITTER_S = 0.015F; // ±15 ms al cooldown
|
constexpr float EMIT_JITTER_S = 0.015F; // ±15 ms al cooldown
|
||||||
constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement
|
constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
// entity_loader.cpp - Implementació del carregador d'entitats YAML
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "core/entities/entity_loader.hpp"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <exception>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/resources/resource_helper.hpp"
|
||||||
|
|
||||||
|
namespace Entities {
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::shared_ptr<fkyaml::node>> EntityLoader::cache;
|
||||||
|
|
||||||
|
auto EntityLoader::load(const std::string& name) -> std::shared_ptr<fkyaml::node> {
|
||||||
|
// Cache hit
|
||||||
|
auto it = cache.find(name);
|
||||||
|
if (it != cache.end()) {
|
||||||
|
std::cout << "[EntityLoader] Cache hit: " << name << '\n';
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string PATH = "entities/" + name + "/" + name + ".yaml";
|
||||||
|
|
||||||
|
std::vector<uint8_t> data = Resource::Helper::loadFile(PATH);
|
||||||
|
if (data.empty()) {
|
||||||
|
std::cerr << "[EntityLoader] Error: no s'ha pogut load " << PATH << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
std::string yaml_content(data.begin(), data.end());
|
||||||
|
std::stringstream stream(yaml_content);
|
||||||
|
auto node = std::make_shared<fkyaml::node>(fkyaml::node::deserialize(stream));
|
||||||
|
|
||||||
|
std::cout << "[EntityLoader] Carregat: " << PATH << '\n';
|
||||||
|
cache[name] = node;
|
||||||
|
return node;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[EntityLoader] Excepció parsejant " << PATH << ": " << e.what() << '\n';
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EntityLoader::clearCache() {
|
||||||
|
std::cout << "[EntityLoader] Netejant caché (" << cache.size() << " entitats)" << '\n';
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto EntityLoader::getCacheSize() -> size_t { return cache.size(); }
|
||||||
|
|
||||||
|
} // namespace Entities
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// entity_loader.hpp - Carregador genèric de descriptors d'entitats en YAML
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Cada entitat viu a `data/entities/<name>/<name>.yaml` (mateix patró que el
|
||||||
|
// projecte germà aee_arcade). Aquest loader resol el path, llegeix del
|
||||||
|
// resource pack via Resource::Helper, parseja amb fkyaml i cacheja el node
|
||||||
|
// per evitar relectures. Retorna nullptr en cas d'error (el caller decideix
|
||||||
|
// si abortar).
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
|
namespace Entities {
|
||||||
|
|
||||||
|
class EntityLoader {
|
||||||
|
public:
|
||||||
|
EntityLoader() = delete; // tot estàtic
|
||||||
|
|
||||||
|
// Carrega el descriptor d'una entitat per nom (ex. "player" →
|
||||||
|
// "entities/player/player.yaml"). Retorna nullptr si no es pot
|
||||||
|
// carregar o parsejar. Cachejat per nom.
|
||||||
|
static auto load(const std::string& name) -> std::shared_ptr<fkyaml::node>;
|
||||||
|
|
||||||
|
// Buidar caché (útil per debug/recàrrega).
|
||||||
|
static void clearCache();
|
||||||
|
|
||||||
|
[[nodiscard]] static auto getCacheSize() -> size_t;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::unordered_map<std::string, std::shared_ptr<fkyaml::node>> cache;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Entities
|
||||||
@@ -51,12 +51,12 @@ namespace Graphics {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Lerp de l'oscil·lador (color base actual) cap a un color "flash" en
|
// Lerp del color base verd fòsfor cap a un color "flash" en funció de
|
||||||
// funció de f ∈ [0, 1]. Retorna sempre amb alpha>0 perquè el line_renderer
|
// f ∈ [0, 1]. Retorna sempre amb alpha>0 perquè el line_renderer l'usi
|
||||||
// l'use directament (sense barrejar amb el global).
|
// directament (sense caure al fallback DEFAULT_LINE_COLOR).
|
||||||
auto lerpColor(SDL_Color flash, float f) -> SDL_Color {
|
auto lerpColor(SDL_Color flash, float f) -> SDL_Color {
|
||||||
const float CLAMPED = std::clamp(f, 0.0F, 1.0F);
|
const float CLAMPED = std::clamp(f, 0.0F, 1.0F);
|
||||||
const SDL_Color BASE = Rendering::getLineColor();
|
constexpr SDL_Color BASE = Rendering::DEFAULT_LINE_COLOR;
|
||||||
const auto LERP_U8 = [&](unsigned char a, unsigned char b) {
|
const auto LERP_U8 = [&](unsigned char a, unsigned char b) {
|
||||||
const float OUT = (static_cast<float>(a) * (1.0F - CLAMPED)) + (static_cast<float>(b) * CLAMPED);
|
const float OUT = (static_cast<float>(a) * (1.0F - CLAMPED)) + (static_cast<float>(b) * CLAMPED);
|
||||||
return static_cast<unsigned char>(OUT);
|
return static_cast<unsigned char>(OUT);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ namespace Graphics {
|
|||||||
return it->second; // Cache hit
|
return it->second; // Cache hit
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize path: "ship.shp" → "shapes/ship.shp"
|
// Normalize path: "ship/arrow.shp" → "shapes/ship/arrow.shp"
|
||||||
// "logo/letra_j.shp" → "shapes/logo/letra_j.shp"
|
// "logo/letra_j.shp" → "shapes/logo/letra_j.shp"
|
||||||
std::string normalized = filename;
|
std::string normalized = filename;
|
||||||
if (!normalized.starts_with("shapes/")) {
|
if (!normalized.starts_with("shapes/")) {
|
||||||
@@ -53,7 +53,8 @@ namespace Graphics {
|
|||||||
|
|
||||||
// Cache and return
|
// Cache and return
|
||||||
std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName()
|
std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName()
|
||||||
<< ", " << shape->getNumPrimitives() << " primitives)" << '\n';
|
<< ", " << shape->getNumPrimitives() << " primitives, bounding_radius="
|
||||||
|
<< shape->getBoundingRadius() << ")" << '\n';
|
||||||
|
|
||||||
cache[filename] = shape;
|
cache[filename] = shape;
|
||||||
return shape;
|
return shape;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ namespace Graphics {
|
|||||||
|
|
||||||
// Carregar shape desde file (con caché)
|
// Carregar shape desde file (con caché)
|
||||||
// Retorna punter compartit (nullptr si error)
|
// Retorna punter compartit (nullptr si error)
|
||||||
// Exemple: load("ship.shp") → busca a "data/shapes/ship.shp"
|
// Exemple: load("ship/arrow.shp") → busca a "data/shapes/ship/arrow.shp"
|
||||||
static auto load(const std::string& filename) -> std::shared_ptr<Shape>;
|
static auto load(const std::string& filename) -> std::shared_ptr<Shape>;
|
||||||
|
|
||||||
// Netejar caché (útil per debug/recàrrega)
|
// Netejar caché (útil per debug/recàrrega)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ namespace Graphics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cargar símbolos
|
// Cargar símbolos
|
||||||
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?"};
|
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?", "/", "(", ")"};
|
||||||
for (const auto& sym : SYMBOLS) {
|
for (const auto& sym : SYMBOLS) {
|
||||||
char c = sym[0];
|
char c = sym[0];
|
||||||
std::string filename = getShapeFilename(c);
|
std::string filename = getShapeFilename(c);
|
||||||
@@ -164,6 +164,12 @@ namespace Graphics {
|
|||||||
return "font/char_exclamation.shp";
|
return "font/char_exclamation.shp";
|
||||||
case '?':
|
case '?':
|
||||||
return "font/char_question.shp";
|
return "font/char_question.shp";
|
||||||
|
case '/':
|
||||||
|
return "font/char_slash.shp";
|
||||||
|
case '(':
|
||||||
|
return "font/char_lparen.shp";
|
||||||
|
case ')':
|
||||||
|
return "font/char_rparen.shp";
|
||||||
case ' ':
|
case ' ':
|
||||||
return ""; // Espai es maneja sin load shape
|
return ""; // Espai es maneja sin load shape
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ namespace Graphics {
|
|||||||
|
|
||||||
// Renderizar string completo
|
// Renderizar string completo
|
||||||
// - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':',
|
// - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':',
|
||||||
// '!', '?', ' ')
|
// '!', '?', '/', '(', ')', ' ')
|
||||||
// - position: posición inicial (esquina superior izquierda)
|
// - position: posición inicial (esquina superior izquierda)
|
||||||
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
||||||
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
||||||
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
|
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
|
||||||
// - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global
|
// - color: color RGBA explícit; si alpha==0 (default) es fa fallback a Rendering::DEFAULT_LINE_COLOR
|
||||||
void render(const std::string& text, const Vec2& position, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
|
void render(const std::string& text, const Vec2& position, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
|
||||||
|
|
||||||
// Renderizar string centrado en un punto
|
// Renderizar string centrado en un punto
|
||||||
@@ -35,7 +35,7 @@ namespace Graphics {
|
|||||||
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
// - scale: factor de scale (1.0 = 20×40 px por carácter)
|
||||||
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
// - spacing: espacio entre caracteres en píxeles (a scale 1.0)
|
||||||
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
|
// - brightness: factor de brightness (0.0-1.0, default 1.0 = màxima brightness)
|
||||||
// - color: color RGBA explícit; si alpha==0 (default) s'usa l'oscil·lador global
|
// - color: color RGBA explícit; si alpha==0 (default) es fa fallback a Rendering::DEFAULT_LINE_COLOR
|
||||||
void renderCentered(const std::string& text, const Vec2& centre_point, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
|
void renderCentered(const std::string& text, const Vec2& centre_point, float scale = 1.0F, float spacing = 2.0F, float brightness = 1.0F, SDL_Color color = {0, 0, 0, 0}) const;
|
||||||
|
|
||||||
// Calcular ancho total de un string (útil para centrado).
|
// Calcular ancho total de un string (útil para centrado).
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
// Mesh3D = llista de vèrtexs Vec3 + llista d'arestes (parells d'índexs).
|
// Mesh3D = llista de vèrtexs Vec3 + llista d'arestes (parells d'índexs).
|
||||||
// drawWireframe() aplica una Transform3D al mesh, projecta amb Camera3D i
|
// drawWireframe() aplica una Transform3D al mesh, projecta amb Camera3D i
|
||||||
// emet cada aresta com una línia 2D pel pipeline `Rendering::linea` (mateix
|
// emet cada aresta com una línia 2D pel pipeline `Rendering::linea` (mateix
|
||||||
// pipeline que la resta del joc: glow verd via ColorOscillator si color.a==0).
|
// pipeline que la resta del joc: verd fòsfor via Rendering::DEFAULT_LINE_COLOR si color.a==0).
|
||||||
//
|
//
|
||||||
// Sense depth buffer: el caller és responsable d'ordenar els meshos per
|
// Sense depth buffer: el caller és responsable d'ordenar els meshos per
|
||||||
// profunditat decreixent si vol oclusió coherent (la pipeline és LINE_LIST
|
// profunditat decreixent si vol oclusió coherent (la pipeline és LINE_LIST
|
||||||
|
|||||||
@@ -0,0 +1,407 @@
|
|||||||
|
// define_inputs.cpp - Implementacio de l'overlay modal de redefinicio
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "core/input/define_inputs.hpp"
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/audio/audio.hpp"
|
||||||
|
#include "core/defaults/service_menu.hpp"
|
||||||
|
#include "core/input/input.hpp"
|
||||||
|
#include "core/locale/locale.hpp"
|
||||||
|
#include "core/types.hpp"
|
||||||
|
#include "game/config_yaml.hpp"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr float CANVAS_W = 1280.0F;
|
||||||
|
constexpr float CANVAS_H = 720.0F;
|
||||||
|
|
||||||
|
// Llindar de trigger per a edge-detect L2/R2 com a boto virtual.
|
||||||
|
constexpr Sint16 TRIGGER_THRESHOLD = 16384;
|
||||||
|
|
||||||
|
// Codis virtuals per als triggers (consistents amb input_types.cpp).
|
||||||
|
constexpr int TRIGGER_L2_VIRTUAL = 100;
|
||||||
|
constexpr int TRIGGER_R2_VIRTUAL = 101;
|
||||||
|
|
||||||
|
// Durada del missatge de confirmacio abans de tancar-se.
|
||||||
|
constexpr float COMPLETE_DISPLAY_S = 1.5F;
|
||||||
|
|
||||||
|
// Llindar dpad als axis sticks: no es captura per evitar conflicte amb el
|
||||||
|
// moviment LEFT/RIGHT/UP/DOWN (que es presuposen no redefinibles al mando).
|
||||||
|
constexpr Sint16 STICK_THRESHOLD = 16384;
|
||||||
|
|
||||||
|
// Crida pushRect amb un SDL_Color (les components s'escalen a [0..1]).
|
||||||
|
void fillRect(Rendering::Renderer* renderer, float x, float y, float w, float h, SDL_Color color) {
|
||||||
|
renderer->pushRect(x, y, w, h, static_cast<float>(color.r) / 255.0F, static_cast<float>(color.g) / 255.0F, static_cast<float>(color.b) / 255.0F, static_cast<float>(color.a) / 255.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto titleKey(System::DefineInputs::Mode mode, System::DefineInputs::Player player) -> std::string {
|
||||||
|
const bool IS_KB = (mode == System::DefineInputs::Mode::KEYBOARD);
|
||||||
|
const bool IS_P1 = (player == System::DefineInputs::Player::P1);
|
||||||
|
if (IS_KB && IS_P1) {
|
||||||
|
return "define.title_keyboard_p1";
|
||||||
|
}
|
||||||
|
if (IS_KB) {
|
||||||
|
return "define.title_keyboard_p2";
|
||||||
|
}
|
||||||
|
if (IS_P1) {
|
||||||
|
return "define.title_gamepad_p1";
|
||||||
|
}
|
||||||
|
return "define.title_gamepad_p2";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scancodes que MAI capturem com a binding (reservats per a navegacio o
|
||||||
|
// global hotkeys). Tornen true → handleEvent les deixa passar al pipeline
|
||||||
|
// global perque facin la seua feina (ESC obre el prompt d'eixida, F1-F12
|
||||||
|
// son hotkeys de sistema, RETURN/BACKSPACE/TAB son navegacio).
|
||||||
|
auto isReservedScancode(SDL_Scancode sc) -> bool {
|
||||||
|
if (sc == SDL_SCANCODE_ESCAPE) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (sc >= SDL_SCANCODE_F1 && sc <= SDL_SCANCODE_F12) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (sc == SDL_SCANCODE_RETURN || sc == SDL_SCANCODE_KP_ENTER) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (sc == SDL_SCANCODE_BACKSPACE || sc == SDL_SCANCODE_TAB) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversio sense pèrdua de SDL_Scancode → int per a comparacions
|
||||||
|
// homogenies dins de sequence_ (que guarda codis de tots dos modes).
|
||||||
|
auto scancodeToInt(SDL_Scancode sc) -> int {
|
||||||
|
return static_cast<int>(sc);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace System {
|
||||||
|
|
||||||
|
std::unique_ptr<DefineInputs> DefineInputs::instance;
|
||||||
|
|
||||||
|
void DefineInputs::init(Rendering::Renderer* renderer) {
|
||||||
|
if (!instance) {
|
||||||
|
instance = std::unique_ptr<DefineInputs>(new DefineInputs(renderer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineInputs::destroy() { instance.reset(); }
|
||||||
|
|
||||||
|
auto DefineInputs::get() -> DefineInputs* { return instance.get(); }
|
||||||
|
|
||||||
|
DefineInputs::DefineInputs(Rendering::Renderer* renderer)
|
||||||
|
: renderer_(renderer),
|
||||||
|
text_(renderer) {}
|
||||||
|
|
||||||
|
auto DefineInputs::isActive() const -> bool {
|
||||||
|
return phase_ != Phase::INACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto DefineInputs::begin(Mode mode, Player player) -> bool {
|
||||||
|
if (mode == Mode::GAMEPAD) {
|
||||||
|
// Requereix un pad assignat al jugador.
|
||||||
|
const auto* input = Input::get();
|
||||||
|
if (input == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const int IDX = (player == Player::P1) ? 0 : 1;
|
||||||
|
if (input->getPlayerGamepad(IDX) == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mode_ = mode;
|
||||||
|
player_ = player;
|
||||||
|
index_ = 0;
|
||||||
|
complete_timer_s_ = 0.0F;
|
||||||
|
l2_was_pressed_ = false;
|
||||||
|
r2_was_pressed_ = false;
|
||||||
|
|
||||||
|
buildSequence();
|
||||||
|
phase_ = Phase::CAPTURING;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineInputs::cancel() {
|
||||||
|
phase_ = Phase::INACTIVE;
|
||||||
|
sequence_.clear();
|
||||||
|
index_ = 0;
|
||||||
|
complete_timer_s_ = 0.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineInputs::buildSequence() {
|
||||||
|
sequence_.clear();
|
||||||
|
if (mode_ == Mode::KEYBOARD) {
|
||||||
|
// Teclat: LEFT, RIGHT, FIRE (SHOOT), ACCELERATE (THRUST)
|
||||||
|
sequence_.push_back({.action_label_key = "define.action.left", .action = InputAction::LEFT, .captured = -1});
|
||||||
|
sequence_.push_back({.action_label_key = "define.action.right", .action = InputAction::RIGHT, .captured = -1});
|
||||||
|
sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1});
|
||||||
|
sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1});
|
||||||
|
} else {
|
||||||
|
// Mando: FIRE, ACCELERATE, START, MENU
|
||||||
|
sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1});
|
||||||
|
sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1});
|
||||||
|
sequence_.push_back({.action_label_key = "define.action.start", .action = InputAction::START, .captured = -1});
|
||||||
|
sequence_.push_back({.action_label_key = "define.action.menu", .action = InputAction::MENU, .captured = -1});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto DefineInputs::isInUse(int code) const -> bool {
|
||||||
|
for (const auto& s : sequence_) {
|
||||||
|
if (s.captured == code) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineInputs::captureAndAdvance(int code) {
|
||||||
|
if (index_ >= sequence_.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isInUse(code)) {
|
||||||
|
return; // Duplicat dins de la sessio: rebutgem silenciosament
|
||||||
|
}
|
||||||
|
sequence_[index_].captured = code;
|
||||||
|
++index_;
|
||||||
|
if (auto* audio = Audio::get(); audio != nullptr) {
|
||||||
|
audio->playSound(Defaults::ServiceMenu::ACCEPT_SOUND, Audio::Group::INTERFACE);
|
||||||
|
}
|
||||||
|
if (index_ >= sequence_.size()) {
|
||||||
|
persistAndComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineInputs::persistAndComplete() {
|
||||||
|
auto& cfg = (player_ == Player::P1)
|
||||||
|
? ConfigYaml::engine_config.player1
|
||||||
|
: ConfigYaml::engine_config.player2;
|
||||||
|
|
||||||
|
if (mode_ == Mode::KEYBOARD) {
|
||||||
|
for (const Step& s : sequence_) {
|
||||||
|
switch (s.action) {
|
||||||
|
case InputAction::LEFT:
|
||||||
|
cfg.keyboard.key_left = static_cast<SDL_Scancode>(s.captured);
|
||||||
|
break;
|
||||||
|
case InputAction::RIGHT:
|
||||||
|
cfg.keyboard.key_right = static_cast<SDL_Scancode>(s.captured);
|
||||||
|
break;
|
||||||
|
case InputAction::SHOOT:
|
||||||
|
cfg.keyboard.key_shoot = static_cast<SDL_Scancode>(s.captured);
|
||||||
|
break;
|
||||||
|
case InputAction::THRUST:
|
||||||
|
cfg.keyboard.key_thrust = static_cast<SDL_Scancode>(s.captured);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break; // START / MENU no es redefineixen al teclat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const Step& s : sequence_) {
|
||||||
|
switch (s.action) {
|
||||||
|
case InputAction::SHOOT:
|
||||||
|
cfg.gamepad.button_shoot = s.captured;
|
||||||
|
break;
|
||||||
|
case InputAction::THRUST:
|
||||||
|
cfg.gamepad.button_thrust = s.captured;
|
||||||
|
break;
|
||||||
|
case InputAction::START:
|
||||||
|
cfg.gamepad.button_start = s.captured;
|
||||||
|
break;
|
||||||
|
case InputAction::MENU:
|
||||||
|
cfg.gamepad.button_menu = s.captured;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break; // LEFT / RIGHT no es redefineixen al mando
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aplicar canvis al runtime de l'Input i persistir a disc.
|
||||||
|
if (auto* input = Input::get(); input != nullptr) {
|
||||||
|
if (player_ == Player::P1) {
|
||||||
|
input->applyPlayer1Bindings(ConfigYaml::engine_config.player1);
|
||||||
|
} else {
|
||||||
|
input->applyPlayer2Bindings(ConfigYaml::engine_config.player2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConfigYaml::saveToFile();
|
||||||
|
|
||||||
|
phase_ = Phase::COMPLETE;
|
||||||
|
complete_timer_s_ = COMPLETE_DISPLAY_S;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineInputs::update(float delta_time) {
|
||||||
|
if (phase_ != Phase::COMPLETE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
complete_timer_s_ -= delta_time;
|
||||||
|
if (complete_timer_s_ <= 0.0F) {
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineInputs::processTrigger(int virtual_button, bool& was_pressed, bool now) {
|
||||||
|
if (now && !was_pressed) {
|
||||||
|
captureAndAdvance(virtual_button);
|
||||||
|
}
|
||||||
|
was_pressed = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto DefineInputs::handleKeyboardEvent(const SDL_Event& event) -> bool {
|
||||||
|
if (event.type != SDL_EVENT_KEY_DOWN) {
|
||||||
|
return true; // Empassem la resta sense fer res
|
||||||
|
}
|
||||||
|
const SDL_Scancode SC = event.key.scancode;
|
||||||
|
if (isReservedScancode(SC)) {
|
||||||
|
// ESC, F1-F12, RETURN, BACKSPACE, TAB es deixen passar al pipeline
|
||||||
|
// global (ESC obre el prompt d'eixida; F1-F12 hotkeys, etc.).
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
captureAndAdvance(scancodeToInt(SC));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto DefineInputs::handleGamepadEvent(const SDL_Event& event) -> bool {
|
||||||
|
// KEY_DOWN no es per al rebind de mando: deixem que el global el
|
||||||
|
// gestioni (ex. ESC → prompt d'eixida, F12 → tanca menu, etc.).
|
||||||
|
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrar events al pad del jugador actiu.
|
||||||
|
const auto* input = Input::get();
|
||||||
|
if (input == nullptr) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const int IDX = (player_ == Player::P1) ? 0 : 1;
|
||||||
|
auto pad = input->getPlayerGamepad(IDX);
|
||||||
|
if (!pad) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
|
||||||
|
if (event.gbutton.which != pad->instance_id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
captureAndAdvance(static_cast<int>(event.gbutton.button));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
|
||||||
|
if (event.gaxis.which != pad->instance_id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const auto AXIS = static_cast<SDL_GamepadAxis>(event.gaxis.axis);
|
||||||
|
const Sint16 VAL = event.gaxis.value;
|
||||||
|
if (AXIS == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) {
|
||||||
|
processTrigger(TRIGGER_L2_VIRTUAL, l2_was_pressed_, VAL >= TRIGGER_THRESHOLD);
|
||||||
|
} else if (AXIS == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
|
||||||
|
processTrigger(TRIGGER_R2_VIRTUAL, r2_was_pressed_, VAL >= TRIGGER_THRESHOLD);
|
||||||
|
}
|
||||||
|
// Sticks LEFTX/LEFTY/RIGHTX/RIGHTY: ignorats (no son redefinibles).
|
||||||
|
(void)STICK_THRESHOLD;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto DefineInputs::handleEvent(const SDL_Event& event) -> bool {
|
||||||
|
if (phase_ == Phase::INACTIVE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// SDL_EVENT_QUIT i WINDOW_CLOSE_REQUESTED han de poder tancar la
|
||||||
|
// finestra encara que el modal estiga obert; els passem al pipeline.
|
||||||
|
if (event.type == SDL_EVENT_QUIT ||
|
||||||
|
event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (phase_ == Phase::COMPLETE) {
|
||||||
|
// Mentre mostrem el missatge OK, empassem la resta d'events sense
|
||||||
|
// capturar perque l'usuari no puga avançar accions sense voler.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (mode_ == Mode::KEYBOARD) {
|
||||||
|
return handleKeyboardEvent(event);
|
||||||
|
}
|
||||||
|
return handleGamepadEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DefineInputs::draw() const {
|
||||||
|
if (phase_ == Phase::INACTIVE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using namespace Defaults::ServiceMenu;
|
||||||
|
|
||||||
|
// Caixa centrada, dimensions fixes (no depen del contingut a redefinir).
|
||||||
|
constexpr float BOX_W = 560.0F;
|
||||||
|
constexpr float BOX_H = 280.0F;
|
||||||
|
const float BOX_X = (CANVAS_W - BOX_W) * 0.5F;
|
||||||
|
const float BOX_Y = (CANVAS_H - BOX_H) * 0.5F;
|
||||||
|
|
||||||
|
// Fons + brackets als 4 cantons (estil HUD del menu de servei).
|
||||||
|
fillRect(renderer_, BOX_X, BOX_Y, BOX_W, BOX_H, BG_COLOR);
|
||||||
|
|
||||||
|
const auto T = static_cast<float>(CORNER_THICKNESS);
|
||||||
|
const auto AH = static_cast<float>(CORNER_ARM_H);
|
||||||
|
const auto AV = static_cast<float>(CORNER_ARM_V);
|
||||||
|
fillRect(renderer_, BOX_X, BOX_Y, AH, T, CORNER_COLOR);
|
||||||
|
fillRect(renderer_, BOX_X, BOX_Y, T, AV, CORNER_COLOR);
|
||||||
|
fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y, AH, T, CORNER_COLOR);
|
||||||
|
fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y, T, AV, CORNER_COLOR);
|
||||||
|
fillRect(renderer_, BOX_X, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR);
|
||||||
|
fillRect(renderer_, BOX_X, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR);
|
||||||
|
fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR);
|
||||||
|
fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR);
|
||||||
|
|
||||||
|
const std::string TITLE = Locale::get().text(titleKey(mode_, player_));
|
||||||
|
const float TITLE_W = Graphics::VectorText::getTextWidth(TITLE, TITLE_SCALE, TEXT_SPACING);
|
||||||
|
const float TITLE_X = BOX_X + ((BOX_W - TITLE_W) * 0.5F);
|
||||||
|
const float TITLE_Y = BOX_Y + 26.0F;
|
||||||
|
text_.render(TITLE, Vec2{.x = TITLE_X, .y = TITLE_Y}, TITLE_SCALE, TEXT_SPACING, 1.0F, TITLE_COLOR);
|
||||||
|
|
||||||
|
if (phase_ == Phase::COMPLETE) {
|
||||||
|
const std::string OK = Locale::get().text("define.complete");
|
||||||
|
constexpr float OK_SCALE = 0.7F;
|
||||||
|
const float OK_W = Graphics::VectorText::getTextWidth(OK, OK_SCALE, TEXT_SPACING);
|
||||||
|
const float OK_X = BOX_X + ((BOX_W - OK_W) * 0.5F);
|
||||||
|
const float OK_Y = BOX_Y + (BOX_H * 0.5F) - 10.0F;
|
||||||
|
constexpr SDL_Color OK_COLOR{.r = 120, .g = 255, .b = 140, .a = 255};
|
||||||
|
text_.render(OK, Vec2{.x = OK_X, .y = OK_Y}, OK_SCALE, TEXT_SPACING, 1.0F, OK_COLOR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instruccio (premeu tecla / boto) + accio actual + progres.
|
||||||
|
const std::string PROMPT = Locale::get().text(
|
||||||
|
mode_ == Mode::KEYBOARD ? "define.press_key" : "define.press_button");
|
||||||
|
const float PROMPT_W = Graphics::VectorText::getTextWidth(PROMPT, ITEM_SCALE, TEXT_SPACING);
|
||||||
|
const float PROMPT_X = BOX_X + ((BOX_W - PROMPT_W) * 0.5F);
|
||||||
|
const float PROMPT_Y = BOX_Y + 86.0F;
|
||||||
|
text_.render(PROMPT, Vec2{.x = PROMPT_X, .y = PROMPT_Y}, ITEM_SCALE, TEXT_SPACING, 1.0F, SUBTITLE_COLOR);
|
||||||
|
|
||||||
|
if (index_ < sequence_.size()) {
|
||||||
|
const std::string ACTION = Locale::get().text(sequence_[index_].action_label_key);
|
||||||
|
constexpr float ACTION_SCALE = 0.9F;
|
||||||
|
const float ACTION_W = Graphics::VectorText::getTextWidth(ACTION, ACTION_SCALE, TEXT_SPACING);
|
||||||
|
const float ACTION_X = BOX_X + ((BOX_W - ACTION_W) * 0.5F);
|
||||||
|
const float ACTION_Y = BOX_Y + 130.0F;
|
||||||
|
text_.render(ACTION, Vec2{.x = ACTION_X, .y = ACTION_Y}, ACTION_SCALE, TEXT_SPACING, 1.0F, CURSOR_COLOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string PROGRESS = std::format("{}/{}", index_ + 1, sequence_.size());
|
||||||
|
constexpr float PROG_SCALE = 0.4F;
|
||||||
|
const float PROG_W = Graphics::VectorText::getTextWidth(PROGRESS, PROG_SCALE, TEXT_SPACING);
|
||||||
|
const float PROG_X = BOX_X + ((BOX_W - PROG_W) * 0.5F);
|
||||||
|
const float PROG_Y = BOX_Y + 200.0F;
|
||||||
|
text_.render(PROGRESS, Vec2{.x = PROG_X, .y = PROG_Y}, PROG_SCALE, TEXT_SPACING, 1.0F, LABEL_COLOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace System
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
// define_inputs.hpp - Overlay modal de redefinici de controls (singleton)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Sub-mòdul inspirat en aee_arcade/source/core/input/define_buttons. Quan el
|
||||||
|
// menú de servei dispara una acció "Redefinir tecles/botons P1/P2", aquest
|
||||||
|
// singleton pren el control: pinta una caixa central, captura events SDL i
|
||||||
|
// avança per una seqüència fixa d'accions, persistint les noves assignacions
|
||||||
|
// a config.yaml en acabar.
|
||||||
|
//
|
||||||
|
// Cicle de vida:
|
||||||
|
// 1. begin(mode, player) → construeix la seqüència (4 passos) i activa
|
||||||
|
// l'overlay. Per a GAMEPAD, retorna false si el jugador no té pad.
|
||||||
|
// 2. handleEvent() captura el següent event vàlid; ESC cancel·la sense
|
||||||
|
// desar; duplicats dins de la sessió es rebutgen silenciosament.
|
||||||
|
// 3. Quan la seqüència es completa, persistim a engine_config + saveToFile,
|
||||||
|
// reapliquem els bindings i mostrem un missatge "OK" durant 1.5 s
|
||||||
|
// abans d'auto-tancar-se.
|
||||||
|
//
|
||||||
|
// El routing d'events es fa des de GlobalEvents::handle: mentre isActive()
|
||||||
|
// retorna true, tots els events SDL es desvien aquí i no arriben al joc ni
|
||||||
|
// al menú de servei.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/graphics/vector_text.hpp"
|
||||||
|
#include "core/input/input_types.hpp"
|
||||||
|
#include "core/rendering/render_context.hpp"
|
||||||
|
|
||||||
|
namespace System {
|
||||||
|
|
||||||
|
class DefineInputs {
|
||||||
|
public:
|
||||||
|
enum class Mode : std::uint8_t { KEYBOARD,
|
||||||
|
GAMEPAD };
|
||||||
|
enum class Player : std::uint8_t { P1,
|
||||||
|
P2 };
|
||||||
|
|
||||||
|
static void init(Rendering::Renderer* renderer);
|
||||||
|
static void destroy();
|
||||||
|
[[nodiscard]] static auto get() -> DefineInputs*;
|
||||||
|
|
||||||
|
// Comença la sessió. Retorna false per a GAMEPAD si el jugador no té
|
||||||
|
// cap pad assignat (el caller hauria de notificar a l'usuari abans).
|
||||||
|
auto begin(Mode mode, Player player) -> bool;
|
||||||
|
void cancel();
|
||||||
|
|
||||||
|
[[nodiscard]] auto isActive() const -> bool;
|
||||||
|
|
||||||
|
void update(float delta_time);
|
||||||
|
void draw() const;
|
||||||
|
|
||||||
|
// Retorna true si l'event s'ha consumit (és a dir, mentre l'overlay
|
||||||
|
// és actiu sempre consumeix tot per evitar passages al joc o menú).
|
||||||
|
auto handleEvent(const SDL_Event& event) -> bool;
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit DefineInputs(Rendering::Renderer* renderer);
|
||||||
|
|
||||||
|
enum class Phase : std::uint8_t {
|
||||||
|
INACTIVE,
|
||||||
|
CAPTURING,
|
||||||
|
COMPLETE, // mostra missatge OK breu abans d'auto-cancel
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Step {
|
||||||
|
std::string action_label_key; // p.ex. "define.action.left"
|
||||||
|
InputAction action; // mapeig a la struct PlayerBindings
|
||||||
|
int captured{-1}; // scancode o button code; -1 = sense capturar
|
||||||
|
};
|
||||||
|
|
||||||
|
void buildSequence();
|
||||||
|
[[nodiscard]] auto isInUse(int code) const -> bool;
|
||||||
|
void captureAndAdvance(int code);
|
||||||
|
void persistAndComplete();
|
||||||
|
|
||||||
|
// Handlers especialitzats segons mode_.
|
||||||
|
auto handleKeyboardEvent(const SDL_Event& event) -> bool;
|
||||||
|
auto handleGamepadEvent(const SDL_Event& event) -> bool;
|
||||||
|
|
||||||
|
// Edge-detect per als triggers L2/R2 com a botons virtuals.
|
||||||
|
void processTrigger(int virtual_button, bool& was_pressed, bool now);
|
||||||
|
|
||||||
|
Rendering::Renderer* renderer_;
|
||||||
|
Graphics::VectorText text_;
|
||||||
|
|
||||||
|
Phase phase_{Phase::INACTIVE};
|
||||||
|
Mode mode_{Mode::KEYBOARD};
|
||||||
|
Player player_{Player::P1};
|
||||||
|
std::vector<Step> sequence_;
|
||||||
|
std::size_t index_{0};
|
||||||
|
float complete_timer_s_{0.0F};
|
||||||
|
|
||||||
|
// Estat d'edge-detect dels triggers durant la sessió GAMEPAD.
|
||||||
|
bool l2_was_pressed_{false};
|
||||||
|
bool r2_was_pressed_{false};
|
||||||
|
|
||||||
|
static std::unique_ptr<DefineInputs> instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace System
|
||||||
+103
-29
@@ -2,12 +2,15 @@
|
|||||||
|
|
||||||
#include <SDL3/SDL.h> // Para SDL_GetGamepadAxis, SDL_GamepadAxis, SDL_GamepadButton, SDL_GetError, SDL_JoystickID, SDL_AddGamepadMappingsFromFile, SDL_Event, SDL_EventType, SDL_GetGamepadButton, SDL_GetKeyboardState, SDL_INIT_GAMEPAD, SDL_InitSubSystem, SDL_LogError, SDL_OpenGamepad, SDL_PollEvent, SDL_WasInit, Sint16, SDL_Gamepad, SDL_LogCategory, SDL_Scancode
|
#include <SDL3/SDL.h> // Para SDL_GetGamepadAxis, SDL_GamepadAxis, SDL_GamepadButton, SDL_GetError, SDL_JoystickID, SDL_AddGamepadMappingsFromFile, SDL_Event, SDL_EventType, SDL_GetGamepadButton, SDL_GetKeyboardState, SDL_INIT_GAMEPAD, SDL_InitSubSystem, SDL_LogError, SDL_OpenGamepad, SDL_PollEvent, SDL_WasInit, Sint16, SDL_Gamepad, SDL_LogCategory, SDL_Scancode
|
||||||
|
|
||||||
#include <algorithm> // Para std::ranges::any_of
|
|
||||||
#include <iostream> // Para basic_ostream, operator<<, cout, cerr
|
#include <iostream> // Para basic_ostream, operator<<, cout, cerr
|
||||||
#include <memory> // Para shared_ptr, __shared_ptr_access, allocator, operator==, make_shared
|
#include <memory> // Para shared_ptr, __shared_ptr_access, allocator, operator==, make_shared
|
||||||
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
|
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
|
||||||
#include <utility> // Para move
|
#include <utility> // Para move
|
||||||
|
|
||||||
|
#include "core/locale/locale.hpp"
|
||||||
|
#include "core/system/notifier.hpp"
|
||||||
|
#include "core/utils/string_utils.hpp"
|
||||||
|
|
||||||
// Singleton
|
// Singleton
|
||||||
Input* Input::instance = nullptr;
|
Input* Input::instance = nullptr;
|
||||||
|
|
||||||
@@ -162,9 +165,12 @@ auto Input::checkAnyButton(bool repeat) -> bool {
|
|||||||
|
|
||||||
// Comprueba si algún player (P1 o P2) presionó alguna acción de una lista
|
// Comprueba si algún player (P1 o P2) presionó alguna acción de una lista
|
||||||
auto Input::checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat) -> bool {
|
auto Input::checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat) -> bool {
|
||||||
return std::ranges::any_of(actions, [this, repeat](const InputAction& action) {
|
for (const auto& action : actions) {
|
||||||
return checkActionPlayer1(action, repeat) || checkActionPlayer2(action, repeat);
|
if (checkActionPlayer1(action, repeat) || checkActionPlayer2(action, repeat)) {
|
||||||
});
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comprueba si hay algun mando conectado
|
// Comprueba si hay algun mando conectado
|
||||||
@@ -303,8 +309,11 @@ auto Input::checkTriggerInput(Action action, const std::shared_ptr<Gamepad>& gam
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Input::addGamepadMappingsFromFile() {
|
void Input::addGamepadMappingsFromFile() {
|
||||||
if (SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str()) < 0) {
|
const int COUNT = SDL_AddGamepadMappingsFromFile(gamepad_mappings_file_.c_str());
|
||||||
std::cout << "Error, could not load " << gamepad_mappings_file_.c_str() << " file: " << SDL_GetError() << '\n';
|
if (COUNT < 0) {
|
||||||
|
std::cerr << "[Input] Error carregant " << gamepad_mappings_file_ << ": " << SDL_GetError() << '\n';
|
||||||
|
} else {
|
||||||
|
std::cout << "[Input] " << gamepad_mappings_file_ << " carregat (" << COUNT << " mappings)\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,8 +331,7 @@ void Input::initSDLGamePad() {
|
|||||||
} else {
|
} else {
|
||||||
addGamepadMappingsFromFile();
|
addGamepadMappingsFromFile();
|
||||||
discoverGamepads();
|
discoverGamepads();
|
||||||
std::cout << "\n** INPUT SYSTEM **\n";
|
std::cout << "[Input] inicialitzat\n";
|
||||||
std::cout << "Input System initialized successfully\n";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,9 +381,25 @@ void Input::update() {
|
|||||||
|
|
||||||
// --- MANDOS ---
|
// --- MANDOS ---
|
||||||
for (const auto& gamepad : gamepads_) {
|
for (const auto& gamepad : gamepads_) {
|
||||||
|
// LEFT i RIGHT NO son redefinibles al mando (assumits dpad o stick).
|
||||||
|
// Llegim el left stick X i el fusionem amb l'estat del dpad: qualsevol
|
||||||
|
// de les dos fonts activa l'accio. Llindar AXIS_THRESHOLD (30000).
|
||||||
|
const Sint16 STICK_X = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX);
|
||||||
|
const bool STICK_LEFT = STICK_X < -AXIS_THRESHOLD;
|
||||||
|
const bool STICK_RIGHT = STICK_X > AXIS_THRESHOLD;
|
||||||
|
|
||||||
for (auto& binding : gamepad->bindings) {
|
for (auto& binding : gamepad->bindings) {
|
||||||
bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0;
|
bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0;
|
||||||
|
|
||||||
|
// Per a LEFT/RIGHT, fer un OR amb el stick X. La resta d'accions
|
||||||
|
// (THRUST/SHOOT/START/MENU) ignoren el stick aqui — si es vol
|
||||||
|
// dispar amb trigger L2/R2 cal binding amb codi 100/101.
|
||||||
|
if (binding.first == Action::LEFT) {
|
||||||
|
button_is_down_now = button_is_down_now || STICK_LEFT;
|
||||||
|
} else if (binding.first == Action::RIGHT) {
|
||||||
|
button_is_down_now = button_is_down_now || STICK_RIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
|
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
|
||||||
binding.second.just_pressed = button_is_down_now && !binding.second.is_held;
|
binding.second.just_pressed = button_is_down_now && !binding.second.is_held;
|
||||||
binding.second.is_held = button_is_down_now;
|
binding.second.is_held = button_is_down_now;
|
||||||
@@ -407,18 +431,40 @@ auto Input::addGamepad(int device_index) -> std::string {
|
|||||||
auto name = gamepad->name;
|
auto name = gamepad->name;
|
||||||
std::cout << "Gamepad connected (" << name << ")" << '\n';
|
std::cout << "Gamepad connected (" << name << ")" << '\n';
|
||||||
gamepads_.push_back(std::move(gamepad));
|
gamepads_.push_back(std::move(gamepad));
|
||||||
|
|
||||||
|
// Toast a pantalla. Pot ser nullptr durant discoverGamepads() inicial
|
||||||
|
// (l'Input::init() es crida abans que el Director instanciï el Notifier).
|
||||||
|
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||||
|
notifier->notifyInfo(localeSubstitute(
|
||||||
|
Locale::get().text("notification.gamepad_connected"),
|
||||||
|
"{name}",
|
||||||
|
Utils::toUpperAscii(name)));
|
||||||
|
}
|
||||||
|
|
||||||
return name + " CONNECTED";
|
return name + " CONNECTED";
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
|
auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
|
||||||
auto it = std::ranges::find_if(gamepads_, [id](const std::shared_ptr<Gamepad>& gamepad) {
|
auto it = gamepads_.end();
|
||||||
return gamepad->instance_id == id;
|
for (auto i = gamepads_.begin(); i != gamepads_.end(); ++i) {
|
||||||
});
|
if ((*i)->instance_id == id) {
|
||||||
|
it = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (it != gamepads_.end()) {
|
if (it != gamepads_.end()) {
|
||||||
std::string name = (*it)->name;
|
std::string name = (*it)->name;
|
||||||
std::cout << "Gamepad disconnected (" << name << ")" << '\n';
|
std::cout << "Gamepad disconnected (" << name << ")" << '\n';
|
||||||
gamepads_.erase(it);
|
gamepads_.erase(it);
|
||||||
|
|
||||||
|
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||||
|
notifier->notifyInfo(localeSubstitute(
|
||||||
|
Locale::get().text("notification.gamepad_disconnected"),
|
||||||
|
"{name}",
|
||||||
|
Utils::toUpperAscii(name)));
|
||||||
|
}
|
||||||
|
|
||||||
return name + " DISCONNECTED";
|
return name + " DISCONNECTED";
|
||||||
}
|
}
|
||||||
std::cerr << "No se encontró el gamepad con ID " << id << '\n';
|
std::cerr << "No se encontró el gamepad con ID " << id << '\n';
|
||||||
@@ -465,6 +511,33 @@ auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::
|
|||||||
|
|
||||||
// ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ==========
|
// ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ==========
|
||||||
|
|
||||||
|
// Cerca el gamepad assignat a un jugador. Prioritat path > name. Si els
|
||||||
|
// dos camps venen buits o no n'hi ha cap match retornem nullptr (sense
|
||||||
|
// mando explicit). L'autoassignacio inicial es resol al boot.
|
||||||
|
auto Input::resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad> {
|
||||||
|
if (gamepads_.empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bindings.gamepad_path.empty()) {
|
||||||
|
for (const auto& pad : gamepads_) {
|
||||||
|
if (pad && pad->path == bindings.gamepad_path) {
|
||||||
|
return pad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bindings.gamepad_name.empty()) {
|
||||||
|
for (const auto& pad : gamepads_) {
|
||||||
|
if (pad && pad->name == bindings.gamepad_name) {
|
||||||
|
return pad;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// Aplica configuración de controles del player 1
|
// Aplica configuración de controles del player 1
|
||||||
void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
|
void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
|
||||||
// 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico)
|
// 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico)
|
||||||
@@ -474,15 +547,8 @@ void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
|
|||||||
player1_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
|
player1_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
|
||||||
player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
|
player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
|
||||||
|
|
||||||
// 2. Encontrar gamepad por nombre (o usar primer gamepad como fallback)
|
// 2. Resoldre gamepad per path/name
|
||||||
std::shared_ptr<Gamepad> gamepad = nullptr;
|
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
|
||||||
if (bindings.gamepad_name.empty()) {
|
|
||||||
// Fallback: usar primer gamepad disponible
|
|
||||||
gamepad = (!gamepads_.empty()) ? gamepads_[0] : nullptr;
|
|
||||||
} else {
|
|
||||||
// Buscar por nombre
|
|
||||||
gamepad = findAvailableGamepadByName(bindings.gamepad_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!gamepad) {
|
if (!gamepad) {
|
||||||
player1_gamepad_ = nullptr;
|
player1_gamepad_ = nullptr;
|
||||||
@@ -494,6 +560,8 @@ void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
|
|||||||
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
|
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
|
||||||
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
|
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
|
||||||
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot;
|
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot;
|
||||||
|
gamepad->bindings[Action::START].button = bindings.gamepad.button_start;
|
||||||
|
gamepad->bindings[Action::MENU].button = bindings.gamepad.button_menu;
|
||||||
|
|
||||||
// 4. Cachear referencia
|
// 4. Cachear referencia
|
||||||
player1_gamepad_ = gamepad;
|
player1_gamepad_ = gamepad;
|
||||||
@@ -508,15 +576,8 @@ void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) {
|
|||||||
player2_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
|
player2_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
|
||||||
player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
|
player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
|
||||||
|
|
||||||
// 2. Encontrar gamepad por nombre (o usar segundo gamepad como fallback)
|
// 2. Resoldre gamepad per path/name
|
||||||
std::shared_ptr<Gamepad> gamepad = nullptr;
|
std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
|
||||||
if (bindings.gamepad_name.empty()) {
|
|
||||||
// Fallback: usar segundo gamepad disponible
|
|
||||||
gamepad = (gamepads_.size() > 1) ? gamepads_[1] : nullptr;
|
|
||||||
} else {
|
|
||||||
// Buscar por nombre
|
|
||||||
gamepad = findAvailableGamepadByName(bindings.gamepad_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!gamepad) {
|
if (!gamepad) {
|
||||||
player2_gamepad_ = nullptr;
|
player2_gamepad_ = nullptr;
|
||||||
@@ -528,6 +589,8 @@ void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) {
|
|||||||
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
|
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
|
||||||
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
|
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
|
||||||
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot;
|
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot;
|
||||||
|
gamepad->bindings[Action::START].button = bindings.gamepad.button_start;
|
||||||
|
gamepad->bindings[Action::MENU].button = bindings.gamepad.button_menu;
|
||||||
|
|
||||||
// 4. Cachear referencia
|
// 4. Cachear referencia
|
||||||
player2_gamepad_ = gamepad;
|
player2_gamepad_ = gamepad;
|
||||||
@@ -555,6 +618,17 @@ auto Input::checkActionPlayer1(Action action, bool repeat) -> bool {
|
|||||||
return keyboard_active || gamepad_active;
|
return keyboard_active || gamepad_active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Retorna el pad assignat (0=P1, 1=P2). Pot ser nullptr.
|
||||||
|
auto Input::getPlayerGamepad(int player_index) const -> std::shared_ptr<Input::Gamepad> {
|
||||||
|
if (player_index == 0) {
|
||||||
|
return player1_gamepad_;
|
||||||
|
}
|
||||||
|
if (player_index == 1) {
|
||||||
|
return player2_gamepad_;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// Consulta de input para player 2
|
// Consulta de input para player 2
|
||||||
auto Input::checkActionPlayer2(Action action, bool repeat) -> bool {
|
auto Input::checkActionPlayer2(Action action, bool repeat) -> bool {
|
||||||
// Comprobar teclado con el mapa específico de P2
|
// Comprobar teclado con el mapa específico de P2
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ class Input {
|
|||||||
{Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}},
|
{Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}},
|
||||||
{Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
|
{Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
|
||||||
{Action::THRUST, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}},
|
{Action::THRUST, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}},
|
||||||
{Action::SHOOT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH)}}} {}
|
{Action::SHOOT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH)}},
|
||||||
|
{Action::START, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_START)}},
|
||||||
|
{Action::MENU, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_BACK)}}} {}
|
||||||
|
|
||||||
~Gamepad() {
|
~Gamepad() {
|
||||||
if (pad != nullptr) {
|
if (pad != nullptr) {
|
||||||
@@ -107,6 +109,10 @@ class Input {
|
|||||||
auto checkActionPlayer1(Action action, bool repeat = true) -> bool;
|
auto checkActionPlayer1(Action action, bool repeat = true) -> bool;
|
||||||
auto checkActionPlayer2(Action action, bool repeat = true) -> bool;
|
auto checkActionPlayer2(Action action, bool repeat = true) -> bool;
|
||||||
|
|
||||||
|
// Accés al gamepad assignat per jugador (0=P1, 1=P2). nullptr si no n'hi
|
||||||
|
// ha cap d'assignat o connectat. Usat per la UI de redefinició de botons.
|
||||||
|
[[nodiscard]] auto getPlayerGamepad(int player_index) const -> std::shared_ptr<Gamepad>;
|
||||||
|
|
||||||
// Check if any player pressed any action from a list
|
// Check if any player pressed any action from a list
|
||||||
auto checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
|
auto checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
|
||||||
|
|
||||||
@@ -142,6 +148,7 @@ class Input {
|
|||||||
auto removeGamepad(SDL_JoystickID id) -> std::string;
|
auto removeGamepad(SDL_JoystickID id) -> std::string;
|
||||||
void addGamepadMappingsFromFile();
|
void addGamepadMappingsFromFile();
|
||||||
void discoverGamepads();
|
void discoverGamepads();
|
||||||
|
auto resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad>;
|
||||||
|
|
||||||
// --- Variables miembro ---
|
// --- Variables miembro ---
|
||||||
static Input* instance; // Instancia única del singleton
|
static Input* instance; // Instancia única del singleton
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
|
|||||||
{InputAction::RIGHT, "RIGHT"},
|
{InputAction::RIGHT, "RIGHT"},
|
||||||
{InputAction::THRUST, "THRUST"},
|
{InputAction::THRUST, "THRUST"},
|
||||||
{InputAction::SHOOT, "SHOOT"},
|
{InputAction::SHOOT, "SHOOT"},
|
||||||
|
{InputAction::START, "START"},
|
||||||
|
{InputAction::MENU, "MENU"},
|
||||||
{InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"},
|
{InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"},
|
||||||
{InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"},
|
{InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"},
|
||||||
{InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"},
|
{InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"},
|
||||||
@@ -18,6 +20,8 @@ const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
|
|||||||
{"RIGHT", InputAction::RIGHT},
|
{"RIGHT", InputAction::RIGHT},
|
||||||
{"THRUST", InputAction::THRUST},
|
{"THRUST", InputAction::THRUST},
|
||||||
{"SHOOT", InputAction::SHOOT},
|
{"SHOOT", InputAction::SHOOT},
|
||||||
|
{"START", InputAction::START},
|
||||||
|
{"MENU", InputAction::MENU},
|
||||||
{"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM},
|
{"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM},
|
||||||
{"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM},
|
{"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM},
|
||||||
{"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN},
|
{"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN},
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ enum class InputAction : std::uint8_t { // Acciones de entrada posibles en el j
|
|||||||
THRUST, // Acelerar
|
THRUST, // Acelerar
|
||||||
SHOOT, // Disparar
|
SHOOT, // Disparar
|
||||||
START, // Empezar match
|
START, // Empezar match
|
||||||
|
MENU, // Abrir/cerrar menu de servicio (equivalent a F12)
|
||||||
|
|
||||||
// Inputs de sistema (globales)
|
// Inputs de sistema (globales)
|
||||||
WINDOW_INC_ZOOM, // F2
|
WINDOW_INC_ZOOM, // F2
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
#include "core/physics/physics_world.hpp"
|
#include "core/physics/physics_world.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
#include "core/physics/rigid_body.hpp"
|
#include "core/physics/rigid_body.hpp"
|
||||||
@@ -14,9 +13,12 @@ namespace Physics {
|
|||||||
if (body == nullptr) {
|
if (body == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (std::ranges::find(bodies_, body) == bodies_.end()) {
|
for (const auto* b : bodies_) {
|
||||||
bodies_.push_back(body);
|
if (b == body) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
bodies_.push_back(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PhysicsWorld::removeBody(RigidBody* body) {
|
void PhysicsWorld::removeBody(RigidBody* body) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <SDL3/SDL_gpu.h>
|
#include <SDL3/SDL_gpu.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
@@ -390,6 +391,10 @@ namespace Rendering::GPU {
|
|||||||
color_target.store_op = SDL_GPU_STOREOP_STORE;
|
color_target.store_op = SDL_GPU_STOREOP_STORE;
|
||||||
render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr);
|
render_pass_ = SDL_BeginGPURenderPass(cmd_buffer_, &color_target, 1, nullptr);
|
||||||
|
|
||||||
|
// L'scissor és per render pass: en reobrir cal restaurar-lo des del top
|
||||||
|
// de la pila si pushClip/popClip s'han usat mid-frame.
|
||||||
|
applyCurrentScissor();
|
||||||
|
|
||||||
SDL_BindGPUGraphicsPipeline(render_pass_, line_pipeline_.get());
|
SDL_BindGPUGraphicsPipeline(render_pass_, line_pipeline_.get());
|
||||||
|
|
||||||
// UBO de líneas usa el tamaño lógico (también del offscreen).
|
// UBO de líneas usa el tamaño lógico (también del offscreen).
|
||||||
@@ -415,6 +420,11 @@ namespace Rendering::GPU {
|
|||||||
SDL_ReleaseGPUBuffer(dev, vbo);
|
SDL_ReleaseGPUBuffer(dev, vbo);
|
||||||
SDL_ReleaseGPUBuffer(dev, ibo);
|
SDL_ReleaseGPUBuffer(dev, ibo);
|
||||||
SDL_ReleaseGPUTransferBuffer(dev, tbo);
|
SDL_ReleaseGPUTransferBuffer(dev, tbo);
|
||||||
|
|
||||||
|
// Buidem el batch perquè pushClip/popClip puguin emetre seccions
|
||||||
|
// separades dins el mateix frame sense re-enviar geometria.
|
||||||
|
vertices_.clear();
|
||||||
|
indices_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GpuFrameRenderer::bloomPass() {
|
void GpuFrameRenderer::bloomPass() {
|
||||||
@@ -603,6 +613,51 @@ namespace Rendering::GPU {
|
|||||||
SDL_DrawGPUPrimitives(render_pass_, 3, 1, 0, 0);
|
SDL_DrawGPUPrimitives(render_pass_, 3, 1, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GpuFrameRenderer::pushClip(int logical_x, int logical_y, int logical_w, int logical_h) {
|
||||||
|
// Convertim coordenades lògiques (espai del joc, 1280×720) a píxels
|
||||||
|
// físics del offscreen (render_w_ × render_h_). Si l'usuari hi treballa
|
||||||
|
// amb upscale (p.ex. 1920×1080), l'scissor escala proporcionalment.
|
||||||
|
const float SX = render_w_ / logical_w_;
|
||||||
|
const float SY = render_h_ / logical_h_;
|
||||||
|
SDL_Rect rect{
|
||||||
|
.x = static_cast<int>(static_cast<float>(logical_x) * SX),
|
||||||
|
.y = static_cast<int>(static_cast<float>(logical_y) * SY),
|
||||||
|
.w = std::max(0, static_cast<int>(static_cast<float>(logical_w) * SX)),
|
||||||
|
.h = std::max(0, static_cast<int>(static_cast<float>(logical_h) * SY)),
|
||||||
|
};
|
||||||
|
// Emetem tot el batch acumulat *abans* d'activar l'scissor perquè quedi
|
||||||
|
// dibuixat sense retallar.
|
||||||
|
flushBatch();
|
||||||
|
clip_stack_.push_back(rect);
|
||||||
|
applyCurrentScissor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuFrameRenderer::popClip() {
|
||||||
|
// Emetem el batch que s'ha acumulat *dins* del clip actiu.
|
||||||
|
flushBatch();
|
||||||
|
if (!clip_stack_.empty()) {
|
||||||
|
clip_stack_.pop_back();
|
||||||
|
}
|
||||||
|
applyCurrentScissor();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GpuFrameRenderer::applyCurrentScissor() {
|
||||||
|
if (render_pass_ == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SDL_Rect rect{};
|
||||||
|
if (clip_stack_.empty()) {
|
||||||
|
// Sense clips: scissor cobreix tot el offscreen.
|
||||||
|
rect.x = 0;
|
||||||
|
rect.y = 0;
|
||||||
|
rect.w = static_cast<int>(render_w_);
|
||||||
|
rect.h = static_cast<int>(render_h_);
|
||||||
|
} else {
|
||||||
|
rect = clip_stack_.back();
|
||||||
|
}
|
||||||
|
SDL_SetGPUScissor(render_pass_, &rect);
|
||||||
|
}
|
||||||
|
|
||||||
void GpuFrameRenderer::endFrame() {
|
void GpuFrameRenderer::endFrame() {
|
||||||
if (cmd_buffer_ == nullptr) {
|
if (cmd_buffer_ == nullptr) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -94,6 +94,15 @@ namespace Rendering::GPU {
|
|||||||
// d'UI (notificacions, panels).
|
// d'UI (notificacions, panels).
|
||||||
void pushRect(float x, float y, float w, float h, float r, float g, float b, float a);
|
void pushRect(float x, float y, float w, float h, float r, float g, float b, float a);
|
||||||
|
|
||||||
|
// Clipping rectangular per a UI (scissor a SDL_GPU). pushClip/popClip
|
||||||
|
// forcen un flush intermedi del batch i activen/restauren l'scissor del
|
||||||
|
// pase actiu. Coordenades en píxels lògics del joc (1280×720); es
|
||||||
|
// converteixen a píxels físics del offscreen automàticament. Stack
|
||||||
|
// d'scissors per a clips niats. Quan la pila queda buida, l'scissor
|
||||||
|
// torna a cobrir el target sencer.
|
||||||
|
void pushClip(int logical_x, int logical_y, int logical_w, int logical_h);
|
||||||
|
void popClip();
|
||||||
|
|
||||||
// endFrame: flush del batch de líneas → composite postpro → submit + presenta.
|
// endFrame: flush del batch de líneas → composite postpro → submit + presenta.
|
||||||
void endFrame();
|
void endFrame();
|
||||||
|
|
||||||
@@ -168,6 +177,10 @@ namespace Rendering::GPU {
|
|||||||
std::vector<LineVertex> vertices_;
|
std::vector<LineVertex> vertices_;
|
||||||
std::vector<uint16_t> indices_;
|
std::vector<uint16_t> indices_;
|
||||||
|
|
||||||
|
// Pila d'scissors actius en píxels físics del offscreen. Buida = sense
|
||||||
|
// clip (full target). Cada push/pop fa un flushBatch i reaplica scissor.
|
||||||
|
std::vector<SDL_Rect> clip_stack_;
|
||||||
|
|
||||||
// Estado del frame en curso.
|
// Estado del frame en curso.
|
||||||
SDL_GPUCommandBuffer* cmd_buffer_{nullptr};
|
SDL_GPUCommandBuffer* cmd_buffer_{nullptr};
|
||||||
SDL_GPUTexture* swapchain_texture_{nullptr};
|
SDL_GPUTexture* swapchain_texture_{nullptr};
|
||||||
@@ -190,6 +203,7 @@ namespace Rendering::GPU {
|
|||||||
void bloomPass(); // pre-composite: H + V passes sobre les bloom textures
|
void bloomPass(); // pre-composite: H + V passes sobre les bloom textures
|
||||||
void compositePass();
|
void compositePass();
|
||||||
void applyFinalViewport();
|
void applyFinalViewport();
|
||||||
|
void applyCurrentScissor(); // re-aplica el top de clip_stack_ al render_pass_
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Rendering::GPU
|
} // namespace Rendering::GPU
|
||||||
|
|||||||
@@ -8,11 +8,6 @@
|
|||||||
|
|
||||||
namespace Rendering {
|
namespace Rendering {
|
||||||
|
|
||||||
// Color global compartido para líneas sin paleta propia (HUD, debug, texto
|
|
||||||
// genérico). Equivale al "color máximo" de la antigua oscilación CPU: verde
|
|
||||||
// fósforo CRT. El pulso de brillo lo aplica ahora el shader de postpro.
|
|
||||||
SDL_Color g_current_line_color = {100, 255, 100, 255};
|
|
||||||
|
|
||||||
// Grosor global por defecto. Configurable via setLineThickness.
|
// Grosor global por defecto. Configurable via setLineThickness.
|
||||||
float g_current_line_thickness = Defaults::Rendering::LINE_THICKNESS_DEFAULT;
|
float g_current_line_thickness = Defaults::Rendering::LINE_THICKNESS_DEFAULT;
|
||||||
|
|
||||||
@@ -36,8 +31,8 @@ namespace Rendering {
|
|||||||
const auto FX2 = static_cast<float>(x2);
|
const auto FX2 = static_cast<float>(x2);
|
||||||
const auto FY2 = static_cast<float>(y2);
|
const auto FY2 = static_cast<float>(y2);
|
||||||
|
|
||||||
// color.alpha==0 → usar color global (verde fósforo). alpha>0 → color directo.
|
// color.alpha==0 → fallback a DEFAULT_LINE_COLOR (verd fòsfor). alpha>0 → color directo.
|
||||||
const SDL_Color SOURCE = (color.a > 0) ? color : g_current_line_color;
|
const SDL_Color SOURCE = (color.a > 0) ? color : DEFAULT_LINE_COLOR;
|
||||||
const float R = (static_cast<float>(SOURCE.r) * brightness) / 255.0F;
|
const float R = (static_cast<float>(SOURCE.r) * brightness) / 255.0F;
|
||||||
const float G = (static_cast<float>(SOURCE.g) * brightness) / 255.0F;
|
const float G = (static_cast<float>(SOURCE.g) * brightness) / 255.0F;
|
||||||
const float B = (static_cast<float>(SOURCE.b) * brightness) / 255.0F;
|
const float B = (static_cast<float>(SOURCE.b) * brightness) / 255.0F;
|
||||||
@@ -68,10 +63,6 @@ namespace Rendering {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setLineColor(SDL_Color color) { g_current_line_color = color; }
|
|
||||||
|
|
||||||
auto getLineColor() -> SDL_Color { return g_current_line_color; }
|
|
||||||
|
|
||||||
void setLineThickness(float thickness) {
|
void setLineThickness(float thickness) {
|
||||||
if (thickness > 0.0F) {
|
if (thickness > 0.0F) {
|
||||||
g_current_line_thickness = thickness;
|
g_current_line_thickness = thickness;
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
//
|
//
|
||||||
// El dibujo de líneas pasa por el pipeline GPU. Las coordenadas (x1,y1,x2,y2)
|
// El dibujo de líneas pasa por el pipeline GPU. Las coordenadas (x1,y1,x2,y2)
|
||||||
// son lógicas (1280×720); el shader las mapea a NDC y el viewport del SDLManager
|
// son lógicas (1280×720); el shader las mapea a NDC y el viewport del SDLManager
|
||||||
// hace el letterbox a píxeles físicos. El brillo modula el color global de
|
// hace el letterbox a píxeles físicos. El pulse de brillo lo aplica el shader
|
||||||
// línea (lo gestiona ColorOscillator). El grosor es configurable por línea
|
// de postpro (ya no hi ha un ColorOscillator a CPU). El grosor es configurable
|
||||||
// (parámetro thickness>0) o global (g_current_line_thickness vía setLineThickness).
|
// per línia (parámetro thickness>0) o global (g_current_line_thickness vía
|
||||||
|
// setLineThickness).
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
@@ -15,12 +16,17 @@
|
|||||||
|
|
||||||
namespace Rendering {
|
namespace Rendering {
|
||||||
|
|
||||||
|
// Color verd fòsfor CRT per defecte: s'usa quan el caller passa color amb
|
||||||
|
// alpha==0 (sentinella "sense color propi"). Constant immutable: la
|
||||||
|
// semàntica de "color global" ja no existeix (era de l'antic ColorOscillator).
|
||||||
|
constexpr SDL_Color DEFAULT_LINE_COLOR = {.r = 100, .g = 255, .b = 100, .a = 255};
|
||||||
|
|
||||||
// Dibuja una línea entre dos puntos en coordenadas lógicas (1280×720).
|
// Dibuja una línea entre dos puntos en coordenadas lógicas (1280×720).
|
||||||
// brightness: factor de brillo (0.0..1.0, default 1.0 = brillo máximo).
|
// brightness: factor de brillo (0.0..1.0, default 1.0 = brillo máximo).
|
||||||
// Pre-multiplica el RGB del color (color dim sobre fons negre).
|
// Pre-multiplica el RGB del color (color dim sobre fons negre).
|
||||||
// thickness: grosor en píxeles lógicos. Si <= 0 usa g_current_line_thickness.
|
// thickness: grosor en píxeles lógicos. Si <= 0 usa g_current_line_thickness.
|
||||||
// color: si alpha==0, se usa el color global del oscilador; si alpha>0 se
|
// color: si alpha==0, se usa DEFAULT_LINE_COLOR (verd fòsfor fallback);
|
||||||
// usa este color directo (paleta semántica por entidad).
|
// si alpha>0 se usa este color directo (paleta semántica por entidad).
|
||||||
// alpha: alpha que arriba al GPU (default 1.0 = opac, behavior original).
|
// alpha: alpha que arriba al GPU (default 1.0 = opac, behavior original).
|
||||||
// Valors <1.0 fan que la línia es barregi de veritat sobre el dest
|
// Valors <1.0 fan que la línia es barregi de veritat sobre el dest
|
||||||
// en comptes de sobrepintar-lo (útil per halos translúcids).
|
// en comptes de sobrepintar-lo (útil per halos translúcids).
|
||||||
@@ -49,10 +55,6 @@ namespace Rendering {
|
|||||||
SDL_Color color = {0, 0, 0, 0},
|
SDL_Color color = {0, 0, 0, 0},
|
||||||
SDL_Color glow_color = {0, 0, 0, 0});
|
SDL_Color glow_color = {0, 0, 0, 0});
|
||||||
|
|
||||||
// Color global de las líneas (lo actualiza ColorOscillator vía SDLManager).
|
|
||||||
void setLineColor(SDL_Color color);
|
|
||||||
[[nodiscard]] auto getLineColor() -> SDL_Color;
|
|
||||||
|
|
||||||
// Grosor global por defecto (en píxeles lógicos). Default: 1.5.
|
// Grosor global por defecto (en píxeles lógicos). Default: 1.5.
|
||||||
void setLineThickness(float thickness);
|
void setLineThickness(float thickness);
|
||||||
[[nodiscard]] auto getLineThickness() -> float;
|
[[nodiscard]] auto getLineThickness() -> float;
|
||||||
|
|||||||
@@ -385,6 +385,26 @@ void SDLManager::toggleAntialias() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SDLManager::setRenderResolution(int w, int h) {
|
||||||
|
if (!Defaults::Rendering::isValidRenderResolution(w, h)) {
|
||||||
|
std::cerr << "[SDLManager] Resolucio no valida (" << w << "x" << h
|
||||||
|
<< "), ignorant.\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (w == cfg_->rendering.render_width && h == cfg_->rendering.render_height) {
|
||||||
|
return; // ja era l'actual
|
||||||
|
}
|
||||||
|
if (!gpu_renderer_.resizeRenderTarget(static_cast<float>(w), static_cast<float>(h))) {
|
||||||
|
std::cerr << "[SDLManager] resizeRenderTarget ha fallat.\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cfg_->rendering.render_width = w;
|
||||||
|
cfg_->rendering.render_height = h;
|
||||||
|
if (on_persist_) {
|
||||||
|
on_persist_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SDLManager::togglePostFx() {
|
void SDLManager::togglePostFx() {
|
||||||
const bool NEW_STATE = !gpu_renderer_.isPostFxEnabled();
|
const bool NEW_STATE = !gpu_renderer_.isPostFxEnabled();
|
||||||
gpu_renderer_.setPostFxEnabled(NEW_STATE);
|
gpu_renderer_.setPostFxEnabled(NEW_STATE);
|
||||||
|
|||||||
@@ -30,12 +30,16 @@ class SDLManager {
|
|||||||
auto operator=(const SDLManager&) -> SDLManager& = delete;
|
auto operator=(const SDLManager&) -> SDLManager& = delete;
|
||||||
|
|
||||||
// [NUEVO] Gestió de finestra dinàmica
|
// [NUEVO] Gestió de finestra dinàmica
|
||||||
void increaseWindowSize(); // F2: +100px
|
void increaseWindowSize(); // F2: +100px
|
||||||
void decreaseWindowSize(); // F1: -100px
|
void decreaseWindowSize(); // F1: -100px
|
||||||
void toggleFullscreen(); // F3
|
void toggleFullscreen(); // F3
|
||||||
void toggleVSync(); // F4
|
void toggleVSync(); // F4
|
||||||
void toggleAntialias(); // F5
|
void toggleAntialias(); // F5
|
||||||
void togglePostFx(); // F6
|
void togglePostFx(); // F6
|
||||||
|
// Canvia la resolució del render target offscreen (recrea la textura).
|
||||||
|
// Cal cridar-lo fora d'un frame (event phase, no draw phase). Si el
|
||||||
|
// valor no es un preset valid o ja es l'actual, es no-op.
|
||||||
|
void setRenderResolution(int w, int h);
|
||||||
auto handleWindowEvent(const SDL_Event& event) -> bool; // Per a SDL_EVENT_WINDOW_RESIZED
|
auto handleWindowEvent(const SDL_Event& event) -> bool; // Per a SDL_EVENT_WINDOW_RESIZED
|
||||||
|
|
||||||
// Funciones principals (renderizado).
|
// Funciones principals (renderizado).
|
||||||
@@ -47,6 +51,8 @@ class SDLManager {
|
|||||||
// Getters
|
// Getters
|
||||||
auto getRenderer() -> Rendering::Renderer* { return &gpu_renderer_; }
|
auto getRenderer() -> Rendering::Renderer* { return &gpu_renderer_; }
|
||||||
[[nodiscard]] auto getScaleFactor() const -> float { return zoom_factor_; }
|
[[nodiscard]] auto getScaleFactor() const -> float { return zoom_factor_; }
|
||||||
|
[[nodiscard]] auto isFullscreen() const -> bool { return is_fullscreen_; }
|
||||||
|
[[nodiscard]] auto isPostFxEnabled() const -> bool { return gpu_renderer_.isPostFxEnabled(); }
|
||||||
|
|
||||||
// [NUEVO] Actualitzar context de renderizado (factor de scale global)
|
// [NUEVO] Actualitzar context de renderizado (factor de scale global)
|
||||||
void updateRenderingContext() const;
|
void updateRenderingContext() const;
|
||||||
|
|||||||
@@ -9,72 +9,77 @@
|
|||||||
|
|
||||||
namespace Resource::Helper {
|
namespace Resource::Helper {
|
||||||
|
|
||||||
// Inicialitzar el sistema de recursos
|
// Inicialitzar el sistema de recursos
|
||||||
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool {
|
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool {
|
||||||
return Loader::get().initialize(pack_file, fallback);
|
return Loader::get().initialize(pack_file, fallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carregar un file
|
// Carregar un file
|
||||||
auto loadFile(const std::string& filepath) -> std::vector<uint8_t> {
|
auto loadFile(const std::string& filepath) -> std::vector<uint8_t> {
|
||||||
// Normalitzar la ruta
|
// Normalitzar la ruta
|
||||||
std::string normalized = normalizePath(filepath);
|
std::string normalized = normalizePath(filepath);
|
||||||
|
|
||||||
// Carregar del sistema de recursos
|
// Carregar del sistema de recursos
|
||||||
return Loader::get().loadResource(normalized);
|
return Loader::get().loadResource(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comprovar si existeix un file
|
// Llistar recursos amb un prefix donat
|
||||||
auto fileExists(const std::string& filepath) -> bool {
|
auto listResources(const std::string& prefix) -> std::vector<std::string> {
|
||||||
std::string normalized = normalizePath(filepath);
|
return Loader::get().listResources(prefix);
|
||||||
return Loader::get().resourceExists(normalized);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Obtenir ruta normalitzada per al paquet
|
// Comprovar si existeix un file
|
||||||
// Elimina prefixos "data/", rutes absolutes, etc.
|
auto fileExists(const std::string& filepath) -> bool {
|
||||||
auto getPackPath(const std::string& asset_path) -> std::string {
|
std::string normalized = normalizePath(filepath);
|
||||||
std::string path = asset_path;
|
return Loader::get().resourceExists(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
// Eliminar rutes absolutes (detectar / o C:\ al principi)
|
// Obtenir ruta normalitzada per al paquet
|
||||||
if (!path.empty() && path[0] == '/') {
|
// Elimina prefixos "data/", rutes absolutes, etc.
|
||||||
// Buscar "data/" i agafar el que ve después
|
auto getPackPath(const std::string& asset_path) -> std::string {
|
||||||
size_t data_pos = path.find("/data/");
|
std::string path = asset_path;
|
||||||
if (data_pos != std::string::npos) {
|
|
||||||
path = path.substr(data_pos + 6); // Saltar "/data/"
|
// Eliminar rutes absolutes (detectar / o C:\ al principi)
|
||||||
|
if (!path.empty() && path[0] == '/') {
|
||||||
|
// Buscar "data/" i agafar el que ve después
|
||||||
|
size_t data_pos = path.find("/data/");
|
||||||
|
if (data_pos != std::string::npos) {
|
||||||
|
path = path.substr(data_pos + 6); // Saltar "/data/"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Eliminar "./" i "../" del principi
|
||||||
|
while (path.starts_with("./")) {
|
||||||
|
path = path.substr(2);
|
||||||
|
}
|
||||||
|
while (path.starts_with("../")) {
|
||||||
|
path = path.substr(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar "data/" del principi
|
||||||
|
if (path.starts_with("data/")) {
|
||||||
|
path = path.substr(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar "Resources/" (macOS bundles)
|
||||||
|
if (path.starts_with("Resources/")) {
|
||||||
|
path = path.substr(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convertir barres invertides a normals
|
||||||
|
std::ranges::replace(path, '\\', '/');
|
||||||
|
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eliminar "./" i "../" del principi
|
// Normalitzar ruta (alias de getPackPath)
|
||||||
while (path.starts_with("./")) {
|
auto normalizePath(const std::string& path) -> std::string {
|
||||||
path = path.substr(2);
|
return getPackPath(path);
|
||||||
}
|
|
||||||
while (path.starts_with("../")) {
|
|
||||||
path = path.substr(3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eliminar "data/" del principi
|
// Comprovar si hay paquet carregat
|
||||||
if (path.starts_with("data/")) {
|
auto isPackLoaded() -> bool {
|
||||||
path = path.substr(5);
|
return Loader::get().isPackLoaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eliminar "Resources/" (macOS bundles)
|
|
||||||
if (path.starts_with("Resources/")) {
|
|
||||||
path = path.substr(10);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convertir barres invertides a normals
|
|
||||||
std::ranges::replace(path, '\\', '/');
|
|
||||||
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalitzar ruta (alias de getPackPath)
|
|
||||||
auto normalizePath(const std::string& path) -> std::string {
|
|
||||||
return getPackPath(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprovar si hay paquet carregat
|
|
||||||
auto isPackLoaded() -> bool {
|
|
||||||
return Loader::get().isPackLoaded();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Resource::Helper
|
} // namespace Resource::Helper
|
||||||
|
|||||||
@@ -10,18 +10,21 @@
|
|||||||
|
|
||||||
namespace Resource::Helper {
|
namespace Resource::Helper {
|
||||||
|
|
||||||
// Inicialización del sistema
|
// Inicialización del sistema
|
||||||
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool;
|
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool;
|
||||||
|
|
||||||
// Càrrega de archivos
|
// Càrrega de archivos
|
||||||
auto loadFile(const std::string& filepath) -> std::vector<uint8_t>;
|
auto loadFile(const std::string& filepath) -> std::vector<uint8_t>;
|
||||||
auto fileExists(const std::string& filepath) -> bool;
|
auto fileExists(const std::string& filepath) -> bool;
|
||||||
|
|
||||||
// Normalització de rutes
|
// Llistat de recursos disponibles amb un prefix (ex. "shapes/", "sounds/").
|
||||||
auto getPackPath(const std::string& asset_path) -> std::string;
|
auto listResources(const std::string& prefix) -> std::vector<std::string>;
|
||||||
auto normalizePath(const std::string& path) -> std::string;
|
|
||||||
|
|
||||||
// Estat
|
// Normalització de rutes
|
||||||
auto isPackLoaded() -> bool;
|
auto getPackPath(const std::string& asset_path) -> std::string;
|
||||||
|
auto normalizePath(const std::string& path) -> std::string;
|
||||||
|
|
||||||
|
// Estat
|
||||||
|
auto isPackLoaded() -> bool;
|
||||||
|
|
||||||
} // namespace Resource::Helper
|
} // namespace Resource::Helper
|
||||||
|
|||||||
@@ -3,141 +3,178 @@
|
|||||||
|
|
||||||
#include "resource_loader.hpp"
|
#include "resource_loader.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
namespace Resource {
|
namespace Resource {
|
||||||
|
|
||||||
// Singleton
|
// Singleton
|
||||||
auto Loader::get() -> Loader& {
|
auto Loader::get() -> Loader& {
|
||||||
static Loader instance_;
|
static Loader instance_;
|
||||||
return instance_;
|
return instance_;
|
||||||
}
|
|
||||||
|
|
||||||
// Inicialitzar el sistema de recursos
|
|
||||||
auto Loader::initialize(const std::string& pack_file, bool enable_fallback) -> bool {
|
|
||||||
fallback_enabled_ = enable_fallback;
|
|
||||||
|
|
||||||
// Intentar load el paquet
|
|
||||||
pack_ = std::make_unique<Pack>();
|
|
||||||
|
|
||||||
if (!pack_->loadPack(pack_file)) {
|
|
||||||
if (!fallback_enabled_) {
|
|
||||||
std::cerr << "[ResourceLoader] ERROR FATAL: No es pot load " << pack_file
|
|
||||||
<< " y el fallback está desactivat\n";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "[ResourceLoader] Paquet no trobat, usant fallback al sistema de archivos\n";
|
|
||||||
pack_.reset(); // No hay paquet
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "[ResourceLoader] Paquet carregat: " << pack_file << "\n";
|
// Inicialitzar el sistema de recursos
|
||||||
return true;
|
auto Loader::initialize(const std::string& pack_file, bool enable_fallback) -> bool {
|
||||||
}
|
fallback_enabled_ = enable_fallback;
|
||||||
|
|
||||||
// Carregar un recurs
|
// Intentar load el paquet
|
||||||
auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
|
pack_ = std::make_unique<Pack>();
|
||||||
// Intentar load del paquet primer
|
|
||||||
if (pack_) {
|
if (!pack_->loadPack(pack_file)) {
|
||||||
if (pack_->hasResource(filename)) {
|
if (!fallback_enabled_) {
|
||||||
auto data = pack_->getResource(filename);
|
std::cerr << "[ResourceLoader] ERROR FATAL: No es pot load " << pack_file
|
||||||
if (!data.empty()) {
|
<< " y el fallback está desactivat\n";
|
||||||
return data;
|
return false;
|
||||||
}
|
}
|
||||||
std::cerr << "[ResourceLoader] Advertència: recurs buit al paquet: " << filename
|
|
||||||
<< "\n";
|
std::cout << "[ResourceLoader] Paquet no trobat, usant fallback al sistema de archivos\n";
|
||||||
|
pack_.reset(); // No hay paquet
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si no está al paquet y no hay fallback, falla
|
std::cout << "[ResourceLoader] Paquet carregat: " << pack_file << "\n";
|
||||||
if (!fallback_enabled_) {
|
|
||||||
std::cerr << "[ResourceLoader] ERROR: Recurs no trobat al paquet i fallback desactivat: "
|
|
||||||
<< filename << "\n";
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback al sistema de archivos
|
|
||||||
if (fallback_enabled_) {
|
|
||||||
return loadFromFilesystem(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprovar si existeix un recurs
|
|
||||||
auto Loader::resourceExists(const std::string& filename) -> bool {
|
|
||||||
// Comprovar al paquet
|
|
||||||
if (pack_ && pack_->hasResource(filename)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comprovar al sistema de archivos si está activat el fallback
|
// Carregar un recurs
|
||||||
if (fallback_enabled_) {
|
auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
|
||||||
std::string fullpath = base_path_.empty() ? "data/" + filename : base_path_ + "/data/" + filename;
|
// Intentar load del paquet primer
|
||||||
return std::filesystem::exists(fullpath);
|
if (pack_) {
|
||||||
|
if (pack_->hasResource(filename)) {
|
||||||
|
auto data = pack_->getResource(filename);
|
||||||
|
if (!data.empty()) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
std::cerr << "[ResourceLoader] Advertència: recurs buit al paquet: " << filename
|
||||||
|
<< "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si no está al paquet y no hay fallback, falla
|
||||||
|
if (!fallback_enabled_) {
|
||||||
|
std::cerr << "[ResourceLoader] ERROR: Recurs no trobat al paquet i fallback desactivat: "
|
||||||
|
<< filename << "\n";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback al sistema de archivos
|
||||||
|
if (fallback_enabled_) {
|
||||||
|
return loadFromFilesystem(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
auto Loader::listResources(const std::string& prefix) -> std::vector<std::string> {
|
||||||
}
|
std::vector<std::string> result;
|
||||||
|
|
||||||
|
if (pack_) {
|
||||||
|
for (const auto& path : pack_->getResourceList()) {
|
||||||
|
if (path.starts_with(prefix)) {
|
||||||
|
result.push_back(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fallback_enabled_) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string root = base_path_.empty() ? "data/" + prefix : base_path_ + "/data/" + prefix;
|
||||||
|
if (!std::filesystem::exists(root)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& entry : std::filesystem::recursive_directory_iterator(root)) {
|
||||||
|
if (!entry.is_regular_file()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::string full = entry.path().generic_string();
|
||||||
|
if (auto pos = full.find("/data/"); pos != std::string::npos) {
|
||||||
|
result.push_back(full.substr(pos + 6));
|
||||||
|
} else if (full.starts_with("data/")) {
|
||||||
|
result.push_back(full.substr(5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::ranges::sort(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprovar si existeix un recurs
|
||||||
|
auto Loader::resourceExists(const std::string& filename) -> bool {
|
||||||
|
// Comprovar al paquet
|
||||||
|
if (pack_ && pack_->hasResource(filename)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comprovar al sistema de archivos si está activat el fallback
|
||||||
|
if (fallback_enabled_) {
|
||||||
|
std::string fullpath = base_path_.empty() ? "data/" + filename : base_path_ + "/data/" + filename;
|
||||||
|
return std::filesystem::exists(fullpath);
|
||||||
|
}
|
||||||
|
|
||||||
// Validar el paquet
|
|
||||||
auto Loader::validatePack() -> bool {
|
|
||||||
if (!pack_) {
|
|
||||||
std::cerr << "[ResourceLoader] Advertència: no hay paquet carregat per validar\n";
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return pack_->validatePack();
|
// Validar el paquet
|
||||||
}
|
auto Loader::validatePack() -> bool {
|
||||||
|
if (!pack_) {
|
||||||
|
std::cerr << "[ResourceLoader] Advertència: no hay paquet carregat per validar\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Comprovar si hay paquet carregat
|
return pack_->validatePack();
|
||||||
auto Loader::isPackLoaded() const -> bool {
|
|
||||||
return pack_ != nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establir la ruta base
|
|
||||||
void Loader::setBasePath(const std::string& path) {
|
|
||||||
base_path_ = path;
|
|
||||||
std::cout << "[ResourceLoader] Ruta base establerta: " << base_path_ << "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtenir la ruta base
|
|
||||||
auto Loader::getBasePath() const -> const std::string& {
|
|
||||||
return base_path_;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Carregar des del sistema de archivos (fallback)
|
|
||||||
auto Loader::loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t> {
|
|
||||||
// The filename is already normalized (e.g., "shapes/logo/letra_j.shp")
|
|
||||||
// We need to prepend base_path + "data/"
|
|
||||||
std::string fullpath;
|
|
||||||
|
|
||||||
if (base_path_.empty()) {
|
|
||||||
fullpath = "data/" + filename;
|
|
||||||
} else {
|
|
||||||
fullpath = base_path_ + "/data/" + filename;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::ifstream file(fullpath, std::ios::binary | std::ios::ate);
|
// Comprovar si hay paquet carregat
|
||||||
if (!file) {
|
auto Loader::isPackLoaded() const -> bool {
|
||||||
std::cerr << "[ResourceLoader] Error: no es pot obrir " << fullpath << "\n";
|
return pack_ != nullptr;
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::streamsize file_size = file.tellg();
|
// Establir la ruta base
|
||||||
file.seekg(0, std::ios::beg);
|
void Loader::setBasePath(const std::string& path) {
|
||||||
|
base_path_ = path;
|
||||||
std::vector<uint8_t> data(file_size);
|
std::cout << "[ResourceLoader] Ruta base establerta: " << base_path_ << "\n";
|
||||||
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
|
|
||||||
std::cerr << "[ResourceLoader] Error: no es pot llegir " << fullpath << "\n";
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "[ResourceLoader] Carregat des del sistema de archivos: " << fullpath << "\n";
|
// Obtenir la ruta base
|
||||||
return data;
|
auto Loader::getBasePath() const -> const std::string& {
|
||||||
}
|
return base_path_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Carregar des del sistema de archivos (fallback)
|
||||||
|
auto Loader::loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t> {
|
||||||
|
// The filename is already normalized (e.g., "shapes/logo/letra_j.shp")
|
||||||
|
// We need to prepend base_path + "data/"
|
||||||
|
std::string fullpath;
|
||||||
|
|
||||||
|
if (base_path_.empty()) {
|
||||||
|
fullpath = "data/" + filename;
|
||||||
|
} else {
|
||||||
|
fullpath = base_path_ + "/data/" + filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ifstream file(fullpath, std::ios::binary | std::ios::ate);
|
||||||
|
if (!file) {
|
||||||
|
std::cerr << "[ResourceLoader] Error: no es pot obrir " << fullpath << "\n";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::streamsize file_size = file.tellg();
|
||||||
|
file.seekg(0, std::ios::beg);
|
||||||
|
|
||||||
|
std::vector<uint8_t> data(file_size);
|
||||||
|
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
|
||||||
|
std::cerr << "[ResourceLoader] Error: no es pot llegir " << fullpath << "\n";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "[ResourceLoader] Carregat des del sistema de archivos: " << fullpath << "\n";
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Resource
|
} // namespace Resource
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
|
|
||||||
namespace Resource {
|
namespace Resource {
|
||||||
|
|
||||||
// Singleton per gestionar la càrrega de recursos
|
// Singleton per gestionar la càrrega de recursos
|
||||||
class Loader {
|
class Loader {
|
||||||
public:
|
public:
|
||||||
// Singleton
|
// Singleton
|
||||||
static auto get() -> Loader&;
|
static auto get() -> Loader&;
|
||||||
|
|
||||||
@@ -25,6 +25,11 @@ class Loader {
|
|||||||
auto loadResource(const std::string& filename) -> std::vector<uint8_t>;
|
auto loadResource(const std::string& filename) -> std::vector<uint8_t>;
|
||||||
auto resourceExists(const std::string& filename) -> bool;
|
auto resourceExists(const std::string& filename) -> bool;
|
||||||
|
|
||||||
|
// Llistat de recursos amb prefix (ex. "shapes/", "sounds/"). Si hi ha
|
||||||
|
// pack, retorna els fitxers del pack filtrats; si no, escaneja el
|
||||||
|
// sistema de fitxers recursivament a `data/<prefix>`.
|
||||||
|
auto listResources(const std::string& prefix) -> std::vector<std::string>;
|
||||||
|
|
||||||
// Validació
|
// Validació
|
||||||
auto validatePack() -> bool;
|
auto validatePack() -> bool;
|
||||||
[[nodiscard]] auto isPackLoaded() const -> bool;
|
[[nodiscard]] auto isPackLoaded() const -> bool;
|
||||||
@@ -37,7 +42,7 @@ class Loader {
|
|||||||
Loader(const Loader&) = delete;
|
Loader(const Loader&) = delete;
|
||||||
auto operator=(const Loader&) -> Loader& = delete;
|
auto operator=(const Loader&) -> Loader& = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Loader() = default;
|
Loader() = default;
|
||||||
~Loader() = default;
|
~Loader() = default;
|
||||||
|
|
||||||
@@ -48,6 +53,6 @@ class Loader {
|
|||||||
|
|
||||||
// Funciones auxiliars
|
// Funciones auxiliars
|
||||||
auto loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t>;
|
auto loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t>;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Resource
|
} // namespace Resource
|
||||||
|
|||||||
@@ -10,300 +10,287 @@
|
|||||||
|
|
||||||
namespace Resource {
|
namespace Resource {
|
||||||
|
|
||||||
// Calcular checksum CRC32 simplificat
|
// Calcular checksum CRC32 simplificat
|
||||||
auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
|
auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
|
||||||
uint32_t checksum = 0x12345678;
|
uint32_t checksum = 0x12345678;
|
||||||
for (unsigned char byte : data) {
|
for (unsigned char byte : data) {
|
||||||
checksum = ((checksum << 5) + checksum) + byte;
|
checksum = ((checksum << 5) + checksum) + byte;
|
||||||
}
|
}
|
||||||
return checksum;
|
return checksum;
|
||||||
}
|
|
||||||
|
|
||||||
// Encriptació XOR (simètrica)
|
|
||||||
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
|
|
||||||
if (key.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (size_t i = 0; i < data.size(); ++i) {
|
|
||||||
data[i] ^= key[i % key.length()];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
|
|
||||||
// XOR es simètric
|
|
||||||
encryptData(data, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Llegir file complet a memòria
|
|
||||||
auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> {
|
|
||||||
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
|
|
||||||
if (!file) {
|
|
||||||
std::cerr << "[ResourcePack] Error: no es pot obrir " << filepath << '\n';
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::streamsize file_size = file.tellg();
|
// Encriptació XOR (simètrica)
|
||||||
file.seekg(0, std::ios::beg);
|
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
|
||||||
|
if (key.empty()) {
|
||||||
std::vector<uint8_t> data(file_size);
|
return;
|
||||||
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
|
}
|
||||||
std::cerr << "[ResourcePack] Error: no es pot llegir " << filepath << '\n';
|
for (size_t i = 0; i < data.size(); ++i) {
|
||||||
return {};
|
data[i] ^= key[i % key.length()];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
|
||||||
}
|
// XOR es simètric
|
||||||
|
encryptData(data, key);
|
||||||
// Añadir un file individual al paquet
|
|
||||||
auto Pack::addFile(const std::string& filepath, const std::string& pack_name) -> bool {
|
|
||||||
auto file_data = readFile(filepath);
|
|
||||||
if (file_data.empty()) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ResourceEntry entry{
|
// Llegir file complet a memòria
|
||||||
.filename = pack_name,
|
auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> {
|
||||||
.offset = data_.size(),
|
std::ifstream file(filepath, std::ios::binary | std::ios::ate);
|
||||||
.size = file_data.size(),
|
if (!file) {
|
||||||
.checksum = calculateChecksum(file_data)};
|
std::cerr << "[ResourcePack] Error: no es pot obrir " << filepath << '\n';
|
||||||
|
return {};
|
||||||
// Añadir dades al bloc de dades
|
|
||||||
data_.insert(data_.end(), file_data.begin(), file_data.end());
|
|
||||||
|
|
||||||
resources_[pack_name] = entry;
|
|
||||||
|
|
||||||
std::cout << "[ResourcePack] Añadido: " << pack_name << " (" << file_data.size()
|
|
||||||
<< " bytes)\n";
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Añadir todos los archivos de un directori recursivament
|
|
||||||
auto Pack::addDirectory(const std::string& dir_path,
|
|
||||||
const std::string& base_path) -> bool {
|
|
||||||
namespace fs = std::filesystem;
|
|
||||||
|
|
||||||
if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
|
|
||||||
std::cerr << "[ResourcePack] Error: directori no trobat: " << dir_path << '\n';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string current_base = base_path.empty() ? "" : base_path + "/";
|
|
||||||
|
|
||||||
for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
|
|
||||||
if (!entry.is_regular_file()) {
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string full_path = entry.path().string();
|
std::streamsize file_size = file.tellg();
|
||||||
std::string relative_path = entry.path().lexically_relative(dir_path).string();
|
file.seekg(0, std::ios::beg);
|
||||||
|
|
||||||
// Convertir barres invertides a normals (Windows)
|
std::vector<uint8_t> data(file_size);
|
||||||
std::ranges::replace(relative_path, '\\', '/');
|
if (!file.read(reinterpret_cast<char*>(data.data()), file_size)) {
|
||||||
|
std::cerr << "[ResourcePack] Error: no es pot llegir " << filepath << '\n';
|
||||||
// Saltar archivos de desenvolupament
|
return {};
|
||||||
if (relative_path.find(".world") != std::string::npos ||
|
|
||||||
relative_path.find(".tsx") != std::string::npos ||
|
|
||||||
relative_path.find(".DS_Store") != std::string::npos ||
|
|
||||||
relative_path.find(".git") != std::string::npos) {
|
|
||||||
std::cout << "[ResourcePack] Saltant: " << relative_path << '\n';
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string pack_name = current_base + relative_path;
|
return data;
|
||||||
addFile(full_path, pack_name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
// Añadir un file individual al paquet
|
||||||
}
|
auto Pack::addFile(const std::string& filepath, const std::string& pack_name) -> bool {
|
||||||
|
auto file_data = readFile(filepath);
|
||||||
|
if (file_data.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Guardar paquet a disc
|
ResourceEntry entry{
|
||||||
auto Pack::savePack(const std::string& pack_file) -> bool {
|
.filename = pack_name,
|
||||||
std::ofstream file(pack_file, std::ios::binary);
|
.offset = data_.size(),
|
||||||
if (!file) {
|
.size = file_data.size(),
|
||||||
std::cerr << "[ResourcePack] Error: no es pot crear " << pack_file << '\n';
|
.checksum = calculateChecksum(file_data)};
|
||||||
return false;
|
|
||||||
|
// Añadir dades al bloc de dades
|
||||||
|
data_.insert(data_.end(), file_data.begin(), file_data.end());
|
||||||
|
|
||||||
|
resources_[pack_name] = entry;
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escriure capçalera
|
// Añadir todos los archivos de un directori recursivament
|
||||||
file.write(MAGIC_HEADER, 4);
|
auto Pack::addDirectory(const std::string& dir_path,
|
||||||
file.write(reinterpret_cast<const char*>(&VERSION), sizeof(VERSION));
|
const std::string& base_path) -> bool {
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
// Escriure nombre de recursos
|
if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
|
||||||
auto resource_count = static_cast<uint32_t>(resources_.size());
|
std::cerr << "[ResourcePack] Error: directori no trobat: " << dir_path << '\n';
|
||||||
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Escriure metadades de recursos
|
std::string current_base = base_path.empty() ? "" : base_path + "/";
|
||||||
for (const auto& [name, entry] : resources_) {
|
|
||||||
// Nom del file
|
|
||||||
auto name_len = static_cast<uint32_t>(entry.filename.length());
|
|
||||||
file.write(reinterpret_cast<const char*>(&name_len), sizeof(name_len));
|
|
||||||
file.write(entry.filename.c_str(), name_len);
|
|
||||||
|
|
||||||
// Offset, mida, checksum
|
for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
|
||||||
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
|
if (!entry.is_regular_file()) {
|
||||||
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
|
continue;
|
||||||
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
|
}
|
||||||
|
|
||||||
|
std::string full_path = entry.path().string();
|
||||||
|
std::string relative_path = entry.path().lexically_relative(dir_path).string();
|
||||||
|
|
||||||
|
// Convertir barres invertides a normals (Windows)
|
||||||
|
std::ranges::replace(relative_path, '\\', '/');
|
||||||
|
|
||||||
|
// Saltar archivos de desenvolupament
|
||||||
|
if (relative_path.find(".world") != std::string::npos ||
|
||||||
|
relative_path.find(".tsx") != std::string::npos ||
|
||||||
|
relative_path.find(".DS_Store") != std::string::npos ||
|
||||||
|
relative_path.find(".git") != std::string::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string pack_name = current_base + relative_path;
|
||||||
|
addFile(full_path, pack_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encriptar dades
|
// Guardar paquet a disc
|
||||||
std::vector<uint8_t> encrypted_data = data_;
|
auto Pack::savePack(const std::string& pack_file) -> bool {
|
||||||
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
|
std::ofstream file(pack_file, std::ios::binary);
|
||||||
|
if (!file) {
|
||||||
|
std::cerr << "[ResourcePack] Error: no es pot crear " << pack_file << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Escriure mida de dades y dades encriptades
|
// Escriure capçalera
|
||||||
auto data_size = static_cast<uint64_t>(encrypted_data.size());
|
file.write(MAGIC_HEADER, 4);
|
||||||
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
|
file.write(reinterpret_cast<const char*>(&VERSION), sizeof(VERSION));
|
||||||
file.write(reinterpret_cast<const char*>(encrypted_data.data()), encrypted_data.size());
|
|
||||||
|
|
||||||
std::cout << "[ResourcePack] Guardat: " << pack_file << " (" << resources_.size()
|
// Escriure nombre de recursos
|
||||||
<< " recursos, " << data_size << " bytes)\n";
|
auto resource_count = static_cast<uint32_t>(resources_.size());
|
||||||
|
file.write(reinterpret_cast<const char*>(&resource_count), sizeof(resource_count));
|
||||||
|
|
||||||
return true;
|
// Escriure metadades de recursos
|
||||||
}
|
for (const auto& [name, entry] : resources_) {
|
||||||
|
// Nom del file
|
||||||
|
auto name_len = static_cast<uint32_t>(entry.filename.length());
|
||||||
|
file.write(reinterpret_cast<const char*>(&name_len), sizeof(name_len));
|
||||||
|
file.write(entry.filename.c_str(), name_len);
|
||||||
|
|
||||||
// Carregar paquet desde disc
|
// Offset, mida, checksum
|
||||||
auto Pack::loadPack(const std::string& pack_file) -> bool {
|
file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(entry.offset));
|
||||||
std::ifstream file(pack_file, std::ios::binary);
|
file.write(reinterpret_cast<const char*>(&entry.size), sizeof(entry.size));
|
||||||
if (!file) {
|
file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(entry.checksum));
|
||||||
std::cerr << "[ResourcePack] Error: no es pot obrir " << pack_file << '\n';
|
}
|
||||||
return false;
|
|
||||||
|
// Encriptar dades
|
||||||
|
std::vector<uint8_t> encrypted_data = data_;
|
||||||
|
encryptData(encrypted_data, DEFAULT_ENCRYPT_KEY);
|
||||||
|
|
||||||
|
// Escriure mida de dades y dades encriptades
|
||||||
|
auto data_size = static_cast<uint64_t>(encrypted_data.size());
|
||||||
|
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
|
||||||
|
file.write(reinterpret_cast<const char*>(encrypted_data.data()), encrypted_data.size());
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Llegir capçalera
|
// Carregar paquet desde disc
|
||||||
char magic[4];
|
auto Pack::loadPack(const std::string& pack_file) -> bool {
|
||||||
file.read(magic, 4);
|
std::ifstream file(pack_file, std::ios::binary);
|
||||||
if (std::string(magic, 4) != MAGIC_HEADER) {
|
if (!file) {
|
||||||
std::cerr << "[ResourcePack] Error: capçalera invàlida (esperava " << MAGIC_HEADER
|
std::cerr << "[ResourcePack] Error: no es pot obrir " << pack_file << '\n';
|
||||||
<< ")\n";
|
return false;
|
||||||
return false;
|
}
|
||||||
|
|
||||||
|
// Llegir capçalera
|
||||||
|
char magic[4];
|
||||||
|
file.read(magic, 4);
|
||||||
|
if (std::string(magic, 4) != MAGIC_HEADER) {
|
||||||
|
std::cerr << "[ResourcePack] Error: capçalera invàlida (esperava " << MAGIC_HEADER
|
||||||
|
<< ")\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t version;
|
||||||
|
file.read(reinterpret_cast<char*>(&version), sizeof(version));
|
||||||
|
if (version != VERSION) {
|
||||||
|
std::cerr << "[ResourcePack] Error: versión incompatible (esperava " << VERSION
|
||||||
|
<< ", trobat " << version << ")\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llegir nombre de recursos
|
||||||
|
uint32_t resource_count;
|
||||||
|
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
|
||||||
|
|
||||||
|
// Llegir metadades de recursos
|
||||||
|
resources_.clear();
|
||||||
|
for (uint32_t i = 0; i < resource_count; ++i) {
|
||||||
|
// Nom del file
|
||||||
|
uint32_t name_len;
|
||||||
|
file.read(reinterpret_cast<char*>(&name_len), sizeof(name_len));
|
||||||
|
|
||||||
|
std::string filename(name_len, '\0');
|
||||||
|
file.read(filename.data(), name_len);
|
||||||
|
|
||||||
|
// Offset, mida, checksum
|
||||||
|
ResourceEntry entry;
|
||||||
|
entry.filename = filename;
|
||||||
|
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
|
||||||
|
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
|
||||||
|
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
|
||||||
|
|
||||||
|
resources_[filename] = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Llegir dades encriptades
|
||||||
|
uint64_t data_size;
|
||||||
|
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
|
||||||
|
|
||||||
|
data_.resize(data_size);
|
||||||
|
file.read(reinterpret_cast<char*>(data_.data()), data_size);
|
||||||
|
|
||||||
|
// Desencriptar
|
||||||
|
decryptData(data_, DEFAULT_ENCRYPT_KEY);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t version;
|
// Obtenir un recurs del paquet
|
||||||
file.read(reinterpret_cast<char*>(&version), sizeof(version));
|
auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> {
|
||||||
if (version != VERSION) {
|
auto it = resources_.find(filename);
|
||||||
std::cerr << "[ResourcePack] Error: versión incompatible (esperava " << VERSION
|
if (it == resources_.end()) {
|
||||||
<< ", trobat " << version << ")\n";
|
std::cerr << "[ResourcePack] Error: recurs no trobat: " << filename << '\n';
|
||||||
return false;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Llegir nombre de recursos
|
const auto& entry = it->second;
|
||||||
uint32_t resource_count;
|
|
||||||
file.read(reinterpret_cast<char*>(&resource_count), sizeof(resource_count));
|
|
||||||
|
|
||||||
// Llegir metadades de recursos
|
// Extreure dades
|
||||||
resources_.clear();
|
|
||||||
for (uint32_t i = 0; i < resource_count; ++i) {
|
|
||||||
// Nom del file
|
|
||||||
uint32_t name_len;
|
|
||||||
file.read(reinterpret_cast<char*>(&name_len), sizeof(name_len));
|
|
||||||
|
|
||||||
std::string filename(name_len, '\0');
|
|
||||||
file.read(filename.data(), name_len);
|
|
||||||
|
|
||||||
// Offset, mida, checksum
|
|
||||||
ResourceEntry entry;
|
|
||||||
entry.filename = filename;
|
|
||||||
file.read(reinterpret_cast<char*>(&entry.offset), sizeof(entry.offset));
|
|
||||||
file.read(reinterpret_cast<char*>(&entry.size), sizeof(entry.size));
|
|
||||||
file.read(reinterpret_cast<char*>(&entry.checksum), sizeof(entry.checksum));
|
|
||||||
|
|
||||||
resources_[filename] = entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Llegir dades encriptades
|
|
||||||
uint64_t data_size;
|
|
||||||
file.read(reinterpret_cast<char*>(&data_size), sizeof(data_size));
|
|
||||||
|
|
||||||
data_.resize(data_size);
|
|
||||||
file.read(reinterpret_cast<char*>(data_.data()), data_size);
|
|
||||||
|
|
||||||
// Desencriptar
|
|
||||||
decryptData(data_, DEFAULT_ENCRYPT_KEY);
|
|
||||||
|
|
||||||
std::cout << "[ResourcePack] Carregat: " << pack_file << " (" << resources_.size()
|
|
||||||
<< " recursos)\n";
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtenir un recurs del paquet
|
|
||||||
auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> {
|
|
||||||
auto it = resources_.find(filename);
|
|
||||||
if (it == resources_.end()) {
|
|
||||||
std::cerr << "[ResourcePack] Error: recurs no trobat: " << filename << '\n';
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto& entry = it->second;
|
|
||||||
|
|
||||||
// Extreure dades
|
|
||||||
if (entry.offset + entry.size > data_.size()) {
|
|
||||||
std::cerr << "[ResourcePack] Error: offset/mida invàlid per " << filename << '\n';
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
|
|
||||||
data_.begin() + entry.offset + entry.size);
|
|
||||||
|
|
||||||
// Verificar checksum
|
|
||||||
uint32_t computed_checksum = calculateChecksum(resource_data);
|
|
||||||
if (computed_checksum != entry.checksum) {
|
|
||||||
std::cerr << "[ResourcePack] ADVERTÈNCIA: checksum invàlid per " << filename
|
|
||||||
<< " (esperat " << entry.checksum << ", calculat " << computed_checksum
|
|
||||||
<< ")\n";
|
|
||||||
// No falla, pero adverteix
|
|
||||||
}
|
|
||||||
|
|
||||||
return resource_data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comprovar si existeix un recurs
|
|
||||||
auto Pack::hasResource(const std::string& filename) const -> bool {
|
|
||||||
return resources_.contains(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtenir list de todos los recursos
|
|
||||||
auto Pack::getResourceList() const -> std::vector<std::string> {
|
|
||||||
std::vector<std::string> list;
|
|
||||||
list.reserve(resources_.size());
|
|
||||||
|
|
||||||
for (const auto& [name, entry] : resources_) {
|
|
||||||
list.push_back(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::ranges::sort(list);
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validar integritat del paquet
|
|
||||||
auto Pack::validatePack() const -> bool {
|
|
||||||
bool valid = true;
|
|
||||||
|
|
||||||
for (const auto& [name, entry] : resources_) {
|
|
||||||
// Verificar offset i mida
|
|
||||||
if (entry.offset + entry.size > data_.size()) {
|
if (entry.offset + entry.size > data_.size()) {
|
||||||
std::cerr << "[ResourcePack] Error de validació: " << name
|
std::cerr << "[ResourcePack] Error: offset/mida invàlid per " << filename << '\n';
|
||||||
<< " té offset/mida invàlid\n";
|
return {};
|
||||||
valid = false;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extreure i verificar checksum
|
|
||||||
std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
|
std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
|
||||||
data_.begin() + entry.offset + entry.size);
|
data_.begin() + entry.offset + entry.size);
|
||||||
|
|
||||||
|
// Verificar checksum
|
||||||
uint32_t computed_checksum = calculateChecksum(resource_data);
|
uint32_t computed_checksum = calculateChecksum(resource_data);
|
||||||
if (computed_checksum != entry.checksum) {
|
if (computed_checksum != entry.checksum) {
|
||||||
std::cerr << "[ResourcePack] Error de validació: " << name
|
std::cerr << "[ResourcePack] ADVERTÈNCIA: checksum invàlid per " << filename
|
||||||
<< " té checksum invàlid\n";
|
<< " (esperat " << entry.checksum << ", calculat " << computed_checksum
|
||||||
valid = false;
|
<< ")\n";
|
||||||
|
// No falla, pero adverteix
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return resource_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valid) {
|
// Comprovar si existeix un recurs
|
||||||
std::cout << "[ResourcePack] Validació OK (" << resources_.size() << " recursos)\n";
|
auto Pack::hasResource(const std::string& filename) const -> bool {
|
||||||
|
return resources_.contains(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
return valid;
|
// Obtenir list de todos los recursos
|
||||||
}
|
auto Pack::getResourceList() const -> std::vector<std::string> {
|
||||||
|
std::vector<std::string> list;
|
||||||
|
list.reserve(resources_.size());
|
||||||
|
|
||||||
|
for (const auto& [name, entry] : resources_) {
|
||||||
|
list.push_back(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ranges::sort(list);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar integritat del paquet
|
||||||
|
auto Pack::validatePack() const -> bool {
|
||||||
|
bool valid = true;
|
||||||
|
|
||||||
|
for (const auto& [name, entry] : resources_) {
|
||||||
|
// Verificar offset i mida
|
||||||
|
if (entry.offset + entry.size > data_.size()) {
|
||||||
|
std::cerr << "[ResourcePack] Error de validació: " << name
|
||||||
|
<< " té offset/mida invàlid\n";
|
||||||
|
valid = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extreure i verificar checksum
|
||||||
|
std::vector<uint8_t> resource_data(data_.begin() + entry.offset,
|
||||||
|
data_.begin() + entry.offset + entry.size);
|
||||||
|
|
||||||
|
uint32_t computed_checksum = calculateChecksum(resource_data);
|
||||||
|
if (computed_checksum != entry.checksum) {
|
||||||
|
std::cerr << "[ResourcePack] Error de validació: " << name
|
||||||
|
<< " té checksum invàlid\n";
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Resource
|
} // namespace Resource
|
||||||
|
|||||||
@@ -2,20 +2,33 @@
|
|||||||
|
|
||||||
#include "core/system/debug_overlay.hpp"
|
#include "core/system/debug_overlay.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <cmath>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/defaults.hpp"
|
#include "core/defaults.hpp"
|
||||||
|
#include "core/rendering/gpu/gpu_frame_renderer.hpp"
|
||||||
#include "core/types.hpp"
|
#include "core/types.hpp"
|
||||||
|
|
||||||
namespace System {
|
namespace System {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
namespace Cfg = Defaults::Hud::DebugOverlay;
|
namespace Cfg = Defaults::Hud::DebugOverlay;
|
||||||
|
|
||||||
|
auto toUpperAscii(std::string s) -> std::string {
|
||||||
|
for (char& c : s) {
|
||||||
|
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
DebugOverlay::DebugOverlay(Rendering::Renderer* renderer,
|
DebugOverlay::DebugOverlay(Rendering::Renderer* renderer,
|
||||||
const Config::RenderingConfig& rendering_cfg)
|
const Config::RenderingConfig& rendering_cfg)
|
||||||
: text_(renderer),
|
: text_(renderer),
|
||||||
|
renderer_(renderer),
|
||||||
rendering_cfg_(&rendering_cfg) {}
|
rendering_cfg_(&rendering_cfg) {}
|
||||||
|
|
||||||
void DebugOverlay::update(float delta_time) {
|
void DebugOverlay::update(float delta_time) {
|
||||||
@@ -23,7 +36,7 @@ namespace System {
|
|||||||
fps_frame_count_++;
|
fps_frame_count_++;
|
||||||
|
|
||||||
if (fps_accumulator_ >= Cfg::FPS_UPDATE_INTERVAL) {
|
if (fps_accumulator_ >= Cfg::FPS_UPDATE_INTERVAL) {
|
||||||
fps_display_ = static_cast<int>(fps_frame_count_ / fps_accumulator_);
|
fps_display_ = static_cast<int>(std::lround(static_cast<float>(fps_frame_count_) / fps_accumulator_));
|
||||||
fps_frame_count_ = 0;
|
fps_frame_count_ = 0;
|
||||||
fps_accumulator_ = 0.0F;
|
fps_accumulator_ = 0.0F;
|
||||||
}
|
}
|
||||||
@@ -35,23 +48,43 @@ namespace System {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const std::string FPS_TEXT = "FPS: " + std::to_string(fps_display_);
|
const std::string FPS_TEXT = "FPS: " + std::to_string(fps_display_);
|
||||||
|
const std::string RES_TEXT = "RES: " + std::to_string(rendering_cfg_->render_width) + "X" + std::to_string(rendering_cfg_->render_height);
|
||||||
|
const char* driver_raw = SDL_GetGPUDeviceDriver(renderer_->device().get());
|
||||||
|
const std::string DRIVER_TEXT = "DRIVER: " + toUpperAscii(driver_raw != nullptr ? driver_raw : "?");
|
||||||
const std::string VSYNC_TEXT = std::string("VSYNC: ") + (rendering_cfg_->vsync == 1 ? "ON" : "OFF");
|
const std::string VSYNC_TEXT = std::string("VSYNC: ") + (rendering_cfg_->vsync == 1 ? "ON" : "OFF");
|
||||||
const std::string AA_TEXT = std::string("AA: ") + (rendering_cfg_->antialias == 1 ? "ON" : "OFF");
|
const std::string AA_TEXT = std::string("AA: ") + (rendering_cfg_->antialias == 1 ? "ON" : "OFF");
|
||||||
|
|
||||||
|
float y = Cfg::Y_FPS;
|
||||||
text_.render(FPS_TEXT,
|
text_.render(FPS_TEXT,
|
||||||
Vec2{.x = Cfg::X, .y = Cfg::Y_FPS},
|
Vec2{.x = Cfg::X, .y = y},
|
||||||
|
Cfg::FPS_SCALE,
|
||||||
|
Cfg::TEXT_SPACING,
|
||||||
|
Cfg::BRIGHTNESS,
|
||||||
|
Cfg::COLOR);
|
||||||
|
y += Cfg::FPS_LINE_HEIGHT;
|
||||||
|
text_.render(RES_TEXT,
|
||||||
|
Vec2{.x = Cfg::X, .y = y},
|
||||||
Cfg::TEXT_SCALE,
|
Cfg::TEXT_SCALE,
|
||||||
Cfg::TEXT_SPACING,
|
Cfg::TEXT_SPACING,
|
||||||
Cfg::BRIGHTNESS,
|
Cfg::BRIGHTNESS,
|
||||||
Cfg::COLOR);
|
Cfg::COLOR);
|
||||||
|
y += Cfg::LINE_HEIGHT;
|
||||||
|
text_.render(DRIVER_TEXT,
|
||||||
|
Vec2{.x = Cfg::X, .y = y},
|
||||||
|
Cfg::TEXT_SCALE,
|
||||||
|
Cfg::TEXT_SPACING,
|
||||||
|
Cfg::BRIGHTNESS,
|
||||||
|
Cfg::COLOR);
|
||||||
|
y += Cfg::LINE_HEIGHT;
|
||||||
text_.render(VSYNC_TEXT,
|
text_.render(VSYNC_TEXT,
|
||||||
Vec2{.x = Cfg::X, .y = Cfg::Y_FPS + Cfg::LINE_HEIGHT},
|
Vec2{.x = Cfg::X, .y = y},
|
||||||
Cfg::TEXT_SCALE,
|
Cfg::TEXT_SCALE,
|
||||||
Cfg::TEXT_SPACING,
|
Cfg::TEXT_SPACING,
|
||||||
Cfg::BRIGHTNESS,
|
Cfg::BRIGHTNESS,
|
||||||
Cfg::COLOR);
|
Cfg::COLOR);
|
||||||
|
y += Cfg::LINE_HEIGHT;
|
||||||
text_.render(AA_TEXT,
|
text_.render(AA_TEXT,
|
||||||
Vec2{.x = Cfg::X, .y = Cfg::Y_FPS + (2.0F * Cfg::LINE_HEIGHT)},
|
Vec2{.x = Cfg::X, .y = y},
|
||||||
Cfg::TEXT_SCALE,
|
Cfg::TEXT_SCALE,
|
||||||
Cfg::TEXT_SPACING,
|
Cfg::TEXT_SPACING,
|
||||||
Cfg::BRIGHTNESS,
|
Cfg::BRIGHTNESS,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ namespace System {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
Graphics::VectorText text_;
|
Graphics::VectorText text_;
|
||||||
|
Rendering::Renderer* renderer_;
|
||||||
const Config::RenderingConfig* rendering_cfg_;
|
const Config::RenderingConfig* rendering_cfg_;
|
||||||
bool visible_{false};
|
bool visible_{false};
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,9 @@
|
|||||||
|
|
||||||
#include "core/audio/audio.hpp"
|
#include "core/audio/audio.hpp"
|
||||||
#include "core/audio/audio_adapter.hpp"
|
#include "core/audio/audio_adapter.hpp"
|
||||||
#include "core/defaults/audio.hpp"
|
|
||||||
#include "core/defaults/window.hpp"
|
#include "core/defaults/window.hpp"
|
||||||
|
#include "core/graphics/shape_loader.hpp"
|
||||||
|
#include "core/input/define_inputs.hpp"
|
||||||
#include "core/input/input.hpp"
|
#include "core/input/input.hpp"
|
||||||
#include "core/input/mouse.hpp"
|
#include "core/input/mouse.hpp"
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "core/resources/resource_loader.hpp"
|
#include "core/resources/resource_loader.hpp"
|
||||||
#include "core/system/notifier.hpp"
|
#include "core/system/notifier.hpp"
|
||||||
|
#include "core/system/service_menu.hpp"
|
||||||
#include "core/utils/path_utils.hpp"
|
#include "core/utils/path_utils.hpp"
|
||||||
#include "debug_overlay.hpp"
|
#include "debug_overlay.hpp"
|
||||||
#include "game/config_yaml.hpp"
|
#include "game/config_yaml.hpp"
|
||||||
@@ -104,8 +106,39 @@ Director::Director(int argc, char* argv[])
|
|||||||
// falla, Locale::text() retorna la clau crua i el joc segueix funcionant.
|
// falla, Locale::text() retorna la clau crua i el joc segueix funcionant.
|
||||||
Locale::get().load(std::string("locale/") + cfg_->locale + ".yaml");
|
Locale::get().load(std::string("locale/") + cfg_->locale + ".yaml");
|
||||||
|
|
||||||
// Inicialitzar sistema de input
|
// Inicialitzar sistema de input. El gamecontrollerdb.txt viu al costat del
|
||||||
Input::init("data/gamecontrollerdb.txt");
|
// binari (no dins de resources.pack, perquè SDL_AddGamepadMappingsFromFile
|
||||||
|
// necessita una ruta real de filesystem). resource_base ja apunta al directori
|
||||||
|
// de l'executable (o a Contents/Resources en bundles de macOS).
|
||||||
|
Input::init(resource_base + "/gamecontrollerdb.txt");
|
||||||
|
|
||||||
|
// Autoassignacio de primer arranque: si cap dels dos jugadors te mando
|
||||||
|
// assignat al config, repartim els que hi haja detectats (P1 = pad 0,
|
||||||
|
// P2 = pad 1 si existeix) i ho persistim. Aixo nomes dispara amb tots
|
||||||
|
// dos buits perque un "SENSE MANDO" explicit ha de sobreviure entre
|
||||||
|
// arrancades.
|
||||||
|
{
|
||||||
|
auto& p1 = cfg_->player1;
|
||||||
|
auto& p2 = cfg_->player2;
|
||||||
|
const bool BOTH_EMPTY = p1.gamepad_name.empty() && p1.gamepad_path.empty() && p2.gamepad_name.empty() && p2.gamepad_path.empty();
|
||||||
|
if (BOTH_EMPTY) {
|
||||||
|
const auto& pads = Input::get()->getGamepads();
|
||||||
|
bool changed = false;
|
||||||
|
if (!pads.empty() && pads[0]) {
|
||||||
|
p1.gamepad_name = pads[0]->name;
|
||||||
|
p1.gamepad_path = pads[0]->path;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (pads.size() > 1 && pads[1]) {
|
||||||
|
p2.gamepad_name = pads[1]->name;
|
||||||
|
p2.gamepad_path = pads[1]->path;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
ConfigYaml::saveToFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Aplicar configuración de controls dels jugadors
|
// Aplicar configuración de controls dels jugadors
|
||||||
Input::get()->applyPlayer1Bindings(cfg_->player1);
|
Input::get()->applyPlayer1Bindings(cfg_->player1);
|
||||||
@@ -137,20 +170,31 @@ Director::Director(int argc, char* argv[])
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Audio::Config AUDIO_CONFIG{
|
const Audio::Config AUDIO_CONFIG{
|
||||||
.enabled = Defaults::Audio::ENABLED,
|
.enabled = cfg_->audio.enabled,
|
||||||
.volume = Defaults::Audio::VOLUME,
|
.volume = cfg_->audio.volume,
|
||||||
.music_enabled = Defaults::Audio::MUSIC_ENABLED,
|
.music_enabled = cfg_->audio.music_enabled,
|
||||||
.music_volume = Defaults::Audio::MUSIC_VOLUME,
|
.music_volume = cfg_->audio.music_volume,
|
||||||
.sound_enabled = Defaults::Audio::SOUND_ENABLED,
|
.sound_enabled = cfg_->audio.sound_enabled,
|
||||||
.sound_volume = Defaults::Audio::SOUND_VOLUME,
|
.sound_volume = cfg_->audio.sound_volume,
|
||||||
};
|
};
|
||||||
Audio::init(AUDIO_CONFIG);
|
Audio::init(AUDIO_CONFIG);
|
||||||
Audio::get()->applySettings(AUDIO_CONFIG);
|
Audio::get()->applySettings(AUDIO_CONFIG);
|
||||||
|
|
||||||
AudioResource::getMusic("title.ogg");
|
// Precàrrega blocant de tots els recursos al boot per evitar hits d'I/O i
|
||||||
AudioResource::getMusic("game.ogg");
|
// de decodificació en transicions (TITLE → GAME, primera explosió, etc.).
|
||||||
|
// Mateix patró que aee_arcade: iterem `listResources` i forcem la càrrega
|
||||||
|
// al cache de cada subsistema.
|
||||||
|
for (const auto& path : Resource::Helper::listResources("music/")) {
|
||||||
|
AudioResource::getMusic(path.substr(std::string_view{"music/"}.size()));
|
||||||
|
}
|
||||||
|
for (const auto& path : Resource::Helper::listResources("sounds/")) {
|
||||||
|
AudioResource::getSound(path.substr(std::string_view{"sounds/"}.size()));
|
||||||
|
}
|
||||||
|
for (const auto& path : Resource::Helper::listResources("shapes/")) {
|
||||||
|
Graphics::ShapeLoader::load(path.substr(std::string_view{"shapes/"}.size()));
|
||||||
|
}
|
||||||
if (cfg_->console) {
|
if (cfg_->console) {
|
||||||
std::cout << "Música precacheada\n";
|
std::cout << "Recursos precachejats (música, sons, shapes)\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
context_ = std::make_unique<SceneContext>();
|
context_ = std::make_unique<SceneContext>();
|
||||||
@@ -165,6 +209,8 @@ Director::Director(int argc, char* argv[])
|
|||||||
cfg_->rendering);
|
cfg_->rendering);
|
||||||
|
|
||||||
System::Notifier::init(sdl_->getRenderer());
|
System::Notifier::init(sdl_->getRenderer());
|
||||||
|
System::ServiceMenu::init(sdl_->getRenderer(), sdl_.get(), debug_overlay_.get());
|
||||||
|
System::DefineInputs::init(sdl_->getRenderer());
|
||||||
|
|
||||||
last_ticks_ms_ = SDL_GetTicks();
|
last_ticks_ms_ = SDL_GetTicks();
|
||||||
}
|
}
|
||||||
@@ -179,6 +225,8 @@ Director::~Director() {
|
|||||||
// l'hem de cridar nosaltres.
|
// l'hem de cridar nosaltres.
|
||||||
current_scene_.reset();
|
current_scene_.reset();
|
||||||
debug_overlay_.reset();
|
debug_overlay_.reset();
|
||||||
|
System::DefineInputs::destroy();
|
||||||
|
System::ServiceMenu::destroy();
|
||||||
System::Notifier::destroy();
|
System::Notifier::destroy();
|
||||||
context_.reset();
|
context_.reset();
|
||||||
sdl_.reset();
|
sdl_.reset();
|
||||||
@@ -359,6 +407,12 @@ auto Director::iterate() -> SDL_AppResult {
|
|||||||
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||||
notifier->update(delta_time);
|
notifier->update(delta_time);
|
||||||
}
|
}
|
||||||
|
if (auto* menu = System::ServiceMenu::get(); menu != nullptr) {
|
||||||
|
menu->update(delta_time);
|
||||||
|
}
|
||||||
|
if (auto* di = System::DefineInputs::get(); di != nullptr) {
|
||||||
|
di->update(delta_time);
|
||||||
|
}
|
||||||
Audio::update();
|
Audio::update();
|
||||||
|
|
||||||
// Si la swapchain no està disponible (finestra minimitzada, etc.),
|
// Si la swapchain no està disponible (finestra minimitzada, etc.),
|
||||||
@@ -372,6 +426,17 @@ auto Director::iterate() -> SDL_AppResult {
|
|||||||
if (const auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
if (const auto* notifier = System::Notifier::get(); notifier != nullptr) {
|
||||||
notifier->draw(); // toast: per damunt de tot
|
notifier->draw(); // toast: per damunt de tot
|
||||||
}
|
}
|
||||||
|
// Mentre l'overlay de redefinicio esta actiu, amaguem el menu de servei
|
||||||
|
// (encara queda "open" per a absorbir events un cop el modal s'auto-tanqui,
|
||||||
|
// pero no es pinta per no confondre's visualment amb el modal).
|
||||||
|
const auto* di = System::DefineInputs::get();
|
||||||
|
const bool DEFINE_ACTIVE = (di != nullptr) && di->isActive();
|
||||||
|
if (const auto* menu = System::ServiceMenu::get(); menu != nullptr && !DEFINE_ACTIVE) {
|
||||||
|
menu->draw(); // service menu: per damunt fins i tot dels toasts
|
||||||
|
}
|
||||||
|
if (di != nullptr) {
|
||||||
|
di->draw(); // overlay de rebind: per damunt de tot
|
||||||
|
}
|
||||||
sdl_->present();
|
sdl_->present();
|
||||||
return SDL_APP_CONTINUE;
|
return SDL_APP_CONTINUE;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,13 @@
|
|||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "core/input/define_inputs.hpp"
|
||||||
#include "core/input/input.hpp"
|
#include "core/input/input.hpp"
|
||||||
#include "core/input/mouse.hpp"
|
#include "core/input/mouse.hpp"
|
||||||
#include "core/locale/locale.hpp"
|
#include "core/locale/locale.hpp"
|
||||||
#include "core/rendering/sdl_manager.hpp"
|
#include "core/rendering/sdl_manager.hpp"
|
||||||
#include "core/system/notifier.hpp"
|
#include "core/system/notifier.hpp"
|
||||||
|
#include "core/system/service_menu.hpp"
|
||||||
#include "game/config_yaml.hpp"
|
#include "game/config_yaml.hpp"
|
||||||
#include "scene_context.hpp"
|
#include "scene_context.hpp"
|
||||||
|
|
||||||
@@ -19,6 +21,81 @@ using SceneType = SceneContext::SceneType;
|
|||||||
|
|
||||||
namespace GlobalEvents {
|
namespace GlobalEvents {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Reenvia events al menu de servei si esta obert. Accepta:
|
||||||
|
// - KEY_DOWN (excepte F1-F12 i ESC, que sempre passen com a globals)
|
||||||
|
// - GAMEPAD_BUTTON_DOWN (per navegacio amb dpad + FIRE/ACCELERATE)
|
||||||
|
// - GAMEPAD_AXIS_MOTION (per navegacio amb stick)
|
||||||
|
// Retorna true si l'event s'ha entregat al menu.
|
||||||
|
auto forwardToServiceMenu(const SDL_Event& event) -> bool {
|
||||||
|
auto* menu = System::ServiceMenu::get();
|
||||||
|
if (menu == nullptr || !menu->isOpen()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||||
|
const SDL_Scancode SC = event.key.scancode;
|
||||||
|
const bool PASSTHROUGH = (SC == SDL_SCANCODE_ESCAPE) ||
|
||||||
|
(SC >= SDL_SCANCODE_F1 && SC <= SDL_SCANCODE_F12);
|
||||||
|
if (PASSTHROUGH) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
menu->handleEvent(event);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN ||
|
||||||
|
event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
|
||||||
|
menu->handleEvent(event);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Engoleix els events que DefineInputs vol consumir mentre l'overlay
|
||||||
|
// es actiu. Els events que el modul torna a passar (QUIT, ESC) cauen
|
||||||
|
// cap al pipeline normal i poden tancar la finestra o obrir el prompt
|
||||||
|
// d'eixida sense haver de completar la sequencia.
|
||||||
|
auto consumeIfDefineActive(const SDL_Event& event) -> bool {
|
||||||
|
auto* di = System::DefineInputs::get();
|
||||||
|
if (di == nullptr || !di->isActive()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return di->handleEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Botó MENU al mando d'algun jugador → alterna el menú de servei
|
||||||
|
// (mateix comportament que F12 al teclat). Retorna true si l'event és
|
||||||
|
// un GAMEPAD_BUTTON_DOWN consumit.
|
||||||
|
auto handleGamepadMenuButton(const SDL_Event& event) -> bool {
|
||||||
|
if (event.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto* input = Input::get();
|
||||||
|
if (input == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto match_player = [&](int player_index) {
|
||||||
|
auto pad = input->getPlayerGamepad(player_index);
|
||||||
|
if (!pad || pad->instance_id != event.gbutton.which) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto it = pad->bindings.find(InputAction::MENU);
|
||||||
|
if (it == pad->bindings.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return it->second.button == static_cast<int>(event.gbutton.button);
|
||||||
|
};
|
||||||
|
if (!match_player(0) && !match_player(1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (auto* menu = System::ServiceMenu::get(); menu != nullptr) {
|
||||||
|
menu->toggle();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
auto handle(const SDL_Event& event, SDLManager& sdl, SceneContext& context) -> bool {
|
auto handle(const SDL_Event& event, SDLManager& sdl, SceneContext& context) -> bool {
|
||||||
// 1. Permitir que Input procese el evento (para hotplug de gamepads)
|
// 1. Permitir que Input procese el evento (para hotplug de gamepads)
|
||||||
auto event_msg = Input::get()->handleEvent(event);
|
auto event_msg = Input::get()->handleEvent(event);
|
||||||
@@ -26,6 +103,12 @@ namespace GlobalEvents {
|
|||||||
std::cout << "[Input] " << event_msg << '\n';
|
std::cout << "[Input] " << event_msg << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1b. Si l'overlay de redefinicio esta actiu, engoleix tots els events
|
||||||
|
// (cap arriba al joc, al menu de servei ni als hotkeys F1-F12).
|
||||||
|
if (consumeIfDefineActive(event)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Procesar SDL_EVENT_QUIT directamente (no es input de juego)
|
// 2. Procesar SDL_EVENT_QUIT directamente (no es input de juego)
|
||||||
if (event.type == SDL_EVENT_QUIT) {
|
if (event.type == SDL_EVENT_QUIT) {
|
||||||
context.setNextScene(SceneType::EXIT);
|
context.setNextScene(SceneType::EXIT);
|
||||||
@@ -36,7 +119,20 @@ namespace GlobalEvents {
|
|||||||
// 3. Gestió del ratolí (auto-ocultar)
|
// 3. Gestió del ratolí (auto-ocultar)
|
||||||
Mouse::handleEvent(event);
|
Mouse::handleEvent(event);
|
||||||
|
|
||||||
// 4. Procesar acciones globales directamente desde eventos SDL
|
// 3b. Botó MENU al mando (equivalent a F12)
|
||||||
|
if (handleGamepadMenuButton(event)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Service Menu (F12): consumeix tot KEY_DOWN excepte tecles de
|
||||||
|
// funció (F1-F12) i ESC, que continuen sent globals (zoom, fullscreen,
|
||||||
|
// vsync, AA, postfx, locale, exit prompt). Aixi el menu captura
|
||||||
|
// ENTER/BACKSPACE/UP/DOWN/LEFT/RIGHT i lletres mentre esta obert.
|
||||||
|
if (forwardToServiceMenu(event)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Procesar acciones globales directamente desde eventos SDL
|
||||||
// (NO usar Input::checkAction() para evitar desfase de timing)
|
// (NO usar Input::checkAction() para evitar desfase de timing)
|
||||||
if (event.type == SDL_EVENT_KEY_DOWN) {
|
if (event.type == SDL_EVENT_KEY_DOWN) {
|
||||||
switch (event.key.scancode) {
|
switch (event.key.scancode) {
|
||||||
@@ -84,6 +180,15 @@ namespace GlobalEvents {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case SDL_SCANCODE_F12: {
|
||||||
|
// Toggle del menu de servei. Sempre passa com a global
|
||||||
|
// (alterna obert/tancat des de qualsevol escena).
|
||||||
|
if (auto* menu = System::ServiceMenu::get(); menu != nullptr) {
|
||||||
|
menu->toggle();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
case SDL_SCANCODE_ESCAPE: {
|
case SDL_SCANCODE_ESCAPE: {
|
||||||
// Doble pulsació per confirmar sortida: la primera ESC
|
// Doble pulsació per confirmar sortida: la primera ESC
|
||||||
// dispara un toast d'avís; només si aquest toast concret
|
// dispara un toast d'avís; només si aquest toast concret
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// relaunch.cpp - Implementacio del reinici en calent
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "core/system/relaunch.hpp"
|
||||||
|
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <process.h> // _execv
|
||||||
|
#else
|
||||||
|
#include <unistd.h> // execv
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Estat global (process-scope). Aquesta TU es la unica que gestiona el
|
||||||
|
// reinici, aixi que els static interns no s'escapen.
|
||||||
|
char** g_argv = nullptr;
|
||||||
|
bool g_requested = false;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace System::Relaunch {
|
||||||
|
|
||||||
|
void setArgv(int /*argc*/, char** argv) {
|
||||||
|
g_argv = argv;
|
||||||
|
}
|
||||||
|
|
||||||
|
void request() {
|
||||||
|
g_requested = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto isRequested() -> bool {
|
||||||
|
return g_requested;
|
||||||
|
}
|
||||||
|
|
||||||
|
void execIfRequested() {
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
// Al navegador el reinici real seria location.reload(); aqui no fem res.
|
||||||
|
return;
|
||||||
|
#else
|
||||||
|
if (!g_requested || g_argv == nullptr || g_argv[0] == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::cout << "[Relaunch] Reiniciant " << g_argv[0] << "...\n";
|
||||||
|
#ifdef _WIN32
|
||||||
|
_execv(g_argv[0], g_argv);
|
||||||
|
#else
|
||||||
|
execv(g_argv[0], g_argv);
|
||||||
|
#endif
|
||||||
|
// Si arribem aqui, execv ha fallat. Tots els subsistemes ja estan
|
||||||
|
// destruits; sortim amb error i el shell rebra el codi.
|
||||||
|
std::cerr << "[Relaunch] Ha fallat: " << std::strerror(errno) << '\n';
|
||||||
|
std::exit(EXIT_FAILURE);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace System::Relaunch
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// relaunch.hpp - Reinici en calent del proces (execv)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Helper desacoblat per a permetre que el menu de servei demani un reinici
|
||||||
|
// sense conèixer Director ni main.cpp. Patro:
|
||||||
|
//
|
||||||
|
// main() → Relaunch::setArgv(argc, argv) (a l'arrencada)
|
||||||
|
// ServiceMenu → Relaunch::request() (en activar REINICIAR)
|
||||||
|
// main() → Relaunch::execIfRequested() (a SDL_AppQuit)
|
||||||
|
//
|
||||||
|
// L'execv() reemplaca el proces actual: si torna, ha fallat. A EMSCRIPTEN
|
||||||
|
// no es pot reiniciar; isRequested() seguira dient true pero execIfRequested
|
||||||
|
// sera no-op.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace System::Relaunch {
|
||||||
|
|
||||||
|
// Emmagatzema l'argv original. Cal cridar-ho una vegada des de main.
|
||||||
|
void setArgv(int argc, char** argv);
|
||||||
|
|
||||||
|
// Demana un reinici (no actua immediatament; nomes marca el flag).
|
||||||
|
void request();
|
||||||
|
|
||||||
|
// Consulta del flag.
|
||||||
|
[[nodiscard]] auto isRequested() -> bool;
|
||||||
|
|
||||||
|
// Si hi ha reinici demanat i tenim argv valid, fa execv. En cas d'exit
|
||||||
|
// no torna. Si execv falla, registra l'error i torna; el caller hauria
|
||||||
|
// de sortir normalment.
|
||||||
|
void execIfRequested();
|
||||||
|
|
||||||
|
} // namespace System::Relaunch
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,179 @@
|
|||||||
|
// service_menu.hpp - Menu de servei (singleton)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Overlay de configuracio global accessible amb F12 des de qualsevol escena
|
||||||
|
// (LOGO, TITLE, GAME). Captura tots els KEY_DOWN excepte F1-F12 i ESC, que
|
||||||
|
// continuen arribant a GlobalEvents. Mentre esta obert, GameScene::update()
|
||||||
|
// fa early return per pausar el joc; LOGO i TITLE continuen renderitzant-se
|
||||||
|
// sota el menu.
|
||||||
|
//
|
||||||
|
// Arquitectura inspirada en aee_arcade service_menu.{hpp,cpp}: pila de
|
||||||
|
// pagines amb cursor, animacio open/close amb easing easeOutQuad i clipping
|
||||||
|
// del contingut mentre la caixa creix/decreix.
|
||||||
|
//
|
||||||
|
// API singleton equivalent a Notifier: init() al startup amb un renderer,
|
||||||
|
// get() retorna el punter, destroy() al teardown.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/graphics/vector_text.hpp"
|
||||||
|
#include "core/rendering/render_context.hpp"
|
||||||
|
|
||||||
|
class SDLManager;
|
||||||
|
|
||||||
|
namespace System {
|
||||||
|
|
||||||
|
class DebugOverlay;
|
||||||
|
|
||||||
|
class ServiceMenu {
|
||||||
|
public:
|
||||||
|
// Tipus d'item de menu. En aquesta iteracio nomes s'usen SUBMENU i
|
||||||
|
// LABEL; la resta queden reservats per a iteracions futures (toggles
|
||||||
|
// de vsync/zoom, picker d'idioma, restart, exit...).
|
||||||
|
enum class Kind : std::uint8_t {
|
||||||
|
LABEL, // No interactiu, nomes es dibuixa
|
||||||
|
TOGGLE, // bool flip — reservat
|
||||||
|
CYCLE, // index amb modul — reservat
|
||||||
|
INT_RANGE, // step ± — reservat
|
||||||
|
SUBMENU, // pushPage en activar — usat
|
||||||
|
ACTION // call al lambda en activar — reservat
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Item {
|
||||||
|
Kind kind = Kind::LABEL;
|
||||||
|
std::string label_key; // Clau de locale (s'ignora si label_text no esta buit)
|
||||||
|
std::string label_text; // Text literal (no locale). Util per a labels que no necessiten traduccio (resolucions, etc.)
|
||||||
|
bool selectable = true;
|
||||||
|
// SUBMENU / ACTION: callback en ENTER / RIGHT.
|
||||||
|
std::function<void()> on_activate;
|
||||||
|
// TOGGLE / CYCLE / INT_RANGE: text del valor actual (renderitzat a la dreta).
|
||||||
|
std::function<std::string()> get_value_text;
|
||||||
|
// TOGGLE / CYCLE / INT_RANGE: callback amb +1 (RIGHT/ENTER) o -1 (LEFT).
|
||||||
|
std::function<void(int)> on_change;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Page {
|
||||||
|
std::string title_key;
|
||||||
|
// Subtitol opcional, renderitzat sota el titol amb tipografia mes
|
||||||
|
// petita i color apagat. Es una funcio perque pot ser dinamic
|
||||||
|
// (versio+hash, etc.). Si esta buit, no es renderitza.
|
||||||
|
std::function<std::string()> subtitle_provider;
|
||||||
|
std::vector<Item> items;
|
||||||
|
std::size_t cursor = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Inicialitza el singleton amb el renderer global, l'SDLManager (video
|
||||||
|
// toggles: fullscreen, vsync, AA, postfx, zoom) i el DebugOverlay
|
||||||
|
// (toggle del HUD de debug a OPCIONS). Tots propietat del Director.
|
||||||
|
static void init(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay);
|
||||||
|
static void destroy();
|
||||||
|
[[nodiscard]] static auto get() -> ServiceMenu*;
|
||||||
|
|
||||||
|
// F12: alterna obrir/tancar amb animacio.
|
||||||
|
void toggle();
|
||||||
|
[[nodiscard]] auto isOpen() const -> bool;
|
||||||
|
|
||||||
|
void update(float delta_time);
|
||||||
|
void draw() const;
|
||||||
|
|
||||||
|
// Processa events de navegacio. Retorna true si l'event s'ha consumit.
|
||||||
|
// Accepta:
|
||||||
|
// - SDL_EVENT_KEY_DOWN: UP/DOWN/ENTER/RIGHT/LEFT/BACKSPACE.
|
||||||
|
// - SDL_EVENT_GAMEPAD_BUTTON_DOWN: DPAD per nav, FIRE = ENTER,
|
||||||
|
// ACCELERATE = BACK. La resta de botons s'ignoren.
|
||||||
|
// - SDL_EVENT_GAMEPAD_AXIS_MOTION: stick X/Y amb edge-detect.
|
||||||
|
auto handleEvent(const SDL_Event& event) -> bool;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ServiceMenu(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay);
|
||||||
|
|
||||||
|
// Sub-handlers de handleEvent. Privats, no son part de l'API publica.
|
||||||
|
auto handleKeyDown(const SDL_Event& event) -> bool;
|
||||||
|
auto handleGamepadButton(const SDL_Event& event) -> bool;
|
||||||
|
auto handleGamepadAxis(const SDL_Event& event) -> bool;
|
||||||
|
// Helpers per a cada eix; permeten que handleGamepadAxis es quedi
|
||||||
|
// com a dispatcher i no bote el llindar de complexitat.
|
||||||
|
void processStickX(Sint16 val);
|
||||||
|
void processStickY(Sint16 val);
|
||||||
|
void processTriggerEdge(SDL_JoystickID which, Sint16 val, int virtual_button, bool& held);
|
||||||
|
|
||||||
|
void buildRootPage();
|
||||||
|
[[nodiscard]] auto buildVideoPage() -> Page;
|
||||||
|
[[nodiscard]] auto buildResolutionPage() const -> Page;
|
||||||
|
[[nodiscard]] static auto buildAudioPage() -> Page;
|
||||||
|
[[nodiscard]] auto buildOptionsPage() const -> Page;
|
||||||
|
[[nodiscard]] auto buildSystemPage() -> Page;
|
||||||
|
[[nodiscard]] auto buildControlsPage() -> Page;
|
||||||
|
// Llista de mandos detectats per a un jugador. Cada item assigna el
|
||||||
|
// pad triat (amb swap automatic si l'altre jugador ja el tenia) i
|
||||||
|
// tanca la picker amb popPage. L'ultim item es "SENSE MANDO" per a
|
||||||
|
// desasignar.
|
||||||
|
[[nodiscard]] auto buildPadPickerPage(int player_index) -> Page;
|
||||||
|
// Pagina de confirmacio "ESTAS SEGUR? NO/SI". on_yes s'executa si
|
||||||
|
// l'usuari selecciona SI; el cursor per defecte apunta a NO.
|
||||||
|
void pushConfirmPage(const std::string& title_key, std::function<void()> on_yes);
|
||||||
|
void pushPage(Page page);
|
||||||
|
void popPage();
|
||||||
|
void moveCursor(int direction);
|
||||||
|
void activateCurrent();
|
||||||
|
// RIGHT (direction=+1) / LEFT (direction=-1). Per a TOGGLE/CYCLE/INT_RANGE
|
||||||
|
// crida on_change. Per a SUBMENU/ACTION nomes +1 (entra/activa).
|
||||||
|
void changeValue(int direction);
|
||||||
|
|
||||||
|
// Alçada objectiu de la caixa per a la pagina superior (sense animacio).
|
||||||
|
[[nodiscard]] auto computeTargetHeight() const -> float;
|
||||||
|
|
||||||
|
// Ample objectiu de la caixa per a la pagina superior (sense animacio).
|
||||||
|
// Pren com a base BOX_WIDTH_MIN i s'eixampla si algun text no hi cap.
|
||||||
|
[[nodiscard]] auto computeTargetWidth() const -> float;
|
||||||
|
|
||||||
|
// Y (top) de l'item index dins una caixa col·locada a box_y. Si la
|
||||||
|
// pagina te subtitol, els items es desplacen cap avall.
|
||||||
|
[[nodiscard]] static auto computeItemTopY(float box_y, std::size_t index, bool has_subtitle) -> float;
|
||||||
|
|
||||||
|
Rendering::Renderer* renderer_;
|
||||||
|
SDLManager* sdl_;
|
||||||
|
DebugOverlay* debug_overlay_;
|
||||||
|
Graphics::VectorText text_;
|
||||||
|
|
||||||
|
std::vector<Page> stack_;
|
||||||
|
bool open_ = false;
|
||||||
|
bool closing_ = false;
|
||||||
|
float open_anim_ = 0.0F; // 0..1 raw (sense easing)
|
||||||
|
float animated_h_ = 0.0F; // Alçada animada amb smoothing exponencial
|
||||||
|
float animated_w_ = 0.0F; // Ample animat (eixampla segons contingut)
|
||||||
|
|
||||||
|
// Estat del highlight (rectangle del cursor). Es lerpa cap a l'item
|
||||||
|
// actiu amb ease-out exponencial; quan el cursor "salta" (open o
|
||||||
|
// push/pop de pagina), s'enganxa directament al nou objectiu.
|
||||||
|
float highlight_y_ = 0.0F;
|
||||||
|
float highlight_h_ = 0.0F;
|
||||||
|
bool highlight_snap_ = true;
|
||||||
|
|
||||||
|
// Edge-detect de stick analogic per a navegacio. Una sola activacio
|
||||||
|
// per direccio: cal tornar a centre (sota el llindar) per disparar
|
||||||
|
// una altra. Compartit entre tots els pads — qualsevol jugador pot
|
||||||
|
// navegar el menu.
|
||||||
|
bool stick_left_held_ = false;
|
||||||
|
bool stick_right_held_ = false;
|
||||||
|
bool stick_up_held_ = false;
|
||||||
|
bool stick_down_held_ = false;
|
||||||
|
// Edge-detect dels triggers L2/R2 com a botons virtuals. SDL3 no
|
||||||
|
// emet button events per als triggers; els llegim com a axis i
|
||||||
|
// sintetitzem una pulsacio quan creuen el llindar.
|
||||||
|
bool trigger_l2_held_ = false;
|
||||||
|
bool trigger_r2_held_ = false;
|
||||||
|
|
||||||
|
static std::unique_ptr<ServiceMenu> instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace System
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// string_utils.hpp - Utilitats genèriques de cadenes
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// VectorText només admet ASCII en majúscules; les notificacions, el menú
|
||||||
|
// de servei i l'overlay de rebind passen els textos dinàmics per aquest
|
||||||
|
// helper abans de pintar-los.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Utils {
|
||||||
|
|
||||||
|
inline auto toUpperAscii(const std::string& s) -> std::string {
|
||||||
|
std::string result = s;
|
||||||
|
for (char& c : result) {
|
||||||
|
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Utils
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include "core/defaults/audio.hpp"
|
||||||
#include "core/defaults/rendering.hpp"
|
#include "core/defaults/rendering.hpp"
|
||||||
#include "core/defaults/window.hpp"
|
#include "core/defaults/window.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
@@ -17,6 +18,7 @@ namespace ConfigYaml {
|
|||||||
// Permeten escriure window.width en lloc d'engine_config.window.width.
|
// Permeten escriure window.width en lloc d'engine_config.window.width.
|
||||||
Config::WindowConfig& window = engine_config.window;
|
Config::WindowConfig& window = engine_config.window;
|
||||||
Config::RenderingConfig& rendering = engine_config.rendering;
|
Config::RenderingConfig& rendering = engine_config.rendering;
|
||||||
|
Config::AudioConfig& audio = engine_config.audio;
|
||||||
Config::PlayerBindings& player1 = engine_config.player1;
|
Config::PlayerBindings& player1 = engine_config.player1;
|
||||||
Config::PlayerBindings& player2 = engine_config.player2;
|
Config::PlayerBindings& player2 = engine_config.player2;
|
||||||
bool& console = engine_config.console;
|
bool& console = engine_config.console;
|
||||||
@@ -209,6 +211,14 @@ namespace ConfigYaml {
|
|||||||
rendering.render_width = Defaults::Rendering::RENDER_WIDTH_DEFAULT;
|
rendering.render_width = Defaults::Rendering::RENDER_WIDTH_DEFAULT;
|
||||||
rendering.render_height = Defaults::Rendering::RENDER_HEIGHT_DEFAULT;
|
rendering.render_height = Defaults::Rendering::RENDER_HEIGHT_DEFAULT;
|
||||||
|
|
||||||
|
// Audio
|
||||||
|
audio.enabled = Defaults::Audio::ENABLED;
|
||||||
|
audio.volume = Defaults::Audio::VOLUME;
|
||||||
|
audio.music_enabled = Defaults::Audio::MUSIC_ENABLED;
|
||||||
|
audio.music_volume = Defaults::Audio::MUSIC_VOLUME;
|
||||||
|
audio.sound_enabled = Defaults::Audio::SOUND_ENABLED;
|
||||||
|
audio.sound_volume = Defaults::Audio::SOUND_VOLUME;
|
||||||
|
|
||||||
// Idioma
|
// Idioma
|
||||||
locale = "ca";
|
locale = "ca";
|
||||||
|
|
||||||
@@ -307,6 +317,22 @@ namespace ConfigYaml {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void loadAudioConfigFromYaml(const fkyaml::node& yaml) {
|
||||||
|
if (!yaml.contains("audio")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto& aud = yaml["audio"];
|
||||||
|
|
||||||
|
auto in_unit_range = [](float v) { return v >= 0.0F && v <= 1.0F; };
|
||||||
|
|
||||||
|
readField(aud, "enabled", audio.enabled, Defaults::Audio::ENABLED);
|
||||||
|
readField(aud, "volume", audio.volume, Defaults::Audio::VOLUME, in_unit_range);
|
||||||
|
readField(aud, "music_enabled", audio.music_enabled, Defaults::Audio::MUSIC_ENABLED);
|
||||||
|
readField(aud, "music_volume", audio.music_volume, Defaults::Audio::MUSIC_VOLUME, in_unit_range);
|
||||||
|
readField(aud, "sound_enabled", audio.sound_enabled, Defaults::Audio::SOUND_ENABLED);
|
||||||
|
readField(aud, "sound_volume", audio.sound_volume, Defaults::Audio::SOUND_VOLUME, in_unit_range);
|
||||||
|
}
|
||||||
|
|
||||||
// Carregar controls del player 1 desde YAML
|
// Carregar controls del player 1 desde YAML
|
||||||
static void loadPlayer1ControlsFromYaml(const fkyaml::node& yaml) {
|
static void loadPlayer1ControlsFromYaml(const fkyaml::node& yaml) {
|
||||||
if (!yaml.contains("player1")) {
|
if (!yaml.contains("player1")) {
|
||||||
@@ -347,12 +373,21 @@ namespace ConfigYaml {
|
|||||||
if (gp.contains("button_shoot")) {
|
if (gp.contains("button_shoot")) {
|
||||||
player1.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
|
player1.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
|
||||||
}
|
}
|
||||||
|
if (gp.contains("button_start")) {
|
||||||
|
player1.gamepad.button_start = stringToButton(gp["button_start"].get_value<std::string>());
|
||||||
|
}
|
||||||
|
if (gp.contains("button_menu")) {
|
||||||
|
player1.gamepad.button_menu = stringToButton(gp["button_menu"].get_value<std::string>());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carregar nom del gamepad
|
// Carregar nom i path del gamepad assignat
|
||||||
if (p1.contains("gamepad_name")) {
|
if (p1.contains("gamepad_name")) {
|
||||||
player1.gamepad_name = p1["gamepad_name"].get_value<std::string>();
|
player1.gamepad_name = p1["gamepad_name"].get_value<std::string>();
|
||||||
}
|
}
|
||||||
|
if (p1.contains("gamepad_path")) {
|
||||||
|
player1.gamepad_path = p1["gamepad_path"].get_value<std::string>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carregar controls del player 2 desde YAML
|
// Carregar controls del player 2 desde YAML
|
||||||
@@ -395,12 +430,21 @@ namespace ConfigYaml {
|
|||||||
if (gp.contains("button_shoot")) {
|
if (gp.contains("button_shoot")) {
|
||||||
player2.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
|
player2.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
|
||||||
}
|
}
|
||||||
|
if (gp.contains("button_start")) {
|
||||||
|
player2.gamepad.button_start = stringToButton(gp["button_start"].get_value<std::string>());
|
||||||
|
}
|
||||||
|
if (gp.contains("button_menu")) {
|
||||||
|
player2.gamepad.button_menu = stringToButton(gp["button_menu"].get_value<std::string>());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carregar nom del gamepad
|
// Carregar nom i path del gamepad assignat
|
||||||
if (p2.contains("gamepad_name")) {
|
if (p2.contains("gamepad_name")) {
|
||||||
player2.gamepad_name = p2["gamepad_name"].get_value<std::string>();
|
player2.gamepad_name = p2["gamepad_name"].get_value<std::string>();
|
||||||
}
|
}
|
||||||
|
if (p2.contains("gamepad_path")) {
|
||||||
|
player2.gamepad_path = p2["gamepad_path"].get_value<std::string>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Carregar configuración des del file YAML
|
// Carregar configuración des del file YAML
|
||||||
@@ -447,6 +491,7 @@ namespace ConfigYaml {
|
|||||||
// Carregar seccions
|
// Carregar seccions
|
||||||
loadWindowConfigFromYaml(yaml);
|
loadWindowConfigFromYaml(yaml);
|
||||||
loadRenderingConfigFromYaml(yaml);
|
loadRenderingConfigFromYaml(yaml);
|
||||||
|
loadAudioConfigFromYaml(yaml);
|
||||||
loadPlayer1ControlsFromYaml(yaml);
|
loadPlayer1ControlsFromYaml(yaml);
|
||||||
loadPlayer2ControlsFromYaml(yaml);
|
loadPlayer2ControlsFromYaml(yaml);
|
||||||
|
|
||||||
@@ -479,6 +524,17 @@ namespace ConfigYaml {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void saveAudioConfigToYaml(std::ofstream& file) {
|
||||||
|
file << "# AUDIO\n";
|
||||||
|
file << "audio:\n";
|
||||||
|
file << " enabled: " << (audio.enabled ? "true" : "false") << " # ON/OFF general\n";
|
||||||
|
file << " volume: " << audio.volume << " # Master 0.0-1.0\n";
|
||||||
|
file << " music_enabled: " << (audio.music_enabled ? "true" : "false") << "\n";
|
||||||
|
file << " music_volume: " << audio.music_volume << " # 0.0-1.0\n";
|
||||||
|
file << " sound_enabled: " << (audio.sound_enabled ? "true" : "false") << "\n";
|
||||||
|
file << " sound_volume: " << audio.sound_volume << " # 0.0-1.0\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
// Guardar controls del player 1 a YAML
|
// Guardar controls del player 1 a YAML
|
||||||
static void savePlayer1ControlsToYaml(std::ofstream& file) {
|
static void savePlayer1ControlsToYaml(std::ofstream& file) {
|
||||||
file << "# CONTROLS JUGADOR 1\n";
|
file << "# CONTROLS JUGADOR 1\n";
|
||||||
@@ -493,7 +549,10 @@ namespace ConfigYaml {
|
|||||||
file << " button_right: " << buttonToString(player1.gamepad.button_right) << "\n";
|
file << " button_right: " << buttonToString(player1.gamepad.button_right) << "\n";
|
||||||
file << " button_thrust: " << buttonToString(player1.gamepad.button_thrust) << "\n";
|
file << " button_thrust: " << buttonToString(player1.gamepad.button_thrust) << "\n";
|
||||||
file << " button_shoot: " << buttonToString(player1.gamepad.button_shoot) << "\n";
|
file << " button_shoot: " << buttonToString(player1.gamepad.button_shoot) << "\n";
|
||||||
file << " gamepad_name: \"" << player1.gamepad_name << "\" # Buit = primer disponible\n\n";
|
file << " button_start: " << buttonToString(player1.gamepad.button_start) << "\n";
|
||||||
|
file << " button_menu: " << buttonToString(player1.gamepad.button_menu) << "\n";
|
||||||
|
file << " gamepad_name: \"" << player1.gamepad_name << "\" # Buit = primer disponible\n";
|
||||||
|
file << " gamepad_path: \"" << player1.gamepad_path << "\" # Prioritari sobre name\n\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guardar controls del player 2 a YAML
|
// Guardar controls del player 2 a YAML
|
||||||
@@ -510,7 +569,10 @@ namespace ConfigYaml {
|
|||||||
file << " button_right: " << buttonToString(player2.gamepad.button_right) << "\n";
|
file << " button_right: " << buttonToString(player2.gamepad.button_right) << "\n";
|
||||||
file << " button_thrust: " << buttonToString(player2.gamepad.button_thrust) << "\n";
|
file << " button_thrust: " << buttonToString(player2.gamepad.button_thrust) << "\n";
|
||||||
file << " button_shoot: " << buttonToString(player2.gamepad.button_shoot) << "\n";
|
file << " button_shoot: " << buttonToString(player2.gamepad.button_shoot) << "\n";
|
||||||
file << " gamepad_name: \"" << player2.gamepad_name << "\" # Buit = segon disponible\n\n";
|
file << " button_start: " << buttonToString(player2.gamepad.button_start) << "\n";
|
||||||
|
file << " button_menu: " << buttonToString(player2.gamepad.button_menu) << "\n";
|
||||||
|
file << " gamepad_name: \"" << player2.gamepad_name << "\" # Buit = segon disponible\n";
|
||||||
|
file << " gamepad_path: \"" << player2.gamepad_path << "\" # Prioritari sobre name\n\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Guardar configuración al file YAML
|
// Guardar configuración al file YAML
|
||||||
@@ -549,6 +611,8 @@ namespace ConfigYaml {
|
|||||||
file << "# IDIOMA\n";
|
file << "# IDIOMA\n";
|
||||||
file << "locale: " << locale << " # ca | en\n\n";
|
file << "locale: " << locale << " # ca | en\n\n";
|
||||||
|
|
||||||
|
saveAudioConfigToYaml(file);
|
||||||
|
|
||||||
// Guardar controls de jugadors
|
// Guardar controls de jugadors
|
||||||
savePlayer1ControlsToYaml(file);
|
savePlayer1ControlsToYaml(file);
|
||||||
savePlayer2ControlsToYaml(file);
|
savePlayer2ControlsToYaml(file);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ namespace ConfigYaml {
|
|||||||
.key_start = SDL_SCANCODE_2,
|
.key_start = SDL_SCANCODE_2,
|
||||||
},
|
},
|
||||||
.gamepad_name = "",
|
.gamepad_name = "",
|
||||||
|
.gamepad_path = "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ namespace Effects {
|
|||||||
SDL_Color color,
|
SDL_Color color,
|
||||||
float lifetime,
|
float lifetime,
|
||||||
float friction,
|
float friction,
|
||||||
int segment_multiplier) {
|
int segment_multiplier,
|
||||||
|
const Vec2& bullet_impulse_velocity,
|
||||||
|
float piece_scale) {
|
||||||
if (!shape || !shape->isValid()) {
|
if (!shape || !shape->isValid()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -84,7 +86,7 @@ namespace Effects {
|
|||||||
Vec2 world_p2 = transformPoint(local_p2, shape_centre, centro, angle, scale);
|
Vec2 world_p2 = transformPoint(local_p2, shape_centre, centro, angle, scale);
|
||||||
|
|
||||||
// Si el pool es ple, no té sentit continuar amb la resta de segments
|
// Si el pool es ple, no té sentit continuar amb la resta de segments
|
||||||
if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness, velocitat_objecte, velocitat_angular, factor_herencia_visual, color, lifetime, friction)) {
|
if (!spawnDebris(world_p1, world_p2, centro, velocitat_base, brightness, velocitat_objecte, velocitat_angular, factor_herencia_visual, color, lifetime, friction, bullet_impulse_velocity, piece_scale)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,34 +112,47 @@ namespace Effects {
|
|||||||
return segments;
|
return segments;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto DebrisManager::spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction) -> bool {
|
auto DebrisManager::spawnDebris(const Vec2& world_p1_in, const Vec2& world_p2_in, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction, const Vec2& bullet_impulse_velocity, float piece_scale) -> bool {
|
||||||
Debris* debris = findFreeSlot();
|
Debris* debris = findFreeSlot();
|
||||||
if (debris == nullptr) {
|
if (debris == nullptr) {
|
||||||
std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n";
|
std::cerr << "[DebrisManager] Warning: no debris slots disponibles\n";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escala el segment al voltant del seu punt mitjà segons piece_scale
|
||||||
|
// (1.0 = original; 0.3 = "esquerda petita"). La resta del càlcul (angle,
|
||||||
|
// half_length, p1/p2) en deriva naturalment.
|
||||||
|
const Vec2 MID = {.x = (world_p1_in.x + world_p2_in.x) / 2.0F,
|
||||||
|
.y = (world_p1_in.y + world_p2_in.y) / 2.0F};
|
||||||
|
const Vec2 WORLD_P1 = {.x = MID.x + ((world_p1_in.x - MID.x) * piece_scale),
|
||||||
|
.y = MID.y + ((world_p1_in.y - MID.y) * piece_scale)};
|
||||||
|
const Vec2 WORLD_P2 = {.x = MID.x + ((world_p2_in.x - MID.x) * piece_scale),
|
||||||
|
.y = MID.y + ((world_p2_in.y - MID.y) * piece_scale)};
|
||||||
|
|
||||||
// Geometria autoritaritzada: centro + original_angle + original_half_length.
|
// Geometria autoritaritzada: centro + original_angle + original_half_length.
|
||||||
// p1/p2 es reconstrueixen cada frame en update() des d'aquestes dades.
|
// p1/p2 es reconstrueixen cada frame en update() des d'aquestes dades.
|
||||||
const float DX = world_p2.x - world_p1.x;
|
const float DX = WORLD_P2.x - WORLD_P1.x;
|
||||||
const float DY = world_p2.y - world_p1.y;
|
const float DY = WORLD_P2.y - WORLD_P1.y;
|
||||||
debris->centro = {.x = (world_p1.x + world_p2.x) / 2.0F,
|
debris->centro = {.x = (WORLD_P1.x + WORLD_P2.x) / 2.0F,
|
||||||
.y = (world_p1.y + world_p2.y) / 2.0F};
|
.y = (WORLD_P1.y + WORLD_P2.y) / 2.0F};
|
||||||
debris->original_angle = std::atan2(DY, DX);
|
debris->original_angle = std::atan2(DY, DX);
|
||||||
debris->original_half_length = std::sqrt((DX * DX) + (DY * DY)) / 2.0F;
|
debris->original_half_length = std::sqrt((DX * DX) + (DY * DY)) / 2.0F;
|
||||||
debris->p1 = world_p1;
|
debris->p1 = WORLD_P1;
|
||||||
debris->p2 = world_p2;
|
debris->p2 = WORLD_P2;
|
||||||
|
|
||||||
// Direcció radial (desde el centro hacia el segment)
|
// Direcció radial (desde el centro hacia el segment)
|
||||||
Vec2 direccio = computeExplosionDirection(world_p1, world_p2, centro);
|
Vec2 direccio = computeExplosionDirection(WORLD_P1, WORLD_P2, centro);
|
||||||
|
|
||||||
// Velocidad inicial (base ± variació aleatòria + velocity heretada de l'objecte)
|
// Velocidad inicial (base ± variació aleatòria + velocity heretada de l'objecte +
|
||||||
|
// velocitat de la bala escalada per BULLET_IMPULSE_FACTOR).
|
||||||
float speed =
|
float speed =
|
||||||
velocitat_base +
|
velocitat_base +
|
||||||
(((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) *
|
(((std::rand() / static_cast<float>(RAND_MAX)) * 2.0F - 1.0F) *
|
||||||
Defaults::Physics::Debris::VARIACIO_SPEED);
|
Defaults::Physics::Debris::VARIACIO_SPEED);
|
||||||
debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x;
|
debris->velocity.x = (direccio.x * speed) + velocitat_objecte.x +
|
||||||
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y;
|
(bullet_impulse_velocity.x * Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR);
|
||||||
|
debris->velocity.y = (direccio.y * speed) + velocitat_objecte.y +
|
||||||
|
(bullet_impulse_velocity.y * Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR);
|
||||||
debris->acceleration = friction;
|
debris->acceleration = friction;
|
||||||
|
|
||||||
// Rotación de trayectoria (con conversió a tangencial si excedeix cap)
|
// Rotación de trayectoria (con conversió a tangencial si excedeix cap)
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ namespace Effects {
|
|||||||
// - lifetime: temps de vida del debris (s, per defecte TEMPS_VIDA = 2s)
|
// - lifetime: temps de vida del debris (s, per defecte TEMPS_VIDA = 2s)
|
||||||
// - friction: desacceleració del debris (px/s², per defecte ACCELERACIO = -60)
|
// - friction: desacceleració del debris (px/s², per defecte ACCELERACIO = -60)
|
||||||
// - segment_multiplier: nombre de còpies per segment (per defecte 1 = sense duplicar)
|
// - segment_multiplier: nombre de còpies per segment (per defecte 1 = sense duplicar)
|
||||||
|
// - bullet_impulse_velocity: velocitat de la bala que ha causat l'impacte (px/s,
|
||||||
|
// per defecte 0). S'aplica a cada fragment escalada per
|
||||||
|
// Defaults::Physics::Debris::BULLET_IMPULSE_FACTOR, independent de
|
||||||
|
// velocitat_objecte. Permet que els trossos "salten amb la força de la bala"
|
||||||
|
// encara que el cos sigui pesat i amb prou feines es mogui.
|
||||||
|
// - piece_scale: multiplicador de la longitud de cada fragment al spawn
|
||||||
|
// (per defecte 1.0). Útil per a debris "parcial" d'impactes no letals
|
||||||
|
// en enemics HP>1 (trossos petits, com d'esquerda).
|
||||||
void explode(const std::shared_ptr<Graphics::Shape>& shape,
|
void explode(const std::shared_ptr<Graphics::Shape>& shape,
|
||||||
const Vec2& centro,
|
const Vec2& centro,
|
||||||
float angle,
|
float angle,
|
||||||
@@ -56,11 +64,13 @@ namespace Effects {
|
|||||||
const Vec2& velocitat_objecte = {.x = 0.0F, .y = 0.0F},
|
const Vec2& velocitat_objecte = {.x = 0.0F, .y = 0.0F},
|
||||||
float velocitat_angular = 0.0F,
|
float velocitat_angular = 0.0F,
|
||||||
float factor_herencia_visual = 0.0F,
|
float factor_herencia_visual = 0.0F,
|
||||||
const std::string& sound = Defaults::Sound::EXPLOSION,
|
const std::string& sound = Defaults::Sound::ENEMY_EXPLOSION,
|
||||||
SDL_Color color = {0, 0, 0, 0}, // alpha==0 → fragmentos usan oscilador global
|
SDL_Color color = {0, 0, 0, 0}, // alpha==0 → fragmentos usan oscilador global
|
||||||
float lifetime = Defaults::Physics::Debris::TEMPS_VIDA,
|
float lifetime = Defaults::Physics::Debris::TEMPS_VIDA,
|
||||||
float friction = Defaults::Physics::Debris::ACCELERACIO,
|
float friction = Defaults::Physics::Debris::ACCELERACIO,
|
||||||
int segment_multiplier = 1);
|
int segment_multiplier = 1,
|
||||||
|
const Vec2& bullet_impulse_velocity = {.x = 0.0F, .y = 0.0F},
|
||||||
|
float piece_scale = 1.0F);
|
||||||
|
|
||||||
// Actualitzar todos los fragments active
|
// Actualitzar todos los fragments active
|
||||||
void update(float delta_time);
|
void update(float delta_time);
|
||||||
@@ -97,7 +107,7 @@ namespace Effects {
|
|||||||
-> std::vector<std::pair<Vec2, Vec2>>;
|
-> std::vector<std::pair<Vec2, Vec2>>;
|
||||||
// Inicialitza un debris en un slot lliure i el deixa actiu. Retorna
|
// Inicialitza un debris en un slot lliure i el deixa actiu. Retorna
|
||||||
// false si el pool está ple (la cridadora ha d'aturar el bucle).
|
// false si el pool está ple (la cridadora ha d'aturar el bucle).
|
||||||
auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction) -> bool;
|
auto spawnDebris(const Vec2& world_p1, const Vec2& world_p2, const Vec2& centro, float velocitat_base, float brightness, const Vec2& velocitat_objecte, float velocitat_angular, float factor_herencia_visual, SDL_Color color, float lifetime, float friction, const Vec2& bullet_impulse_velocity, float piece_scale) -> bool;
|
||||||
static void applyAngularVelocity(Debris& debris, const Vec2& direccio, float velocitat_angular);
|
static void applyAngularVelocity(Debris& debris, const Vec2& direccio, float velocitat_angular);
|
||||||
static void applyVisualRotation(Debris& debris, float velocitat_angular, float factor_herencia_visual);
|
static void applyVisualRotation(Debris& debris, float velocitat_angular, float factor_herencia_visual);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ namespace Effects {
|
|||||||
|
|
||||||
TrailManager::TrailManager(Rendering::Renderer* renderer)
|
TrailManager::TrailManager(Rendering::Renderer* renderer)
|
||||||
: renderer_(renderer),
|
: renderer_(renderer),
|
||||||
star_shape_(Graphics::ShapeLoader::load("star.shp")) {
|
star_shape_(Graphics::ShapeLoader::load("effect/starfield.shp")) {
|
||||||
if (!star_shape_ || !star_shape_->isValid()) {
|
if (!star_shape_ || !star_shape_->isValid()) {
|
||||||
std::cerr << "[TrailManager] Warning: no s'ha pogut load star.shp\n";
|
std::cerr << "[TrailManager] Warning: no s'ha pogut load effect/starfield.shp\n";
|
||||||
}
|
}
|
||||||
for (auto& particle : pool_) {
|
for (auto& particle : pool_) {
|
||||||
particle.active = false;
|
particle.active = false;
|
||||||
|
|||||||
@@ -14,38 +14,39 @@
|
|||||||
#include "core/rendering/shape_renderer.hpp"
|
#include "core/rendering/shape_renderer.hpp"
|
||||||
#include "core/types.hpp"
|
#include "core/types.hpp"
|
||||||
#include "game/constants.hpp"
|
#include "game/constants.hpp"
|
||||||
|
#include "game/entities/bullet_config.hpp"
|
||||||
|
#include "game/entities/bullet_registry.hpp"
|
||||||
|
|
||||||
Bullet::Bullet(Rendering::Renderer* renderer)
|
Bullet::Bullet(Rendering::Renderer* renderer)
|
||||||
: Entity(renderer) {
|
: Entity(renderer),
|
||||||
// Brightness específico para balas
|
config_(&BulletRegistry::get()) {
|
||||||
brightness_ = Defaults::Brightness::BALA;
|
brightness_ = Defaults::Brightness::BALA;
|
||||||
|
|
||||||
// Configuración del cuerpo físico.
|
// Cinemàtiques pures: no col·lisionen al PhysicsWorld (body_.radius = 0).
|
||||||
// Las balas son cinemáticas: no colisionan con otros bodies ni paredes.
|
// El gameplay (GameScene) gestiona els hits via checkCollisionSwept i la
|
||||||
// El gameplay (GameScene) gestiona los hits con check_collision y la
|
// sortida del PLAYAREA.
|
||||||
// salida del PLAYAREA. Por eso radius=0 en el world (no participa en
|
body_.setMass(config_->physics.mass);
|
||||||
// resolveBodyCollisions ni resolveBoundsCollisions).
|
body_.radius = 0.0F;
|
||||||
body_.setMass(0.5F); // Ligera (no afecta a nadie, pero por consistencia)
|
body_.restitution = config_->physics.restitution;
|
||||||
body_.radius = 0.0F; // Sin colisión física (cinemática pura)
|
body_.linear_damping = config_->physics.linear_damping;
|
||||||
body_.restitution = 0.0F; // Irrelevante (no rebota)
|
body_.angular_damping = config_->physics.angular_damping;
|
||||||
body_.linear_damping = 0.0F; // Sin fricción (movimiento rectilíneo uniforme)
|
|
||||||
body_.angular_damping = 0.0F;
|
|
||||||
|
|
||||||
// Cargar shape compartida desde archivo
|
shape_ = Graphics::ShapeLoader::load(config_->shape.path);
|
||||||
shape_ = Graphics::ShapeLoader::load("bullet.shp");
|
|
||||||
if (!shape_ || !shape_->isValid()) {
|
if (!shape_ || !shape_->isValid()) {
|
||||||
std::cerr << "[Bullet] Error: no s'ha pogut load bullet.shp" << '\n';
|
std::cerr << "[Bullet] Error: no s'ha pogut carregar " << config_->shape.path << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Radi de col·lisió derivat del cercle circumscrit de la shape.
|
||||||
|
const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F;
|
||||||
|
collision_radius_ = BOUNDING * config_->shape.scale * config_->shape.collision_factor;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Bullet::init() {
|
void Bullet::init() {
|
||||||
// Inicialment inactiva
|
|
||||||
is_active_ = false;
|
is_active_ = false;
|
||||||
center_ = {.x = 0.0F, .y = 0.0F};
|
center_ = {.x = 0.0F, .y = 0.0F};
|
||||||
prev_position_ = {.x = 0.0F, .y = 0.0F};
|
prev_position_ = {.x = 0.0F, .y = 0.0F};
|
||||||
angle_ = 0.0F;
|
angle_ = 0.0F;
|
||||||
|
|
||||||
// Reset del cuerpo físico
|
|
||||||
body_.position = Vec2{};
|
body_.position = Vec2{};
|
||||||
body_.velocity = Vec2{};
|
body_.velocity = Vec2{};
|
||||||
body_.angle = 0.0F;
|
body_.angle = 0.0F;
|
||||||
@@ -53,29 +54,43 @@ void Bullet::init() {
|
|||||||
body_.clearAccumulators();
|
body_.clearAccumulators();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id) {
|
void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed, const BulletConfig* cfg) {
|
||||||
// Activar bullet
|
|
||||||
is_active_ = true;
|
is_active_ = true;
|
||||||
|
|
||||||
// Almacenar propietario (0=P1, 1=P2)
|
|
||||||
owner_id_ = owner_id;
|
owner_id_ = owner_id;
|
||||||
|
|
||||||
// Posición y orientación iniciales = ship
|
// Si no es passa cfg, restaurem al config per defecte (BulletRegistry::get):
|
||||||
|
// els slots són reutilitzables i una bala que abans ha estat disparada amb
|
||||||
|
// una variant (p.ex. bullet_long d'enemic) ha de tornar al bullet.shp del
|
||||||
|
// player quan aquest la reutilitza.
|
||||||
|
const BulletConfig* effective = (cfg != nullptr) ? cfg : &BulletRegistry::get();
|
||||||
|
if (effective != config_) {
|
||||||
|
config_ = effective;
|
||||||
|
shape_ = Graphics::ShapeLoader::load(config_->shape.path);
|
||||||
|
if (!shape_ || !shape_->isValid()) {
|
||||||
|
std::cerr << "[Bullet] Error: no s'ha pogut carregar " << config_->shape.path << '\n';
|
||||||
|
}
|
||||||
|
const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F;
|
||||||
|
collision_radius_ = BOUNDING * config_->shape.scale * config_->shape.collision_factor;
|
||||||
|
body_.setMass(config_->physics.mass);
|
||||||
|
body_.restitution = config_->physics.restitution;
|
||||||
|
body_.linear_damping = config_->physics.linear_damping;
|
||||||
|
body_.angular_damping = config_->physics.angular_damping;
|
||||||
|
}
|
||||||
|
|
||||||
center_ = position;
|
center_ = position;
|
||||||
prev_position_ = position; // Al spawn no hi ha moviment encara: swept degenera a punt-cercle
|
prev_position_ = position; // spawn: swept degenera a punt-cercle
|
||||||
angle_ = angle;
|
angle_ = angle;
|
||||||
|
|
||||||
// Sincronizar el body físico: posición + velocidad cartesiana
|
// Sincronizar el body físic: posició + velocitat cartesiana.
|
||||||
// angle - PI/2 porque angle=0 apunta hacia arriba (eje Y negativo SDL)
|
// angle - PI/2 perquè angle=0 apunta cap amunt (eje Y negatiu SDL).
|
||||||
body_.position = position;
|
body_.position = position;
|
||||||
body_.angle = angle;
|
body_.angle = angle;
|
||||||
const float DIR_X = std::cos(angle - (Constants::PI / 2.0F));
|
const float DIR_X = std::cos(angle - (Constants::PI / 2.0F));
|
||||||
const float DIR_Y = std::sin(angle - (Constants::PI / 2.0F));
|
const float DIR_Y = std::sin(angle - (Constants::PI / 2.0F));
|
||||||
body_.velocity = Vec2{.x = DIR_X * Defaults::Game::BULLET_SPEED, .y = DIR_Y * Defaults::Game::BULLET_SPEED};
|
body_.velocity = Vec2{.x = DIR_X * bullet_speed, .y = DIR_Y * bullet_speed};
|
||||||
body_.angular_velocity = 0.0F;
|
body_.angular_velocity = 0.0F;
|
||||||
body_.clearAccumulators();
|
body_.clearAccumulators();
|
||||||
|
|
||||||
// Reproducir sonido de disparo láser
|
|
||||||
Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME);
|
Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,24 +102,18 @@ void Bullet::update(float /*delta_time*/) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Bullet::postUpdate(float /*delta_time*/) {
|
void Bullet::postUpdate(float /*delta_time*/) {
|
||||||
// Captura la posició al final del frame anterior abans de sobreescriure center_;
|
|
||||||
// així el sistema de col·lisions pot fer swept (segment-vs-cercle) entre prev_position_
|
|
||||||
// i la nova center_, evitant tunneling a velocitats altes.
|
|
||||||
prev_position_ = center_;
|
prev_position_ = center_;
|
||||||
center_ = body_.position;
|
center_ = body_.position;
|
||||||
// angle_ no cambia (las balas no rotan visualmente).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Bullet::desactivar() {
|
void Bullet::desactivar() {
|
||||||
is_active_ = false;
|
is_active_ = false;
|
||||||
// Detener el cuerpo físico para que no acumule deriva mientras inactiva.
|
|
||||||
body_.velocity = Vec2{};
|
body_.velocity = Vec2{};
|
||||||
body_.angular_velocity = 0.0F;
|
body_.angular_velocity = 0.0F;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Bullet::draw() const {
|
void Bullet::draw() const {
|
||||||
if (is_active_ && shape_) {
|
if (is_active_ && shape_) {
|
||||||
// Les bales roten segons l'angle de trayectòria (estático tras disparo)
|
Rendering::renderShape(renderer_, shape_, center_, angle_, config_->shape.scale, 1.0F, brightness_, config_->colors.normal);
|
||||||
Rendering::renderShape(renderer_, shape_, center_, angle_, 1.0F, 1.0F, brightness_, Defaults::Palette::BULLET);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
#include "core/defaults.hpp"
|
|
||||||
#include "core/entities/entity.hpp"
|
#include "core/entities/entity.hpp"
|
||||||
#include "core/types.hpp"
|
#include "core/types.hpp"
|
||||||
|
|
||||||
|
// Forward declaration — la definició completa s'inclou només al .cpp.
|
||||||
|
struct BulletConfig;
|
||||||
|
|
||||||
class Bullet : public Entities::Entity {
|
class Bullet : public Entities::Entity {
|
||||||
public:
|
public:
|
||||||
Bullet()
|
Bullet()
|
||||||
@@ -17,7 +19,11 @@ class Bullet : public Entities::Entity {
|
|||||||
explicit Bullet(Rendering::Renderer* renderer);
|
explicit Bullet(Rendering::Renderer* renderer);
|
||||||
|
|
||||||
void init() override;
|
void init() override;
|
||||||
void fire(const Vec2& position, float angle, uint8_t owner_id);
|
// cfg = nullptr → manté el config actual (per defecte: BulletRegistry::get()).
|
||||||
|
// cfg != nullptr → substitueix el config per a aquesta bala (recarrega
|
||||||
|
// shape, recalcula collision_radius_, mass, etc.). Útil per a bales
|
||||||
|
// d'enemic amb shape pròpia.
|
||||||
|
void fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed, const BulletConfig* cfg = nullptr);
|
||||||
void update(float delta_time) override;
|
void update(float delta_time) override;
|
||||||
void postUpdate(float delta_time) override;
|
void postUpdate(float delta_time) override;
|
||||||
void draw() const override;
|
void draw() const override;
|
||||||
@@ -25,25 +31,25 @@ class Bullet : public Entities::Entity {
|
|||||||
// Override: Interfaz de Entity
|
// Override: Interfaz de Entity
|
||||||
[[nodiscard]] auto isActive() const -> bool override { return is_active_; }
|
[[nodiscard]] auto isActive() const -> bool override { return is_active_; }
|
||||||
|
|
||||||
// Override: Interfaz de colisión (gameplay-level: PLAYAREA bounds-check)
|
// Override: Interfaz de colisión (radi derivat al ctor des del shape).
|
||||||
[[nodiscard]] auto getCollisionRadius() const -> float override {
|
[[nodiscard]] auto getCollisionRadius() const -> float override { return collision_radius_; }
|
||||||
return Defaults::Entities::BULLET_RADIUS;
|
|
||||||
}
|
|
||||||
[[nodiscard]] auto isCollidable() const -> bool override {
|
[[nodiscard]] auto isCollidable() const -> bool override {
|
||||||
return is_active_;
|
return is_active_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Configuració associada (vàlida des del ctor — la bala l'agafa del BulletRegistry).
|
||||||
|
[[nodiscard]] auto getConfig() const -> const BulletConfig& { return *config_; }
|
||||||
|
|
||||||
// Getters (API pública sin cambios)
|
// Getters (API pública sin cambios)
|
||||||
[[nodiscard]] auto getOwnerId() const -> uint8_t { return owner_id_; }
|
[[nodiscard]] auto getOwnerId() const -> uint8_t { return owner_id_; }
|
||||||
// Posició al final del frame anterior, per a CCD segment-vs-cercle.
|
|
||||||
[[nodiscard]] auto getPrevPosition() const -> const Vec2& { return prev_position_; }
|
[[nodiscard]] auto getPrevPosition() const -> const Vec2& { return prev_position_; }
|
||||||
void desactivar();
|
void desactivar();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Miembros específicos de Bullet (heredados: renderer_, shape_, center_, angle_, brightness_, body_).
|
const BulletConfig* config_{nullptr}; // apunta al BulletRegistry; vàlid post-ctor
|
||||||
// Inicializados en la declaración para que tanto el ctor por defecto como el que toma renderer
|
float collision_radius_{0.0F}; // derivat: shape.bounding_radius × scale × collision_factor
|
||||||
// dejen el objeto en estado coherente (proyectil inactivo, sin owner).
|
|
||||||
bool is_active_{false};
|
bool is_active_{false};
|
||||||
uint8_t owner_id_{0}; // 0=P1, 1=P2
|
uint8_t owner_id_{0}; // 0=P1, 1=P2
|
||||||
Vec2 prev_position_{}; // Posició al final del frame anterior (per a swept collision)
|
Vec2 prev_position_{}; // posició al final del frame anterior (swept CCD)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// bullet_config.cpp - Implementació del parser de BulletConfig
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "game/entities/bullet_config.hpp"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <exception>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto parseColor(const fkyaml::node& node, SDL_Color& out) -> bool {
|
||||||
|
if (!node.is_sequence() || node.size() != 3) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto R = node[0].get_value<uint32_t>();
|
||||||
|
const auto G = node[1].get_value<uint32_t>();
|
||||||
|
const auto B = node[2].get_value<uint32_t>();
|
||||||
|
out = SDL_Color{
|
||||||
|
.r = static_cast<uint8_t>(R),
|
||||||
|
.g = static_cast<uint8_t>(G),
|
||||||
|
.b = static_cast<uint8_t>(B),
|
||||||
|
.a = 255};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parseShape(const fkyaml::node& node, BulletConfig::ShapeCfg& out) -> bool {
|
||||||
|
if (!node.contains("shape") || !node["shape"].contains("path")) {
|
||||||
|
std::cerr << "[BulletConfig] Error: falta 'shape.path'\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto& s = node["shape"];
|
||||||
|
out.path = s["path"].get_value<std::string>();
|
||||||
|
out.scale = s.contains("scale") ? s["scale"].get_value<float>() : 1.0F;
|
||||||
|
out.collision_factor = s.contains("collision_factor")
|
||||||
|
? s["collision_factor"].get_value<float>()
|
||||||
|
: 1.0F;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parsePhysics(const fkyaml::node& node, BulletConfig::PhysicsCfg& out) -> bool {
|
||||||
|
if (!node.contains("physics")) {
|
||||||
|
std::cerr << "[BulletConfig] Error: falta 'physics'\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto& p = node["physics"];
|
||||||
|
out.mass = p["mass"].get_value<float>();
|
||||||
|
out.restitution = p["restitution"].get_value<float>();
|
||||||
|
out.linear_damping = p["linear_damping"].get_value<float>();
|
||||||
|
out.angular_damping = p["angular_damping"].get_value<float>();
|
||||||
|
out.impact_momentum_factor = p["impact_momentum_factor"].get_value<float>();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parseColors(const fkyaml::node& node, BulletConfig::ColorsCfg& out) -> bool {
|
||||||
|
if (!node.contains("colors") || !parseColor(node["colors"]["normal"], out.normal)) {
|
||||||
|
std::cerr << "[BulletConfig] Error: 'colors.normal' no és [r,g,b]\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto BulletConfig::fromYaml(const fkyaml::node& node) -> std::optional<BulletConfig> {
|
||||||
|
try {
|
||||||
|
BulletConfig cfg;
|
||||||
|
cfg.name = node.contains("name") ? node["name"].get_value<std::string>() : "bullet";
|
||||||
|
|
||||||
|
if (!parseShape(node, cfg.shape)) { return std::nullopt; }
|
||||||
|
if (!parsePhysics(node, cfg.physics)) { return std::nullopt; }
|
||||||
|
if (!parseColors(node, cfg.colors)) { return std::nullopt; }
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[BulletConfig] Excepció parsejant: " << e.what() << '\n';
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// bullet_config.hpp - Configuració de la bala carregada des de YAML
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Paral·lel a Player/EnemyConfig. Una sola instància a tot el joc (per ara);
|
||||||
|
// es comparteix entre totes les bales actives via BulletRegistry.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "external/fkyaml_node.hpp"
|
||||||
|
|
||||||
|
struct BulletConfig {
|
||||||
|
struct ShapeCfg {
|
||||||
|
std::string path;
|
||||||
|
float scale;
|
||||||
|
float collision_factor;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PhysicsCfg {
|
||||||
|
float mass;
|
||||||
|
float restitution;
|
||||||
|
float linear_damping;
|
||||||
|
float angular_damping;
|
||||||
|
float impact_momentum_factor; // factor de transferència bullet→enemic
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ColorsCfg {
|
||||||
|
SDL_Color normal;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
ShapeCfg shape;
|
||||||
|
PhysicsCfg physics;
|
||||||
|
ColorsCfg colors;
|
||||||
|
|
||||||
|
static auto fromYaml(const fkyaml::node& node) -> std::optional<BulletConfig>;
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user