Compare commits
36 Commits
| 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 |
@@ -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
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
project(orni VERSION 0.8.0 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")
|
||||||
|
|||||||
@@ -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)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ name: bullet
|
|||||||
# Shape de la bala. El bounding_radius del .shp dóna el hitbox base (~3 px);
|
# Shape de la bala. El bounding_radius del .shp dóna el hitbox base (~3 px);
|
||||||
# scale el modula visualment i pel hitbox.
|
# scale el modula visualment i pel hitbox.
|
||||||
shape:
|
shape:
|
||||||
path: bullet.shp
|
path: bullet/basic.shp
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
collision_factor: 1.0
|
collision_factor: 1.0
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -2,7 +2,7 @@ name: pentagon
|
|||||||
ai_type: pentagon # Validat contra el directori; mapeja a EnemyType::PENTAGON.
|
ai_type: pentagon # Validat contra el directori; mapeja a EnemyType::PENTAGON.
|
||||||
|
|
||||||
shape:
|
shape:
|
||||||
path: enemy_pentagon.shp
|
path: enemy/pentagon.shp
|
||||||
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||||
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||||
|
|
||||||
@@ -55,10 +55,14 @@ colors:
|
|||||||
score: 100
|
score: 100
|
||||||
|
|
||||||
events:
|
events:
|
||||||
# Comportament clàssic: dos impactes per matar (set_hurt entra wounded;
|
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
|
||||||
# el segon hit detecta wounded i destrueix automàticament).
|
# 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:
|
on_hit:
|
||||||
|
- action: decrease_health
|
||||||
- action: apply_impulse
|
- action: apply_impulse
|
||||||
|
on_no_health:
|
||||||
- action: set_hurt
|
- action: set_hurt
|
||||||
on_hurt_end:
|
on_hurt_end:
|
||||||
- action: destroy
|
- action: destroy
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: pinwheel
|
|||||||
ai_type: pinwheel # Validat contra el directori; mapeja a EnemyType::PINWHEEL.
|
ai_type: pinwheel # Validat contra el directori; mapeja a EnemyType::PINWHEEL.
|
||||||
|
|
||||||
shape:
|
shape:
|
||||||
path: enemy_pinwheel.shp
|
path: enemy/pinwheel.shp
|
||||||
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||||
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||||
|
|
||||||
@@ -55,8 +55,11 @@ colors:
|
|||||||
score: 200
|
score: 200
|
||||||
|
|
||||||
events:
|
events:
|
||||||
|
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
|
||||||
on_hit:
|
on_hit:
|
||||||
|
- action: decrease_health
|
||||||
- action: apply_impulse
|
- action: apply_impulse
|
||||||
|
on_no_health:
|
||||||
- action: set_hurt
|
- action: set_hurt
|
||||||
on_hurt_end:
|
on_hurt_end:
|
||||||
- action: destroy
|
- action: destroy
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: player_ship
|
name: player_ship
|
||||||
|
|
||||||
# Shape de la nau. Resolt per ShapeLoader (busca a "shapes/<path>").
|
# Shape de la nau. Resolt per ShapeLoader (busca a "shapes/<path>").
|
||||||
# Nota: el segon jugador rep un override del shape ("ship2.shp") al ctor.
|
# 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
|
# Quan s'introdueixin variants reals de nau, es crearà un YAML separat
|
||||||
# per cada model.
|
# per cada model.
|
||||||
#
|
#
|
||||||
@@ -10,7 +10,7 @@ name: player_ship
|
|||||||
# automàtic de la shape; tocar només si el feel del hitbox
|
# automàtic de la shape; tocar només si el feel del hitbox
|
||||||
# no quadra amb la silueta visual (default 1.0).
|
# no quadra amb la silueta visual (default 1.0).
|
||||||
shape:
|
shape:
|
||||||
path: ship.shp
|
path: ship/arrow.shp
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
collision_factor: 1.0
|
collision_factor: 1.0
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: square
|
|||||||
ai_type: square # Validat contra el directori; mapeja a EnemyType::SQUARE.
|
ai_type: square # Validat contra el directori; mapeja a EnemyType::SQUARE.
|
||||||
|
|
||||||
shape:
|
shape:
|
||||||
path: enemy_square.shp
|
path: enemy/square.shp
|
||||||
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
|
||||||
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
|
||||||
|
|
||||||
@@ -15,10 +15,11 @@ physics:
|
|||||||
linear_damping: 0.0
|
linear_damping: 0.0
|
||||||
angular_damping: 0.0
|
angular_damping: 0.0
|
||||||
|
|
||||||
behavior:
|
ai:
|
||||||
# Square: tracking discret cap a la nau cada N segons.
|
# Square: persecució contínua del ship més proper (steering suau, "tanc lent").
|
||||||
tracking_strength: 0.5 # Interpolació LERP cap a la direcció desitjada (0..1)
|
movement:
|
||||||
tracking_interval: 1.0 # segons entre updates d'angle
|
type: chase
|
||||||
|
chase_strength: 0.5 # Força/segon de la LERP cap a la direcció ideal (1.0 = ~1s per realinear)
|
||||||
|
|
||||||
animation:
|
animation:
|
||||||
pulse:
|
pulse:
|
||||||
@@ -55,8 +56,11 @@ colors:
|
|||||||
score: 150
|
score: 150
|
||||||
|
|
||||||
events:
|
events:
|
||||||
|
# HP=1 (default): decrement → on_no_health → set_hurt → wounded → mort.
|
||||||
on_hit:
|
on_hit:
|
||||||
|
- action: decrease_health
|
||||||
- action: apply_impulse
|
- action: apply_impulse
|
||||||
|
on_no_health:
|
||||||
- action: set_hurt
|
- action: set_hurt
|
||||||
on_hurt_end:
|
on_hurt_end:
|
||||||
- action: destroy
|
- action: destroy
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: star
|
|||||||
ai_type: star # Validat contra el directori; mapeja a EnemyType::STAR.
|
ai_type: star # Validat contra el directori; mapeja a EnemyType::STAR.
|
||||||
|
|
||||||
shape:
|
shape:
|
||||||
path: star_5.shp
|
path: enemy/star.shp
|
||||||
scale: 0.7 # Lleugerament més petit que els altres enemics per diferenciar visualment.
|
scale: 0.7 # Lleugerament més petit que els altres enemics per diferenciar visualment.
|
||||||
collision_factor: 1.0
|
collision_factor: 1.0
|
||||||
|
|
||||||
@@ -15,10 +15,20 @@ physics:
|
|||||||
linear_damping: 0.0
|
linear_damping: 0.0
|
||||||
angular_damping: 0.0
|
angular_damping: 0.0
|
||||||
|
|
||||||
behavior:
|
ai:
|
||||||
# Hereta el comportament de Pentagon (zigzag esquivador).
|
# Movement: zigzag esquivador (com Pentagon).
|
||||||
angle_change_max: 1.0
|
movement:
|
||||||
zigzag_prob_per_second: 0.8
|
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:
|
animation:
|
||||||
pulse:
|
pulse:
|
||||||
@@ -56,8 +66,10 @@ score: 100
|
|||||||
|
|
||||||
events:
|
events:
|
||||||
# STAR: mor al primer impacte, sense passar per wounded.
|
# STAR: mor al primer impacte, sense passar per wounded.
|
||||||
|
# HP=1 (default): decrement → on_no_health → destroy directe (sense wounded).
|
||||||
on_hit:
|
on_hit:
|
||||||
- action: apply_impulse
|
- action: decrease_health
|
||||||
|
on_no_health:
|
||||||
- action: destroy
|
- action: destroy
|
||||||
on_destroy:
|
on_destroy:
|
||||||
- action: add_score
|
- action: add_score
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# bullet_double.shp - Bala anular (dos cercles concèntrics)
|
# bullet/double.shp - Bala anular (dos cercles concèntrics)
|
||||||
# © 2026 JailDesigner
|
# © 2026 JailDesigner
|
||||||
#
|
#
|
||||||
# Dos octàgons concèntrics al centre (0,0):
|
# Dos octàgons concèntrics al centre (0,0):
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
# - Interior: radi 2 (lleugerament més petit que la bala estàndard)
|
# - Interior: radi 2 (lleugerament més petit que la bala estàndard)
|
||||||
# Aspecte d'anell / aura de plasma. Bounding radius natiu = 4.
|
# Aspecte d'anell / aura de plasma. Bounding radius natiu = 4.
|
||||||
|
|
||||||
name: bullet_double
|
name: double
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -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,28 +0,0 @@
|
|||||||
# bullet_long.shp - Bala allargada (dos octàgons tangents + tapes superior i inferior)
|
|
||||||
# © 2026 JailDesigner
|
|
||||||
#
|
|
||||||
# Dos cercles (octàgons radi 3) tangents externament al punt (0,0), units
|
|
||||||
# per una línia horitzontal superior i una d'inferior. La silueta resultant
|
|
||||||
# és una càpsula amb la separació visible dels dos cercles al centre.
|
|
||||||
#
|
|
||||||
# Geometria:
|
|
||||||
# Centre octàgon esquerre: (-3, 0)
|
|
||||||
# Centre octàgon dret: ( 3, 0)
|
|
||||||
# Punt de tangència: ( 0, 0)
|
|
||||||
# Bounding radius natiu ≈ 6 (extrem horitzontal a x=±6).
|
|
||||||
|
|
||||||
name: bullet_long
|
|
||||||
scale: 1.0
|
|
||||||
center: 0, 0
|
|
||||||
|
|
||||||
# Octàgon esquerre (centre x=-3, radi 3)
|
|
||||||
polyline: -3,-3 -0.88,-2.12 0,0 -0.88,2.12 -3,3 -5.12,2.12 -6,0 -5.12,-2.12 -3,-3
|
|
||||||
|
|
||||||
# Octàgon dret (centre x=3, radi 3)
|
|
||||||
polyline: 3,-3 5.12,-2.12 6,0 5.12,2.12 3,3 0.88,2.12 0,0 0.88,-2.12 3,-3
|
|
||||||
|
|
||||||
# Tapa superior: uneix el cim de l'octàgon esquerre amb el del dret
|
|
||||||
polyline: -3,-3 3,-3
|
|
||||||
|
|
||||||
# Tapa inferior: uneix la base de l'octàgon esquerre amb la del dret
|
|
||||||
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
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# star_5.shp - ORNI enemic (estrella de 5 puntes, només perímetre)
|
# enemy/star.shp - ORNI enemic (estrella de 5 puntes, només perímetre)
|
||||||
# © 2026 JailDesigner
|
# © 2026 JailDesigner
|
||||||
#
|
#
|
||||||
# Pentagrama clàssic: 5 vèrtexs exteriors (radi 20) alternant amb 5 vèrtexs
|
# Pentagrama clàssic: 5 vèrtexs exteriors (radi 20) alternant amb 5 vèrtexs
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
# Sense línies interiors: una única polyline que recorre el perímetre.
|
# Sense línies interiors: una única polyline que recorre el perímetre.
|
||||||
# Bounding radius natiu ≈ 20 (alineat amb pentagon/square/pinwheel).
|
# Bounding radius natiu ≈ 20 (alineat amb pentagon/square/pinwheel).
|
||||||
|
|
||||||
name: star_5
|
name: star
|
||||||
scale: 1.0
|
scale: 1.0
|
||||||
center: 0, 0
|
center: 0, 0
|
||||||
|
|
||||||
@@ -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.
+158
-144
@@ -1,169 +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 4 tipus al 25% per mostrar-los junts
|
# 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: 25
|
spawn_interval: 0.5
|
||||||
cuadrado: 25
|
next: all_dead
|
||||||
molinillo: 25
|
- spawn: [pentagon, pentagon, square, square]
|
||||||
star: 25
|
spawn_interval: 0.4
|
||||||
difficulty_multipliers:
|
next: end
|
||||||
speed_multiplier: 0.7
|
|
||||||
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
@@ -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
|
||||||
|
|||||||
@@ -16,3 +16,30 @@ namespace Defaults::Enemies::Spawn {
|
|||||||
constexpr int MAX_SPAWN_ATTEMPTS = 50;
|
constexpr int MAX_SPAWN_ATTEMPTS = 50;
|
||||||
|
|
||||||
} // namespace Defaults::Enemies::Spawn
|
} // namespace Defaults::Enemies::Spawn
|
||||||
|
|
||||||
|
namespace Defaults::Enemies::Visual {
|
||||||
|
|
||||||
|
// Duració del "flash" que dispara l'acció FLASH (feedback per impacte
|
||||||
|
// parcial en enemics HP>1). Curt: l'efecte ha de llegir-se com un cop,
|
||||||
|
// no com una transició.
|
||||||
|
constexpr float FLASH_DURATION = 0.08F;
|
||||||
|
|
||||||
|
} // namespace Defaults::Enemies::Visual
|
||||||
|
|
||||||
|
namespace Defaults::Enemies::Debris {
|
||||||
|
|
||||||
|
// Escala dels fragments per a l'acció CREATE_DEBRIS_PARTIAL (xip d'impacte
|
||||||
|
// en enemics HP>1). 0.3 = trossos petits, com de "casc esquerdat".
|
||||||
|
constexpr float PARTIAL_PIECE_SCALE = 0.3F;
|
||||||
|
|
||||||
|
} // namespace Defaults::Enemies::Debris
|
||||||
|
|
||||||
|
namespace Defaults::Enemies::Fireworks {
|
||||||
|
|
||||||
|
// Paràmetres del firework "petit" per a l'acció CREATE_FIREWORKS_SMALL
|
||||||
|
// (feedback per impacte parcial en enemics HP>1). Pocs punts i baixa
|
||||||
|
// velocitat: una espurna breu, no una explosió.
|
||||||
|
constexpr int SMALL_N_POINTS = 20;
|
||||||
|
constexpr float SMALL_SPEED = 250.0F;
|
||||||
|
|
||||||
|
} // namespace Defaults::Enemies::Fireworks
|
||||||
|
|||||||
@@ -3,10 +3,24 @@
|
|||||||
|
|
||||||
#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
|
||||||
|
|
||||||
|
// Total real de slots a l'array global bullets_: zona P1, zona P2 i zona enemic.
|
||||||
|
// Reservar zones impedeix que les bales d'enemic ocupin slots del jugador.
|
||||||
|
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
|
// SHIP_RADIUS / ENEMY_RADIUS / BULLET_RADIUS han migrat: ara cada entitat
|
||||||
// calcula el seu collision_radius com a
|
// calcula el seu collision_radius com a
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
#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
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ namespace Defaults::Physics::Debris {
|
|||||||
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
|
// 1.0 = inèrcia completa; >1.0 amplifica la deriva; <1.0 la atenua.
|
||||||
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
|
constexpr float ENEMY_VELOCITY_INHERITANCE = 1.0F;
|
||||||
|
|
||||||
|
// Velocitat de la bala traspassada a cada fragment de debris al moment
|
||||||
|
// 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;
|
||||||
|
|
||||||
// Tuneig específic de l'explosió d'enemic (overrides als defaults
|
// Tuneig específic de l'explosió d'enemic (overrides als defaults
|
||||||
// que es passen com a paràmetres opcionals a explode()).
|
// que es passen com a paràmetres opcionals a explode()).
|
||||||
constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més
|
constexpr float ENEMY_LIFETIME = 2.5F; // Vida mínima del debris (s) — els que segueixen movent-se viuen més
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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/")) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -26,7 +26,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 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
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
#include "core/input/define_inputs.hpp"
|
#include "core/input/define_inputs.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -154,9 +153,12 @@ namespace System {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto DefineInputs::isInUse(int code) const -> bool {
|
auto DefineInputs::isInUse(int code) const -> bool {
|
||||||
return std::ranges::any_of(sequence_, [code](const Step& s) {
|
for (const auto& s : sequence_) {
|
||||||
return s.captured == code;
|
if (s.captured == code) {
|
||||||
});
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DefineInputs::captureAndAdvance(int code) {
|
void DefineInputs::captureAndAdvance(int code) {
|
||||||
|
|||||||
+19
-11
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#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
|
||||||
@@ -166,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
|
||||||
@@ -307,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";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,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";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,9 +445,13 @@ auto Input::addGamepad(int device_index) -> std::string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -106,8 +106,11 @@ 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
|
// Autoassignacio de primer arranque: si cap dels dos jugadors te mando
|
||||||
// assignat al config, repartim els que hi haja detectats (P1 = pad 0,
|
// assignat al config, repartim els que hi haja detectats (P1 = pad 0,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -54,10 +54,29 @@ void Bullet::init() {
|
|||||||
body_.clearAccumulators();
|
body_.clearAccumulators();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed) {
|
void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed, const BulletConfig* cfg) {
|
||||||
is_active_ = true;
|
is_active_ = true;
|
||||||
owner_id_ = owner_id;
|
owner_id_ = owner_id;
|
||||||
|
|
||||||
|
// 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; // spawn: swept degenera a punt-cercle
|
prev_position_ = position; // spawn: swept degenera a punt-cercle
|
||||||
angle_ = angle;
|
angle_ = angle;
|
||||||
@@ -95,6 +114,6 @@ void Bullet::desactivar() {
|
|||||||
|
|
||||||
void Bullet::draw() const {
|
void Bullet::draw() const {
|
||||||
if (is_active_ && shape_) {
|
if (is_active_ && shape_) {
|
||||||
Rendering::renderShape(renderer_, shape_, center_, angle_, 1.0F, 1.0F, brightness_, config_->colors.normal);
|
Rendering::renderShape(renderer_, shape_, center_, angle_, config_->shape.scale, 1.0F, brightness_, config_->colors.normal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,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, float bullet_speed);
|
// 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;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// bullet_registry.cpp - Implementació del registre de bala
|
// bullet_registry.cpp - Implementació del registre de bales
|
||||||
// © 2026 JailDesigner
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#include "game/entities/bullet_registry.hpp"
|
#include "game/entities/bullet_registry.hpp"
|
||||||
@@ -8,23 +8,34 @@
|
|||||||
|
|
||||||
#include "core/entities/entity_loader.hpp"
|
#include "core/entities/entity_loader.hpp"
|
||||||
|
|
||||||
BulletConfig BulletRegistry::config;
|
std::unordered_map<std::string, BulletConfig> BulletRegistry::configs;
|
||||||
bool BulletRegistry::loaded = false;
|
bool BulletRegistry::loaded = false;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Tria comú: carrega el YAML d'un name, parseja a BulletConfig i el guarda
|
||||||
|
// al map. Retorna punter al config inserit, o nullptr si falla.
|
||||||
|
auto loadInto(std::unordered_map<std::string, BulletConfig>& configs, const std::string& name) -> const BulletConfig* {
|
||||||
|
auto yaml = Entities::EntityLoader::load(name);
|
||||||
|
if (!yaml) {
|
||||||
|
std::cerr << "[BulletRegistry] Error: no s'ha pogut carregar " << name << ".yaml\n";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto cfg = BulletConfig::fromYaml(*yaml);
|
||||||
|
if (!cfg) {
|
||||||
|
std::cerr << "[BulletRegistry] Error: format invàlid a " << name << ".yaml\n";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
auto [it, _] = configs.insert_or_assign(name, *cfg);
|
||||||
|
return &it->second;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
auto BulletRegistry::load() -> bool {
|
auto BulletRegistry::load() -> bool {
|
||||||
auto yaml = Entities::EntityLoader::load("bullet");
|
if (loadInto(configs, "bullet") == nullptr) {
|
||||||
if (!yaml) {
|
|
||||||
std::cerr << "[BulletRegistry] Error: no s'ha pogut carregar bullet.yaml\n";
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
auto cfg = BulletConfig::fromYaml(*yaml);
|
|
||||||
if (!cfg) {
|
|
||||||
std::cerr << "[BulletRegistry] Error: format invàlid a bullet.yaml\n";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
config = *cfg;
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
std::cout << "[BulletRegistry] Configuració de bala carregada.\n";
|
std::cout << "[BulletRegistry] Configuració de bala 'bullet' carregada.\n";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,5 +44,13 @@ auto BulletRegistry::get() -> const BulletConfig& {
|
|||||||
std::cerr << "[BulletRegistry] FATAL: get() abans de load()\n";
|
std::cerr << "[BulletRegistry] FATAL: get() abans de load()\n";
|
||||||
std::exit(EXIT_FAILURE);
|
std::exit(EXIT_FAILURE);
|
||||||
}
|
}
|
||||||
return config;
|
return configs.at("bullet");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto BulletRegistry::get(const std::string& name) -> const BulletConfig* {
|
||||||
|
auto it = configs.find(name);
|
||||||
|
if (it != configs.end()) {
|
||||||
|
return &it->second;
|
||||||
|
}
|
||||||
|
return loadInto(configs, name);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,34 @@
|
|||||||
// bullet_registry.hpp - Registre estàtic de la configuració de la bala
|
// bullet_registry.hpp - Registre estàtic de configuracions de bala
|
||||||
// © 2026 JailDesigner
|
// © 2026 JailDesigner
|
||||||
//
|
//
|
||||||
// Una única instància per a tota la sessió. Es manté el patró registry
|
// Diverses configuracions per nom (data/entities/<name>/<name>.yaml). El
|
||||||
// (paral·lel a EnemyRegistry) tot i ser una sola entitat: si el dia de demà
|
// config "bullet" es manté com a default històric (player) i es carrega via
|
||||||
// hi ha més tipus de bala (laser/plasma/etc.) només cal estendre-ho.
|
// load(). Els altres (ex. "bullet_long" per a bales d'enemic) es carreguen
|
||||||
|
// peresosament la primera vegada que algú els demana per nom.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
#include "game/entities/bullet_config.hpp"
|
#include "game/entities/bullet_config.hpp"
|
||||||
|
|
||||||
class BulletRegistry {
|
class BulletRegistry {
|
||||||
public:
|
public:
|
||||||
BulletRegistry() = delete;
|
BulletRegistry() = delete;
|
||||||
|
|
||||||
// Carrega data/entities/bullet/bullet.yaml. Retorna false si falla.
|
// Carrega data/entities/bullet/bullet.yaml com a default. Retorna false si falla.
|
||||||
static auto load() -> bool;
|
static auto load() -> bool;
|
||||||
|
|
||||||
// Accés a la configuració. Avorta amb log fatal si load() no s'ha cridat
|
// Accés a la configuració per defecte ("bullet"). Avorta amb log fatal si
|
||||||
// o ha fallat.
|
// load() no s'ha cridat o ha fallat. Mantingut per a backwards compat del Bullet ctor.
|
||||||
static auto get() -> const BulletConfig&;
|
static auto get() -> const BulletConfig&;
|
||||||
|
|
||||||
|
// Accés a una configuració per nom. Lazy-load: si no està al map, intenta
|
||||||
|
// carregar data/entities/<name>/<name>.yaml. Retorna nullptr si no es pot.
|
||||||
|
static auto get(const std::string& name) -> const BulletConfig*;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static BulletConfig config;
|
static std::unordered_map<std::string, BulletConfig> configs;
|
||||||
static bool loaded;
|
static bool loaded;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,14 +28,6 @@ namespace {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recupera el "ángulo equivalente" de un body en movimiento (para zigzag).
|
|
||||||
auto velocityToAngle(const Vec2& velocity) -> float {
|
|
||||||
if (velocity.lengthSquared() < 0.0001F) {
|
|
||||||
return 0.0F;
|
|
||||||
}
|
|
||||||
return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Random float [0..1).
|
// Random float [0..1).
|
||||||
auto randFloat01() -> float {
|
auto randFloat01() -> float {
|
||||||
return static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
return static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||||
@@ -62,9 +54,14 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
|||||||
config_ = &EnemyRegistry::get(type);
|
config_ = &EnemyRegistry::get(type);
|
||||||
const EnemyConfig& cfg = *config_;
|
const EnemyConfig& cfg = *config_;
|
||||||
|
|
||||||
if (type_ == EnemyType::SQUARE) {
|
ai_state_ = EnemyAiState{};
|
||||||
tracking_timer_ = 0.0F;
|
ai_state_.tracking_strength = cfg.ai.movement.tracking_strength;
|
||||||
tracking_strength_ = cfg.behavior.tracking_strength;
|
|
||||||
|
// Timers paral·lels a tick: random [0, interval) per evitar que tots els
|
||||||
|
// enemics del mateix tipus disparin sincronitzats al spawn.
|
||||||
|
ai_tick_timers_.resize(cfg.ai.tick.size());
|
||||||
|
for (std::size_t i = 0; i < cfg.ai.tick.size(); ++i) {
|
||||||
|
ai_tick_timers_[i] = randFloat01() * cfg.ai.tick[i].interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
shape_ = Graphics::ShapeLoader::load(cfg.shape.path);
|
shape_ = Graphics::ShapeLoader::load(cfg.shape.path);
|
||||||
@@ -136,7 +133,8 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
|
|||||||
invulnerability_timer_ = cfg.spawn.invulnerability_duration;
|
invulnerability_timer_ = cfg.spawn.invulnerability_duration;
|
||||||
brightness_ = cfg.spawn.invulnerability_brightness_start;
|
brightness_ = cfg.spawn.invulnerability_brightness_start;
|
||||||
|
|
||||||
direction_change_timer_ = 0.0F;
|
health_ = cfg.health;
|
||||||
|
flash_timer_ = 0.0F;
|
||||||
|
|
||||||
is_active_ = true;
|
is_active_ = true;
|
||||||
}
|
}
|
||||||
@@ -155,6 +153,11 @@ void Enemy::update(float delta_time) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (flash_timer_ > 0.0F) {
|
||||||
|
flash_timer_ -= delta_time;
|
||||||
|
flash_timer_ = std::max(flash_timer_, 0.0F);
|
||||||
|
}
|
||||||
|
|
||||||
if (invulnerability_timer_ > 0.0F) {
|
if (invulnerability_timer_ > 0.0F) {
|
||||||
invulnerability_timer_ -= delta_time;
|
invulnerability_timer_ -= delta_time;
|
||||||
invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F);
|
invulnerability_timer_ = std::max(invulnerability_timer_, 0.0F);
|
||||||
@@ -167,22 +170,9 @@ void Enemy::update(float delta_time) {
|
|||||||
brightness_ = START + ((END - START) * SMOOTH_T);
|
brightness_ = START + ((END - START) * SMOOTH_T);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isWounded()) {
|
// El moviment es delega a Systems::EnemyAi::tick, invocat des de l'scene
|
||||||
switch (type_) {
|
// ABANS d'aquest update (manté l'ordre: AI escriu velocity/rotation_delta,
|
||||||
case EnemyType::PENTAGON:
|
// després animation pot modular rotation_delta via rotation_accel).
|
||||||
case EnemyType::STAR:
|
|
||||||
// STAR reusa el zigzag esquivador de Pentagon. Si en el futur
|
|
||||||
// vol comportament propi, separa-li el cas.
|
|
||||||
behaviorPentagon(delta_time);
|
|
||||||
break;
|
|
||||||
case EnemyType::SQUARE:
|
|
||||||
behaviorSquare(delta_time);
|
|
||||||
break;
|
|
||||||
case EnemyType::PINWHEEL:
|
|
||||||
behaviorPinwheel(delta_time);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateAnimation(delta_time);
|
updateAnimation(delta_time);
|
||||||
|
|
||||||
@@ -210,7 +200,15 @@ void Enemy::draw() const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rendering::renderShape(renderer_, shape_, center_, rotation_, SCALE, 1.0F, brightness_, color);
|
// Flash d'impacte parcial (HP>1): força el color a blanc i el brillo a
|
||||||
|
// 1.0 durant la finestra de flash. Té prioritat sobre el blink wounded.
|
||||||
|
float effective_brightness = brightness_;
|
||||||
|
if (flash_timer_ > 0.0F) {
|
||||||
|
color = SDL_Color{.r = 255, .g = 255, .b = 255, .a = 255};
|
||||||
|
effective_brightness = 1.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rendering::renderShape(renderer_, shape_, center_, rotation_, SCALE, 1.0F, effective_brightness, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Enemy::destroy() {
|
void Enemy::destroy() {
|
||||||
@@ -221,6 +219,7 @@ void Enemy::destroy() {
|
|||||||
wounded_timer_ = 0.0F;
|
wounded_timer_ = 0.0F;
|
||||||
wound_expired_this_frame_ = false;
|
wound_expired_this_frame_ = false;
|
||||||
last_hit_by_ = 0xFF;
|
last_hit_by_ = 0xFF;
|
||||||
|
flash_timer_ = 0.0F;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Enemy::hurt(uint8_t shooter_id) {
|
void Enemy::hurt(uint8_t shooter_id) {
|
||||||
@@ -246,59 +245,6 @@ void Enemy::setVelocityFromAngle(float angle_movement, float speed) {
|
|||||||
body_.velocity = angleToDirection(angle_movement) * speed;
|
body_.velocity = angleToDirection(angle_movement) * speed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PENTAGON: zigzag esquivador. Canvis de direcció periòdics (probabilístics)
|
|
||||||
// en lloc de detectar parets; el rebot contra murs el fa PhysicsWorld.
|
|
||||||
void Enemy::behaviorPentagon(float delta_time) {
|
|
||||||
direction_change_timer_ += delta_time;
|
|
||||||
|
|
||||||
if (randFloat01() < config_->behavior.zigzag_prob_per_second * delta_time) {
|
|
||||||
const float CURRENT_ANGLE = velocityToAngle(body_.velocity);
|
|
||||||
const float DELTA = randFloat01() * config_->behavior.angle_change_max;
|
|
||||||
const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA);
|
|
||||||
const float SPEED = body_.velocity.length();
|
|
||||||
setVelocityFromAngle(NEW_ANGLE, SPEED);
|
|
||||||
direction_change_timer_ = 0.0F;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SQUARE: tracking discret cap a la nau cada N segons.
|
|
||||||
void Enemy::behaviorSquare(float delta_time) {
|
|
||||||
tracking_timer_ += delta_time;
|
|
||||||
|
|
||||||
if (tracking_timer_ >= config_->behavior.tracking_interval && ship_position_ != nullptr) {
|
|
||||||
tracking_timer_ = 0.0F;
|
|
||||||
|
|
||||||
const Vec2 TO_SHIP = *ship_position_ - center_;
|
|
||||||
const float DIST = TO_SHIP.length();
|
|
||||||
if (DIST > 0.0F) {
|
|
||||||
const Vec2 DESIRED_DIR = TO_SHIP / DIST;
|
|
||||||
const float SPEED = body_.velocity.length();
|
|
||||||
const Vec2 DESIRED_VEL = DESIRED_DIR * SPEED;
|
|
||||||
|
|
||||||
body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) +
|
|
||||||
(DESIRED_VEL * tracking_strength_);
|
|
||||||
|
|
||||||
const float NEW_SPEED = body_.velocity.length();
|
|
||||||
if (NEW_SPEED > 0.0F) {
|
|
||||||
body_.velocity = body_.velocity * (SPEED / NEW_SPEED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PINWHEEL: movement rectilini + boost de rotació visual prop del ship.
|
|
||||||
void Enemy::behaviorPinwheel(float /*delta_time*/) {
|
|
||||||
if (ship_position_ != nullptr) {
|
|
||||||
const Vec2 TO_SHIP = *ship_position_ - center_;
|
|
||||||
const float DIST = TO_SHIP.length();
|
|
||||||
if (DIST < config_->behavior.proximity_distance) {
|
|
||||||
rotation_delta_ = animation_.rotation_delta_base * config_->behavior.rotation_proximity_multiplier;
|
|
||||||
} else {
|
|
||||||
rotation_delta_ = animation_.rotation_delta_base;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Enemy::updateAnimation(float delta_time) {
|
void Enemy::updateAnimation(float delta_time) {
|
||||||
updatePulse(delta_time);
|
updatePulse(delta_time);
|
||||||
updateRotationAcceleration(delta_time);
|
updateRotationAcceleration(delta_time);
|
||||||
@@ -373,7 +319,7 @@ auto Enemy::getBaseRotation() const -> float {
|
|||||||
|
|
||||||
void Enemy::setTrackingStrength(float strength) {
|
void Enemy::setTrackingStrength(float strength) {
|
||||||
if (type_ == EnemyType::SQUARE) {
|
if (type_ == EnemyType::SQUARE) {
|
||||||
tracking_strength_ = strength;
|
ai_state_.tracking_strength = strength;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,17 +4,24 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/defaults/enemies.hpp"
|
||||||
#include "core/entities/entity.hpp"
|
#include "core/entities/entity.hpp"
|
||||||
#include "core/types.hpp"
|
#include "core/types.hpp"
|
||||||
|
#include "game/entities/enemy_ai.hpp"
|
||||||
|
|
||||||
|
class Ship;
|
||||||
|
|
||||||
// Tipo de enemy
|
// Tipo de enemy
|
||||||
enum class EnemyType : uint8_t {
|
enum class EnemyType : uint8_t {
|
||||||
PENTAGON = 0, // Pentágono esquivador (zigzag)
|
PENTAGON = 0, // Pentágono esquivador (zigzag)
|
||||||
SQUARE = 1, // Square perseguidor (tracks ship)
|
SQUARE = 1, // Square perseguidor (tracks ship)
|
||||||
PINWHEEL = 2, // Molinillo agresivo (rápido, girando)
|
PINWHEEL = 2, // Molinillo agresivo (rápido, girando)
|
||||||
STAR = 3 // Estrella de 5 puntes (clone visual de Pentagon, comportament zigzag)
|
STAR = 3, // Estrella de 5 puntes (clone visual de Pentagon, comportament zigzag)
|
||||||
|
ORB = 4, // Orb gegant tough (HP=10, chase lent — primer enemic HP>1)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forward declaration — EnemyConfig viu a enemy_config.hpp i s'inclou només a enemy.cpp.
|
// Forward declaration — EnemyConfig viu a enemy_config.hpp i s'inclou només a enemy.cpp.
|
||||||
@@ -70,8 +77,18 @@ class Enemy : public Entities::Entity {
|
|||||||
// ha estat inicialitzat almenys un cop; abans és nullptr.
|
// ha estat inicialitzat almenys un cop; abans és nullptr.
|
||||||
[[nodiscard]] auto getConfig() const -> const EnemyConfig& { return *config_; }
|
[[nodiscard]] auto getConfig() const -> const EnemyConfig& { return *config_; }
|
||||||
|
|
||||||
// Set ship position reference for tracking behavior
|
// Referències als 2 ships per a AI de tracking/proximity/chase/flee.
|
||||||
void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; }
|
// nullptr = ship inexistent al match. El sistema d'IA filtra per ship->isActive().
|
||||||
|
void setShips(const Ship* p1, const Ship* p2) { ships_ = {p1, p2}; }
|
||||||
|
[[nodiscard]] auto getShips() const -> const std::array<const Ship*, 2>& { return ships_; }
|
||||||
|
|
||||||
|
// Accessors per al sistema d'IA (Systems::EnemyAi).
|
||||||
|
[[nodiscard]] auto getAiState() -> EnemyAiState& { return ai_state_; }
|
||||||
|
[[nodiscard]] auto getAiTickTimers() -> std::vector<float>& { return ai_tick_timers_; }
|
||||||
|
[[nodiscard]] auto getRotationBase() const -> float { return animation_.rotation_delta_base; }
|
||||||
|
void setRotationDelta(float rot) { rotation_delta_ = rot; }
|
||||||
|
// Public: el sistema d'IA reorienta la velocitat des d'un angle.
|
||||||
|
void setVelocityFromAngle(float angle_movement, float speed);
|
||||||
|
|
||||||
// Stage system API (base stats)
|
// Stage system API (base stats)
|
||||||
[[nodiscard]] auto getBaseVelocity() const -> float;
|
[[nodiscard]] auto getBaseVelocity() const -> float;
|
||||||
@@ -101,6 +118,20 @@ class Enemy : public Entities::Entity {
|
|||||||
void consumeWoundExpired() { wound_expired_this_frame_ = false; }
|
void consumeWoundExpired() { wound_expired_this_frame_ = false; }
|
||||||
[[nodiscard]] auto getLastHitBy() const -> uint8_t { return last_hit_by_; }
|
[[nodiscard]] auto getLastHitBy() const -> uint8_t { return last_hit_by_; }
|
||||||
|
|
||||||
|
// Salut: decrementada per l'acció DECREASE_HEALTH al dispatcher d'events.
|
||||||
|
// Quan arriba a 0 o menys, el dispatcher dispara ON_NO_HEALTH (que
|
||||||
|
// típicament encadena SET_HURT o DESTROY al YAML). last_hit_by s'actualitza
|
||||||
|
// al decrement perquè la mort posterior atribueixi correctament el kill.
|
||||||
|
[[nodiscard]] auto getHealth() const -> int { return health_; }
|
||||||
|
void decrementHealth(uint8_t shooter_id = 0xFF) {
|
||||||
|
--health_;
|
||||||
|
last_hit_by_ = shooter_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flash visual: brief impacto-feedback quan rep un hit no letal (HP>1).
|
||||||
|
// Disparat per l'acció FLASH; el render alça la lluminositat mentre dura.
|
||||||
|
void triggerFlash() { flash_timer_ = Defaults::Enemies::Visual::FLASH_DURATION; }
|
||||||
|
|
||||||
// Aplica un impulso (cambio inmediato de velocidad mass-aware) al cuerpo físico.
|
// Aplica un impulso (cambio inmediato de velocidad mass-aware) al cuerpo físico.
|
||||||
void applyImpulse(const Vec2& impulse);
|
void applyImpulse(const Vec2& impulse);
|
||||||
|
|
||||||
@@ -120,11 +151,16 @@ class Enemy : public Entities::Entity {
|
|||||||
EnemyType type_{EnemyType::PENTAGON};
|
EnemyType type_{EnemyType::PENTAGON};
|
||||||
EnemyAnimation animation_;
|
EnemyAnimation animation_;
|
||||||
|
|
||||||
// Comportamiento type-specific
|
// Estat per-instància que la primitiva de moviment manté entre frames.
|
||||||
float tracking_timer_{0.0F};
|
EnemyAiState ai_state_;
|
||||||
const Vec2* ship_position_{nullptr};
|
|
||||||
float tracking_strength_{0.0F};
|
// Timers paral·lels a config_->ai.tick: timers_[i] és el temps restant
|
||||||
float direction_change_timer_{0.0F};
|
// (en segons) fins a la pròxima execució de l'acció i. Re-dimensionat a
|
||||||
|
// init() segons la mida de config_->ai.tick.
|
||||||
|
std::vector<float> ai_tick_timers_;
|
||||||
|
|
||||||
|
// Referències als 2 ships per a AI de tracking/proximity/chase/flee.
|
||||||
|
std::array<const Ship*, 2> ships_{nullptr, nullptr};
|
||||||
|
|
||||||
// Invulnerabilidad post-spawn
|
// Invulnerabilidad post-spawn
|
||||||
float invulnerability_timer_{0.0F};
|
float invulnerability_timer_{0.0F};
|
||||||
@@ -134,18 +170,21 @@ class Enemy : public Entities::Entity {
|
|||||||
bool wound_expired_this_frame_{false};
|
bool wound_expired_this_frame_{false};
|
||||||
uint8_t last_hit_by_{0xFF};
|
uint8_t last_hit_by_{0xFF};
|
||||||
|
|
||||||
|
// Salut per-instància. Reseteja a config_->health a init(); el dispatcher
|
||||||
|
// d'events la decrementa via DECREASE_HEALTH i dispara ON_NO_HEALTH quan
|
||||||
|
// creua zero. Permet enemics tough (HP>1) sense canvis al motor.
|
||||||
|
int health_{1};
|
||||||
|
|
||||||
|
// Flash visual temporitzat per a feedback d'impacte parcial (HP>1).
|
||||||
|
// L'acció FLASH el reseteja a FLASH_DURATION; draw() alça la lluminositat
|
||||||
|
// mentre dura, i update() el decrementa.
|
||||||
|
float flash_timer_{0.0F};
|
||||||
|
|
||||||
// Métodos privados
|
// Métodos privados
|
||||||
void updateAnimation(float delta_time);
|
void updateAnimation(float delta_time);
|
||||||
void updatePulse(float delta_time);
|
void updatePulse(float delta_time);
|
||||||
void updateRotationAcceleration(float delta_time);
|
void updateRotationAcceleration(float delta_time);
|
||||||
void behaviorPentagon(float delta_time);
|
|
||||||
void behaviorSquare(float delta_time);
|
|
||||||
void behaviorPinwheel(float delta_time);
|
|
||||||
[[nodiscard]] auto computeCurrentScale() const -> float;
|
[[nodiscard]] auto computeCurrentScale() const -> float;
|
||||||
// Static: passa els paràmetres com a args per no acoblar a *this.
|
// Static: passa els paràmetres com a args per no acoblar a *this.
|
||||||
static auto attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float safety_distance, float& out_x, float& out_y) -> bool;
|
static auto attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float safety_distance, float& out_x, float& out_y) -> bool;
|
||||||
|
|
||||||
// Helper: setear body_.velocity desde un ángulo y magnitud.
|
|
||||||
// angle_movement=0 apunta hacia arriba (eje Y negativo SDL).
|
|
||||||
void setVelocityFromAngle(float angle_movement, float speed);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// enemy_ai.hpp - Sistema declaratiu d'IA per a enemics
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Cada enemic declara al seu YAML quin movement primitiu fa servir i, opcional-
|
||||||
|
// ment, una llista d'accions periòdiques (tick). El motor només dispatcha; el
|
||||||
|
// comportament viu a les dades. Patró paral·lel al d'events declaratius
|
||||||
|
// (enemy_event.hpp).
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Primitiva de moviment activa per a un enemic. Substitueix el switch
|
||||||
|
// hardcoded sobre EnemyType.
|
||||||
|
enum class MovementType : uint8_t {
|
||||||
|
ZIGZAG, // Canvi de direcció probabilístic agressiu (Pentagon/Star)
|
||||||
|
TRACKING, // LERP discret cap al ship cada N segons (Square)
|
||||||
|
RECTILINEAR_PROXIMITY, // Rectilini + boost rotació visual prop del ship (Pinwheel)
|
||||||
|
WANDER, // Canvi de direcció probabilístic suau, sense target
|
||||||
|
CHASE, // Steering continu cap al ship més proper
|
||||||
|
FLEE, // Steering continu allunyant-se del ship més proper
|
||||||
|
};
|
||||||
|
|
||||||
|
// Accions que s'executen periòdicament (un timer per acció). Futur (Fase C):
|
||||||
|
// SHOOT amb aim_mode/jitter/bullet config.
|
||||||
|
enum class AiActionType : uint8_t {
|
||||||
|
SHOOT,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class AimMode : uint8_t {
|
||||||
|
RANDOM, // Angle uniformement aleatori
|
||||||
|
AIMED, // atan2(nearest_ship - center) + soroll gaussià (jitter_rad)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Camps de tots els movements; només el subset rellevant per al type actiu
|
||||||
|
// s'usa. Els altres queden a 0.0F. Mateixa filosofia que la BehaviorCfg
|
||||||
|
// llegacy però amb el type explícit dins.
|
||||||
|
struct MovementConfig {
|
||||||
|
MovementType type{MovementType::ZIGZAG};
|
||||||
|
|
||||||
|
// ZIGZAG i WANDER (canvi de direcció probabilístic; comparteixen camps).
|
||||||
|
float angle_change_max{0.0F};
|
||||||
|
float zigzag_prob_per_second{0.0F};
|
||||||
|
|
||||||
|
// TRACKING
|
||||||
|
float tracking_strength{0.0F};
|
||||||
|
float tracking_interval{0.0F};
|
||||||
|
|
||||||
|
// RECTILINEAR_PROXIMITY
|
||||||
|
float rotation_proximity_multiplier{0.0F};
|
||||||
|
float proximity_distance{0.0F};
|
||||||
|
|
||||||
|
// CHASE / FLEE: força del steering per segon (LERP velocity ↔ direcció ideal).
|
||||||
|
// 1.0 = en ~1s la velocitat queda totalment realineada cap al target.
|
||||||
|
float chase_strength{0.0F};
|
||||||
|
float flee_strength{0.0F};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Acció periòdica. interval = segons entre disparades; el dispatcher manté un
|
||||||
|
// timer per acció (paral·lel a aquesta llista) i dispara quan arriba a 0.
|
||||||
|
struct AiTickAction {
|
||||||
|
AiActionType type{AiActionType::SHOOT};
|
||||||
|
float interval{1.0F};
|
||||||
|
AimMode aim_mode{AimMode::RANDOM};
|
||||||
|
float jitter_rad{0.0F};
|
||||||
|
float bullet_speed{200.0F}; // px/s; la magnitud la decideix l'enemic, no el bullet config
|
||||||
|
std::string bullet_config_name; // bullet config a usar (lazy-load des de BulletRegistry)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct EnemyAiConfig {
|
||||||
|
MovementConfig movement;
|
||||||
|
std::vector<AiTickAction> tick;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Estat per-instància que la primitiva de moviment manté entre frames (timers
|
||||||
|
// d'interval, contadors de canvi de direcció...). Es viu dins de Enemy i el
|
||||||
|
// sistema d'IA hi escriu via getAiState().
|
||||||
|
struct EnemyAiState {
|
||||||
|
float direction_change_timer{0.0F}; // ZIGZAG
|
||||||
|
float tracking_timer{0.0F}; // TRACKING
|
||||||
|
float tracking_strength{0.0F}; // TRACKING (cau de cfg, mutable per dificultat)
|
||||||
|
};
|
||||||
@@ -30,6 +30,7 @@ namespace {
|
|||||||
if (s == "square") { return EnemyType::SQUARE; }
|
if (s == "square") { return EnemyType::SQUARE; }
|
||||||
if (s == "pinwheel") { return EnemyType::PINWHEEL; }
|
if (s == "pinwheel") { return EnemyType::PINWHEEL; }
|
||||||
if (s == "star") { return EnemyType::STAR; }
|
if (s == "star") { return EnemyType::STAR; }
|
||||||
|
if (s == "orb") { return EnemyType::ORB; }
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,13 +178,31 @@ namespace {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// health és opcional: si el YAML no l'inclou, el default {1} de l'struct
|
||||||
|
// ja cobreix el comportament de tots els enemics actuals (1 hit → mort).
|
||||||
|
void parseHealth(const fkyaml::node& node, int& out) {
|
||||||
|
if (node.contains("health")) {
|
||||||
|
out = node["health"].get_value<int>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward-decl: aimModeFromString viu més avall (junt amb la resta de
|
||||||
|
// helpers d'AI) però parseActionList el necessita per al payload de
|
||||||
|
// FIRE_BULLET. Evita reordenar tot el bloc.
|
||||||
|
auto aimModeFromString(const std::string& s) -> std::optional<AimMode>;
|
||||||
|
|
||||||
auto actionTypeFromString(const std::string& s) -> std::optional<EnemyActionType> {
|
auto actionTypeFromString(const std::string& s) -> std::optional<EnemyActionType> {
|
||||||
if (s == "set_hurt") { return EnemyActionType::SET_HURT; }
|
if (s == "set_hurt") { return EnemyActionType::SET_HURT; }
|
||||||
if (s == "destroy") { return EnemyActionType::DESTROY; }
|
if (s == "destroy") { return EnemyActionType::DESTROY; }
|
||||||
if (s == "add_score") { return EnemyActionType::ADD_SCORE; }
|
if (s == "add_score") { return EnemyActionType::ADD_SCORE; }
|
||||||
if (s == "create_debris") { return EnemyActionType::CREATE_DEBRIS; }
|
if (s == "create_debris") { return EnemyActionType::CREATE_DEBRIS; }
|
||||||
|
if (s == "create_debris_partial") { return EnemyActionType::CREATE_DEBRIS_PARTIAL; }
|
||||||
if (s == "create_fireworks") { return EnemyActionType::CREATE_FIREWORKS; }
|
if (s == "create_fireworks") { return EnemyActionType::CREATE_FIREWORKS; }
|
||||||
|
if (s == "create_fireworks_small") { return EnemyActionType::CREATE_FIREWORKS_SMALL; }
|
||||||
if (s == "apply_impulse") { return EnemyActionType::APPLY_IMPULSE; }
|
if (s == "apply_impulse") { return EnemyActionType::APPLY_IMPULSE; }
|
||||||
|
if (s == "decrease_health") { return EnemyActionType::DECREASE_HEALTH; }
|
||||||
|
if (s == "flash") { return EnemyActionType::FLASH; }
|
||||||
|
if (s == "fire_bullet") { return EnemyActionType::FIRE_BULLET; }
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,20 +225,198 @@ namespace {
|
|||||||
<< event_name << " (" << enemy_name << ")\n";
|
<< event_name << " (" << enemy_name << ")\n";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
out.push_back({*PARSED});
|
EnemyAction action;
|
||||||
|
action.type = *PARSED;
|
||||||
|
// Payload de FIRE_BULLET. Camps opcionals; els defaults són els del struct.
|
||||||
|
if (action.type == EnemyActionType::FIRE_BULLET) {
|
||||||
|
if (item.contains("bullet")) {
|
||||||
|
action.bullet_config_name = item["bullet"].get_value<std::string>();
|
||||||
|
}
|
||||||
|
if (item.contains("bullet_speed")) {
|
||||||
|
action.bullet_speed = item["bullet_speed"].get_value<float>();
|
||||||
|
}
|
||||||
|
if (item.contains("aim_mode")) {
|
||||||
|
const auto AIM_STR = item["aim_mode"].get_value<std::string>();
|
||||||
|
const auto AIM = aimModeFromString(AIM_STR);
|
||||||
|
if (!AIM) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: aim_mode desconegut '" << AIM_STR
|
||||||
|
<< "' a " << event_name << " (" << enemy_name << ")\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
action.aim_mode = *AIM;
|
||||||
|
}
|
||||||
|
if (item.contains("jitter_rad")) {
|
||||||
|
action.jitter_rad = item["jitter_rad"].get_value<float>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push_back(action);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defaults: replica el flux hardcoded actual (set_hurt → destroy → score+debris+fireworks).
|
// Defaults: replica el flux hardcoded actual (set_hurt → destroy → score+debris+fireworks).
|
||||||
|
// Construïm via mutació per esquivar warnings de designated-init parcial sobre
|
||||||
|
// l'EnemyAction (que té payload de FIRE_BULLET no rellevant ací).
|
||||||
void fillLegacyDefaults(EnemyEventConfig& events) {
|
void fillLegacyDefaults(EnemyEventConfig& events) {
|
||||||
events.on_hit = {{EnemyActionType::SET_HURT}};
|
const auto MAKE = [](EnemyActionType type) {
|
||||||
events.on_hurt_end = {{EnemyActionType::DESTROY}};
|
EnemyAction a;
|
||||||
events.on_destroy = {
|
a.type = type;
|
||||||
{EnemyActionType::ADD_SCORE},
|
return a;
|
||||||
{EnemyActionType::CREATE_DEBRIS},
|
|
||||||
{EnemyActionType::CREATE_FIREWORKS},
|
|
||||||
};
|
};
|
||||||
|
events.on_hit = {MAKE(EnemyActionType::SET_HURT)};
|
||||||
|
events.on_hurt_end = {MAKE(EnemyActionType::DESTROY)};
|
||||||
|
events.on_destroy = {
|
||||||
|
MAKE(EnemyActionType::ADD_SCORE),
|
||||||
|
MAKE(EnemyActionType::CREATE_DEBRIS),
|
||||||
|
MAKE(EnemyActionType::CREATE_FIREWORKS),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto movementTypeFromString(const std::string& s) -> std::optional<MovementType> {
|
||||||
|
if (s == "zigzag") { return MovementType::ZIGZAG; }
|
||||||
|
if (s == "tracking") { return MovementType::TRACKING; }
|
||||||
|
if (s == "rectilinear_proximity") { return MovementType::RECTILINEAR_PROXIMITY; }
|
||||||
|
if (s == "wander") { return MovementType::WANDER; }
|
||||||
|
if (s == "chase") { return MovementType::CHASE; }
|
||||||
|
if (s == "flee") { return MovementType::FLEE; }
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto aiActionTypeFromString(const std::string& s) -> std::optional<AiActionType> {
|
||||||
|
if (s == "shoot") { return AiActionType::SHOOT; }
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto aimModeFromString(const std::string& s) -> std::optional<AimMode> {
|
||||||
|
if (s == "random") { return AimMode::RANDOM; }
|
||||||
|
if (s == "aimed") { return AimMode::AIMED; }
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parseMovement(const fkyaml::node& mv_node, const std::string& enemy_name, MovementConfig& out) -> bool {
|
||||||
|
if (!mv_node.contains("type")) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: falta 'ai.movement.type' a " << enemy_name << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto TYPE_STR = mv_node["type"].get_value<std::string>();
|
||||||
|
const auto PARSED = movementTypeFromString(TYPE_STR);
|
||||||
|
if (!PARSED) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: movement type desconegut '" << TYPE_STR
|
||||||
|
<< "' a " << enemy_name << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.type = *PARSED;
|
||||||
|
const auto READ_OPT = [&mv_node](const char* key, float& dst) {
|
||||||
|
if (mv_node.contains(key)) {
|
||||||
|
dst = mv_node[key].get_value<float>();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
READ_OPT("angle_change_max", out.angle_change_max);
|
||||||
|
READ_OPT("zigzag_prob_per_second", out.zigzag_prob_per_second);
|
||||||
|
READ_OPT("tracking_strength", out.tracking_strength);
|
||||||
|
READ_OPT("tracking_interval", out.tracking_interval);
|
||||||
|
READ_OPT("rotation_proximity_multiplier", out.rotation_proximity_multiplier);
|
||||||
|
READ_OPT("proximity_distance", out.proximity_distance);
|
||||||
|
READ_OPT("chase_strength", out.chase_strength);
|
||||||
|
READ_OPT("flee_strength", out.flee_strength);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parseTickList(const fkyaml::node& list_node, const std::string& enemy_name, std::vector<AiTickAction>& out) -> bool {
|
||||||
|
if (!list_node.is_sequence()) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: 'ai.tick' ha de ser una llista a " << enemy_name << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const auto& item : list_node) {
|
||||||
|
if (!item.contains("action")) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: entrada sense 'action' a ai.tick ("
|
||||||
|
<< enemy_name << ")\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const auto STR = item["action"].get_value<std::string>();
|
||||||
|
const auto PARSED = aiActionTypeFromString(STR);
|
||||||
|
if (!PARSED) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: acció d'ai desconeguda '" << STR
|
||||||
|
<< "' a " << enemy_name << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AiTickAction action;
|
||||||
|
action.type = *PARSED;
|
||||||
|
if (item.contains("interval")) {
|
||||||
|
action.interval = item["interval"].get_value<float>();
|
||||||
|
}
|
||||||
|
if (item.contains("aim_mode")) {
|
||||||
|
const auto AIM_STR = item["aim_mode"].get_value<std::string>();
|
||||||
|
const auto AIM = aimModeFromString(AIM_STR);
|
||||||
|
if (!AIM) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: aim_mode desconegut '" << AIM_STR
|
||||||
|
<< "' a ai.tick (" << enemy_name << ")\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
action.aim_mode = *AIM;
|
||||||
|
}
|
||||||
|
if (item.contains("jitter_rad")) {
|
||||||
|
action.jitter_rad = item["jitter_rad"].get_value<float>();
|
||||||
|
}
|
||||||
|
if (item.contains("bullet_speed")) {
|
||||||
|
action.bullet_speed = item["bullet_speed"].get_value<float>();
|
||||||
|
}
|
||||||
|
if (item.contains("bullet")) {
|
||||||
|
action.bullet_config_name = item["bullet"].get_value<std::string>();
|
||||||
|
}
|
||||||
|
out.push_back(action);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migració progressiva: si el YAML no porta secció `ai:`, derivem el
|
||||||
|
// movement a partir de l'ai_type i copiem els paràmetres de la BehaviorCfg
|
||||||
|
// ja parsejada. Comportament idèntic al hardcoded actual.
|
||||||
|
void fillLegacyAiDefaults(EnemyType ai_type, const EnemyConfig::BehaviorCfg& legacy, EnemyAiConfig& out) {
|
||||||
|
switch (ai_type) {
|
||||||
|
case EnemyType::PENTAGON:
|
||||||
|
case EnemyType::STAR:
|
||||||
|
out.movement.type = MovementType::ZIGZAG;
|
||||||
|
out.movement.angle_change_max = legacy.angle_change_max;
|
||||||
|
out.movement.zigzag_prob_per_second = legacy.zigzag_prob_per_second;
|
||||||
|
break;
|
||||||
|
case EnemyType::SQUARE:
|
||||||
|
out.movement.type = MovementType::TRACKING;
|
||||||
|
out.movement.tracking_strength = legacy.tracking_strength;
|
||||||
|
out.movement.tracking_interval = legacy.tracking_interval;
|
||||||
|
break;
|
||||||
|
case EnemyType::PINWHEEL:
|
||||||
|
out.movement.type = MovementType::RECTILINEAR_PROXIMITY;
|
||||||
|
out.movement.rotation_proximity_multiplier = legacy.rotation_proximity_multiplier;
|
||||||
|
out.movement.proximity_distance = legacy.proximity_distance;
|
||||||
|
break;
|
||||||
|
case EnemyType::ORB:
|
||||||
|
// Sense legacy fallback: el YAML de l'orb ha de definir
|
||||||
|
// ai.movement explícitament. Default chase lent perquè el switch
|
||||||
|
// siga exhaustiu i no falli si algú omet el bloc ai.
|
||||||
|
out.movement.type = MovementType::CHASE;
|
||||||
|
out.movement.chase_strength = 0.3F;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parseAi(const fkyaml::node& node, const std::string& name, EnemyType ai_type, const EnemyConfig::BehaviorCfg& legacy, EnemyAiConfig& out) -> bool {
|
||||||
|
if (!node.contains("ai")) {
|
||||||
|
fillLegacyAiDefaults(ai_type, legacy, out);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const auto& ai = node["ai"];
|
||||||
|
if (!ai.contains("movement")) {
|
||||||
|
std::cerr << "[EnemyConfig] Error: falta 'ai.movement' a " << name << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseMovement(ai["movement"], name, out.movement)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ai.contains("tick") && !parseTickList(ai["tick"], name, out.tick)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto parseEvents(const fkyaml::node& node, const std::string& name, EnemyEventConfig& out) -> bool {
|
auto parseEvents(const fkyaml::node& node, const std::string& name, EnemyEventConfig& out) -> bool {
|
||||||
@@ -231,6 +428,10 @@ namespace {
|
|||||||
if (e.contains("on_hit") && !parseActionList(e["on_hit"], name, "on_hit", out.on_hit)) {
|
if (e.contains("on_hit") && !parseActionList(e["on_hit"], name, "on_hit", out.on_hit)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (e.contains("on_no_health") &&
|
||||||
|
!parseActionList(e["on_no_health"], name, "on_no_health", out.on_no_health)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (e.contains("on_hurt_end") &&
|
if (e.contains("on_hurt_end") &&
|
||||||
!parseActionList(e["on_hurt_end"], name, "on_hurt_end", out.on_hurt_end)) {
|
!parseActionList(e["on_hurt_end"], name, "on_hurt_end", out.on_hurt_end)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -267,7 +468,9 @@ auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
|
|||||||
if (!parseSpawn(node, cfg.name, cfg.spawn)) { return std::nullopt; }
|
if (!parseSpawn(node, cfg.name, cfg.spawn)) { return std::nullopt; }
|
||||||
if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; }
|
if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; }
|
||||||
if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; }
|
if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; }
|
||||||
|
parseHealth(node, cfg.health);
|
||||||
if (!parseEvents(node, cfg.name, cfg.events)) { return std::nullopt; }
|
if (!parseEvents(node, cfg.name, cfg.events)) { return std::nullopt; }
|
||||||
|
if (!parseAi(node, cfg.name, cfg.ai_type, cfg.behavior, cfg.ai)) { return std::nullopt; }
|
||||||
|
|
||||||
return cfg;
|
return cfg;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
#include "game/entities/enemy.hpp" // EnemyType
|
#include "game/entities/enemy.hpp" // EnemyType
|
||||||
|
#include "game/entities/enemy_ai.hpp"
|
||||||
#include "game/entities/enemy_event.hpp"
|
#include "game/entities/enemy_event.hpp"
|
||||||
|
|
||||||
struct EnemyConfig {
|
struct EnemyConfig {
|
||||||
@@ -99,7 +100,12 @@ struct EnemyConfig {
|
|||||||
SpawnCfg spawn;
|
SpawnCfg spawn;
|
||||||
ColorsCfg colors;
|
ColorsCfg colors;
|
||||||
int score;
|
int score;
|
||||||
|
// Salut inicial: per defecte 1 (un balazo → on_no_health). Els YAMLs poden
|
||||||
|
// pujar-lo (p.ex. 10 per a un enemic tough). El sistema d'events és qui
|
||||||
|
// decideix què passa quan la salut arriba a 0 via on_no_health.
|
||||||
|
int health{1};
|
||||||
EnemyEventConfig events;
|
EnemyEventConfig events;
|
||||||
|
EnemyAiConfig ai;
|
||||||
|
|
||||||
// Parseja un descriptor d'enemic. expected_ai_type valida que ai_type del
|
// Parseja un descriptor d'enemic. expected_ai_type valida que ai_type del
|
||||||
// YAML coincideix amb el tipus que el caller espera (segons el directori).
|
// YAML coincideix amb el tipus que el caller espera (segons el directori).
|
||||||
|
|||||||
@@ -8,29 +8,46 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "game/entities/enemy_ai.hpp" // AimMode
|
||||||
|
|
||||||
enum class EnemyEventType : uint8_t {
|
enum class EnemyEventType : uint8_t {
|
||||||
ON_HIT, // Impactat per una bala
|
ON_HIT, // Impactat per una bala
|
||||||
ON_HURT_END, // Timer wounded ha expirat aquest frame
|
ON_NO_HEALTH, // health ha arribat a 0 o menys aquest frame (via DECREASE_HEALTH)
|
||||||
ON_DESTROY, // L'acció destroy s'està executant (efectes col·laterals)
|
ON_HURT_END, // Timer wounded ha expirat aquest frame
|
||||||
|
ON_DESTROY, // L'acció destroy s'està executant (efectes col·laterals)
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class EnemyActionType : uint8_t {
|
enum class EnemyActionType : uint8_t {
|
||||||
SET_HURT, // Entra estat wounded (o destrueix si ja era wounded)
|
SET_HURT, // Entra estat wounded (o destrueix si ja era wounded)
|
||||||
DESTROY, // Dispara on_destroy + desactiva físicament
|
DESTROY, // Dispara on_destroy + desactiva físicament
|
||||||
ADD_SCORE, // Suma config.score al shooter + floating score
|
ADD_SCORE, // Suma config.score al shooter + floating score
|
||||||
CREATE_DEBRIS, // Explosió de debris amb herència de velocitat
|
CREATE_DEBRIS, // Explosió de debris amb herència de velocitat
|
||||||
CREATE_FIREWORKS, // Burst radial de firework
|
CREATE_DEBRIS_PARTIAL, // Debris de xip parcial (trossos a escala 0.3, per hits HP>1)
|
||||||
APPLY_IMPULSE, // Aplica l'impuls de la bala impactant
|
CREATE_FIREWORKS, // Burst radial de firework
|
||||||
|
CREATE_FIREWORKS_SMALL, // Burst petit (pocs punts, poca velocitat) — feedback per hit
|
||||||
|
APPLY_IMPULSE, // Aplica l'impuls de la bala impactant
|
||||||
|
DECREASE_HEALTH, // Decrementa health_; si <=0, dispatcha ON_NO_HEALTH
|
||||||
|
FLASH, // Flash visual breu (feedback per impacte parcial)
|
||||||
|
FIRE_BULLET, // Dispara una bala (config per nom) dirigida o aleatòria
|
||||||
};
|
};
|
||||||
|
|
||||||
struct EnemyAction {
|
struct EnemyAction {
|
||||||
EnemyActionType type;
|
EnemyActionType type;
|
||||||
|
|
||||||
|
// Payload de FIRE_BULLET (ignorat per altres tipus). Paral·lel a AiTickAction
|
||||||
|
// perquè un futur refactor pugui compartir doShoot/doFireBullet si val la pena.
|
||||||
|
std::string bullet_config_name;
|
||||||
|
float bullet_speed{200.0F};
|
||||||
|
AimMode aim_mode{AimMode::RANDOM};
|
||||||
|
float jitter_rad{0.0F};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct EnemyEventConfig {
|
struct EnemyEventConfig {
|
||||||
std::vector<EnemyAction> on_hit;
|
std::vector<EnemyAction> on_hit;
|
||||||
|
std::vector<EnemyAction> on_no_health;
|
||||||
std::vector<EnemyAction> on_hurt_end;
|
std::vector<EnemyAction> on_hurt_end;
|
||||||
std::vector<EnemyAction> on_destroy;
|
std::vector<EnemyAction> on_destroy;
|
||||||
|
|
||||||
@@ -38,6 +55,8 @@ struct EnemyEventConfig {
|
|||||||
switch (event) {
|
switch (event) {
|
||||||
case EnemyEventType::ON_HIT:
|
case EnemyEventType::ON_HIT:
|
||||||
return on_hit;
|
return on_hit;
|
||||||
|
case EnemyEventType::ON_NO_HEALTH:
|
||||||
|
return on_no_health;
|
||||||
case EnemyEventType::ON_HURT_END:
|
case EnemyEventType::ON_HURT_END:
|
||||||
return on_hurt_end;
|
return on_hurt_end;
|
||||||
case EnemyEventType::ON_DESTROY:
|
case EnemyEventType::ON_DESTROY:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ EnemyConfig EnemyRegistry::pentagon_config;
|
|||||||
EnemyConfig EnemyRegistry::square_config;
|
EnemyConfig EnemyRegistry::square_config;
|
||||||
EnemyConfig EnemyRegistry::pinwheel_config;
|
EnemyConfig EnemyRegistry::pinwheel_config;
|
||||||
EnemyConfig EnemyRegistry::star_config;
|
EnemyConfig EnemyRegistry::star_config;
|
||||||
|
EnemyConfig EnemyRegistry::orb_config;
|
||||||
bool EnemyRegistry::loaded = false;
|
bool EnemyRegistry::loaded = false;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -38,10 +39,11 @@ auto EnemyRegistry::loadAll() -> bool {
|
|||||||
const bool OK = loadOne("pentagon", EnemyType::PENTAGON, pentagon_config) &&
|
const bool OK = loadOne("pentagon", EnemyType::PENTAGON, pentagon_config) &&
|
||||||
loadOne("square", EnemyType::SQUARE, square_config) &&
|
loadOne("square", EnemyType::SQUARE, square_config) &&
|
||||||
loadOne("pinwheel", EnemyType::PINWHEEL, pinwheel_config) &&
|
loadOne("pinwheel", EnemyType::PINWHEEL, pinwheel_config) &&
|
||||||
loadOne("star", EnemyType::STAR, star_config);
|
loadOne("star", EnemyType::STAR, star_config) &&
|
||||||
|
loadOne("orb", EnemyType::ORB, orb_config);
|
||||||
loaded = OK;
|
loaded = OK;
|
||||||
if (OK) {
|
if (OK) {
|
||||||
std::cout << "[EnemyRegistry] 4 configuracions d'enemic carregades.\n";
|
std::cout << "[EnemyRegistry] 5 configuracions d'enemic carregades.\n";
|
||||||
}
|
}
|
||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
@@ -60,6 +62,8 @@ auto EnemyRegistry::get(EnemyType type) -> const EnemyConfig& {
|
|||||||
return pinwheel_config;
|
return pinwheel_config;
|
||||||
case EnemyType::STAR:
|
case EnemyType::STAR:
|
||||||
return star_config;
|
return star_config;
|
||||||
|
case EnemyType::ORB:
|
||||||
|
return orb_config;
|
||||||
}
|
}
|
||||||
std::cerr << "[EnemyRegistry] FATAL: tipus desconegut\n";
|
std::cerr << "[EnemyRegistry] FATAL: tipus desconegut\n";
|
||||||
std::exit(EXIT_FAILURE);
|
std::exit(EXIT_FAILURE);
|
||||||
|
|||||||
@@ -27,5 +27,6 @@ class EnemyRegistry {
|
|||||||
static EnemyConfig square_config;
|
static EnemyConfig square_config;
|
||||||
static EnemyConfig pinwheel_config;
|
static EnemyConfig pinwheel_config;
|
||||||
static EnemyConfig star_config;
|
static EnemyConfig star_config;
|
||||||
|
static EnemyConfig orb_config;
|
||||||
static bool loaded;
|
static bool loaded;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Ship::Ship(Rendering::Renderer* renderer, PlayerConfig config, const char* shape
|
|||||||
config_(std::move(config)) {
|
config_(std::move(config)) {
|
||||||
brightness_ = Defaults::Brightness::NAU;
|
brightness_ = Defaults::Brightness::NAU;
|
||||||
|
|
||||||
// El shape pot venir del YAML o ser overridden (ex: P2 amb "ship2.shp").
|
// El shape pot venir del YAML o ser overridden (ex: P2 amb "ship/wedge.shp").
|
||||||
const std::string SHAPE_PATH = (shape_override != nullptr) ? shape_override : config_.shape.path;
|
const std::string SHAPE_PATH = (shape_override != nullptr) ? shape_override : config_.shape.path;
|
||||||
shape_ = Graphics::ShapeLoader::load(SHAPE_PATH);
|
shape_ = Graphics::ShapeLoader::load(SHAPE_PATH);
|
||||||
if (!shape_ || !shape_->isValid()) {
|
if (!shape_ || !shape_->isValid()) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
#include "game/stage_system/stage_loader.hpp"
|
#include "game/stage_system/stage_loader.hpp"
|
||||||
#include "game/systems/collision_system.hpp"
|
#include "game/systems/collision_system.hpp"
|
||||||
#include "game/systems/continue_system.hpp"
|
#include "game/systems/continue_system.hpp"
|
||||||
|
#include "game/systems/enemy_ai_system.hpp"
|
||||||
#include "game/systems/init_hud_animator.hpp"
|
#include "game/systems/init_hud_animator.hpp"
|
||||||
|
|
||||||
// Using declarations per simplificar el codi
|
// Using declarations per simplificar el codi
|
||||||
@@ -81,8 +82,8 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inicialitzar naves: P1 amb el shape del YAML, P2 amb override visual.
|
// Inicialitzar naves: P1 amb el shape del YAML, P2 amb override visual.
|
||||||
ships_[0] = Ship(sdl.getRenderer(), *player_config); // Jugador 1: nau estàndard
|
ships_[0] = Ship(sdl.getRenderer(), *player_config); // Jugador 1: nau estàndard
|
||||||
ships_[1] = Ship(sdl.getRenderer(), *player_config, "ship2.shp"); // Jugador 2: interceptor amb ales
|
ships_[1] = Ship(sdl.getRenderer(), *player_config, "ship/wedge.shp"); // Jugador 2: triangle amb cercle central
|
||||||
|
|
||||||
// Inicialitzar balas con renderer
|
// Inicialitzar balas con renderer
|
||||||
std::ranges::fill(bullets_, Bullet(sdl.getRenderer()));
|
std::ranges::fill(bullets_, Bullet(sdl.getRenderer()));
|
||||||
@@ -141,7 +142,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
|||||||
stage_manager_->init();
|
stage_manager_->init();
|
||||||
|
|
||||||
// Set ship position reference for safe spawn (P1 for now, TODO: dual tracking)
|
// Set ship position reference for safe spawn (P1 for now, TODO: dual tracking)
|
||||||
stage_manager_->getSpawnController().setShipPosition(&ships_[0].getCenter());
|
stage_manager_->getWaveRunner().setShipPosition(&ships_[0].getCenter());
|
||||||
|
|
||||||
// Inicialitzar timers de muerte per player
|
// Inicialitzar timers de muerte per player
|
||||||
hit_timer_per_player_[0] = 0.0F;
|
hit_timer_per_player_[0] = 0.0F;
|
||||||
@@ -185,7 +186,7 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
|
|||||||
// Registramos el body al world incluso inactivo: con radius=0 no colisiona
|
// Registramos el body al world incluso inactivo: con radius=0 no colisiona
|
||||||
// ni se mueve, y al init() del stage system se activa sin re-registrar.
|
// ni se mueve, y al init() del stage system se activa sin re-registrar.
|
||||||
for (auto& enemy : enemies_) {
|
for (auto& enemy : enemies_) {
|
||||||
enemy.setShipPosition(&ships_[0].getCenter()); // Set ship reference (P1 for now)
|
enemy.setShips(ships_.data(), &ships_[1]);
|
||||||
physics_world_.addBody(&enemy.getBody());
|
physics_world_.addBody(&enemy.getBody());
|
||||||
// DON'T call enemy.init() here - stage system handles spawning
|
// DON'T call enemy.init() here - stage system handles spawning
|
||||||
}
|
}
|
||||||
@@ -353,6 +354,7 @@ auto GameScene::stepContinueScreen(float delta_time) -> bool {
|
|||||||
|
|
||||||
// Enemies, bullets y efectos siguen moviéndose en background.
|
// Enemies, bullets y efectos siguen moviéndose en background.
|
||||||
for (auto& enemy : enemies_) {
|
for (auto& enemy : enemies_) {
|
||||||
|
Systems::EnemyAi::move(enemy, delta_time);
|
||||||
enemy.update(delta_time);
|
enemy.update(delta_time);
|
||||||
}
|
}
|
||||||
for (auto& bullet : bullets_) {
|
for (auto& bullet : bullets_) {
|
||||||
@@ -379,6 +381,7 @@ auto GameScene::stepGameOver(float delta_time) -> bool {
|
|||||||
|
|
||||||
// Enemies, bullets y efectos siguen moviéndose como fondo.
|
// Enemies, bullets y efectos siguen moviéndose como fondo.
|
||||||
for (auto& enemy : enemies_) {
|
for (auto& enemy : enemies_) {
|
||||||
|
Systems::EnemyAi::move(enemy, delta_time);
|
||||||
enemy.update(delta_time);
|
enemy.update(delta_time);
|
||||||
}
|
}
|
||||||
for (auto& bullet : bullets_) {
|
for (auto& bullet : bullets_) {
|
||||||
@@ -428,6 +431,7 @@ void GameScene::stepDeathSequence(float delta_time) {
|
|||||||
// aunque otros jugadores aún jueguen.
|
// aunque otros jugadores aún jueguen.
|
||||||
if (algun_mort) {
|
if (algun_mort) {
|
||||||
for (auto& enemy : enemies_) {
|
for (auto& enemy : enemies_) {
|
||||||
|
Systems::EnemyAi::move(enemy, delta_time);
|
||||||
enemy.update(delta_time);
|
enemy.update(delta_time);
|
||||||
}
|
}
|
||||||
for (auto& bullet : bullets_) {
|
for (auto& bullet : bullets_) {
|
||||||
@@ -509,11 +513,11 @@ void GameScene::runStageLevelStart(float delta_time) {
|
|||||||
|
|
||||||
void GameScene::runStagePlaying(float delta_time) {
|
void GameScene::runStagePlaying(float delta_time) {
|
||||||
const bool PAUSE_SPAWN = (hit_timer_per_player_[0] > 0.0F && hit_timer_per_player_[1] > 0.0F);
|
const bool PAUSE_SPAWN = (hit_timer_per_player_[0] > 0.0F && hit_timer_per_player_[1] > 0.0F);
|
||||||
stage_manager_->getSpawnController().update(delta_time, enemies_, PAUSE_SPAWN);
|
stage_manager_->getWaveRunner().update(delta_time, enemies_, PAUSE_SPAWN);
|
||||||
|
|
||||||
// Stage completado: cuando al menos un jugador está vivo y todos los enemies muertos.
|
// Stage completado: cuando al menos un jugador está vivo y todas las onades emeses y arena buida.
|
||||||
const bool ALGU_VIU = (hit_timer_per_player_[0] == 0.0F || hit_timer_per_player_[1] == 0.0F);
|
const bool ALGU_VIU = (hit_timer_per_player_[0] == 0.0F || hit_timer_per_player_[1] == 0.0F);
|
||||||
if (ALGU_VIU && stage_manager_->getSpawnController().allEnemiesDestroyed(enemies_)) {
|
if (ALGU_VIU && stage_manager_->getWaveRunner().stageComplete(enemies_)) {
|
||||||
stage_manager_->markStageCompleted();
|
stage_manager_->markStageCompleted();
|
||||||
Audio::get()->playSound(Defaults::Sound::GOOD_JOB_COMMANDER, Audio::Group::GAME);
|
Audio::get()->playSound(Defaults::Sound::GOOD_JOB_COMMANDER, Audio::Group::GAME);
|
||||||
return;
|
return;
|
||||||
@@ -527,8 +531,10 @@ void GameScene::runStagePlaying(float delta_time) {
|
|||||||
ships_[i].update(delta_time);
|
ships_[i].update(delta_time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (auto& enemy : enemies_) {
|
auto ai_ctx = buildCollisionContext();
|
||||||
enemy.update(delta_time);
|
for (std::size_t i = 0; i < enemies_.size(); ++i) {
|
||||||
|
Systems::EnemyAi::tick(ai_ctx, enemies_[i], i, delta_time);
|
||||||
|
enemies_[i].update(delta_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Col·lisions primer, després desactivació per fora-de-zone: així una bala que
|
// Col·lisions primer, després desactivació per fora-de-zone: així una bala que
|
||||||
@@ -562,8 +568,8 @@ void GameScene::runStageLevelCompleted(float delta_time) {
|
|||||||
floating_score_manager_.update(delta_time);
|
floating_score_manager_.update(delta_time);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::runCollisionDetections() {
|
auto GameScene::buildCollisionContext() -> Systems::Collision::Context {
|
||||||
Systems::Collision::Context col_ctx{
|
return Systems::Collision::Context{
|
||||||
.ships = ships_,
|
.ships = ships_,
|
||||||
.enemies = enemies_,
|
.enemies = enemies_,
|
||||||
.bullets = bullets_,
|
.bullets = bullets_,
|
||||||
@@ -574,8 +580,12 @@ void GameScene::runCollisionDetections() {
|
|||||||
.firework_manager = firework_manager_,
|
.firework_manager = firework_manager_,
|
||||||
.floating_score_manager = floating_score_manager_,
|
.floating_score_manager = floating_score_manager_,
|
||||||
.match_config = match_config_,
|
.match_config = match_config_,
|
||||||
.on_player_hit = [this](uint8_t pid) { tocado(pid); },
|
.on_player_hit = [this](uint8_t pid, const Vec2& bv) { tocado(pid, bv); },
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void GameScene::runCollisionDetections() {
|
||||||
|
auto col_ctx = buildCollisionContext();
|
||||||
Systems::Collision::detectAll(col_ctx);
|
Systems::Collision::detectAll(col_ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,7 +710,7 @@ void GameScene::drawInitHudState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (score_progress > 0.0F) {
|
if (score_progress > 0.0F) {
|
||||||
Systems::InitHud::drawScoreboardAnimated(text_, buildScoreboard(), score_progress);
|
Systems::InitHud::drawScoreboardAnimated(text_, buildScoreboardSegments(), score_progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ship1_progress > 0.0F && match_config_.player1_active && ships_[0].isActive()) {
|
if (ship1_progress > 0.0F && match_config_.player1_active && ships_[0].isActive()) {
|
||||||
@@ -754,7 +764,7 @@ void GameScene::drawLevelCompletedState() {
|
|||||||
drawScoreboard();
|
drawScoreboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::tocado(uint8_t player_id) {
|
void GameScene::tocado(uint8_t player_id, const Vec2& bullet_velocity) {
|
||||||
// Death sequence: 3 phases
|
// Death sequence: 3 phases
|
||||||
// Phase 1: First call (hit_timer_per_player_[player_id] == 0) - trigger explosion
|
// Phase 1: First call (hit_timer_per_player_[player_id] == 0) - trigger explosion
|
||||||
// Phase 2: Animation (0 < itocado_ < 3.0s) - debris animation
|
// Phase 2: Animation (0 < itocado_ < 3.0s) - debris animation
|
||||||
@@ -778,6 +788,9 @@ void GameScene::tocado(uint8_t player_id) {
|
|||||||
|
|
||||||
// Mateixa dispersió i efecte que els debris d'enemic (lifetime,
|
// Mateixa dispersió i efecte que els debris d'enemic (lifetime,
|
||||||
// friction, segment_multiplier alineats); només canvien sound i color.
|
// friction, segment_multiplier alineats); només canvien sound i color.
|
||||||
|
// bullet_velocity arriba a explode() com a impuls extra independent
|
||||||
|
// de la inèrcia del cos del ship — els trossos volen amb la força
|
||||||
|
// de la bala encara que el ship estiga quiet.
|
||||||
debris_manager_.explode(
|
debris_manager_.explode(
|
||||||
ships_[player_id].getShape(),
|
ships_[player_id].getShape(),
|
||||||
SHIP_POS,
|
SHIP_POS,
|
||||||
@@ -788,11 +801,12 @@ void GameScene::tocado(uint8_t player_id) {
|
|||||||
INHERITED_VEL,
|
INHERITED_VEL,
|
||||||
0.0F, // sense herència angular
|
0.0F, // sense herència angular
|
||||||
0.0F, // sin herencia visual
|
0.0F, // sin herencia visual
|
||||||
Defaults::Sound::EXPLOSION2,
|
Defaults::Sound::PLAYER_EXPLOSION,
|
||||||
ships_[player_id].getConfig().colors.normal,
|
ships_[player_id].getConfig().colors.normal,
|
||||||
Defaults::Physics::Debris::ENEMY_LIFETIME,
|
Defaults::Physics::Debris::ENEMY_LIFETIME,
|
||||||
Defaults::Physics::Debris::ENEMY_FRICTION,
|
Defaults::Physics::Debris::ENEMY_FRICTION,
|
||||||
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER);
|
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER,
|
||||||
|
bullet_velocity);
|
||||||
|
|
||||||
// Start death timer (non-zero to avoid re-triggering)
|
// Start death timer (non-zero to avoid re-triggering)
|
||||||
hit_timer_per_player_[player_id] = Defaults::Game::HIT_TIMER_TRIGGER_DEATH;
|
hit_timer_per_player_[player_id] = Defaults::Game::HIT_TIMER_TRIGGER_DEATH;
|
||||||
@@ -802,59 +816,49 @@ void GameScene::tocado(uint8_t player_id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GameScene::drawScoreboard() {
|
void GameScene::drawScoreboard() {
|
||||||
// Construir text del marcador
|
|
||||||
std::string text = buildScoreboard();
|
|
||||||
|
|
||||||
// Parámetros de renderització
|
|
||||||
const float SCALE = Defaults::Hud::SCOREBOARD_TEXT_SCALE;
|
const float SCALE = Defaults::Hud::SCOREBOARD_TEXT_SCALE;
|
||||||
const float SPACING = Defaults::Hud::SCOREBOARD_TEXT_SPACING;
|
const float SPACING = Defaults::Hud::SCOREBOARD_TEXT_SPACING;
|
||||||
|
|
||||||
// Calcular centro de la zone del marcador
|
|
||||||
const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD;
|
const SDL_FRect& scoreboard_zone = Defaults::Zones::SCOREBOARD;
|
||||||
float center_x = scoreboard_zone.w / 2.0F;
|
const Vec2 CENTER = {
|
||||||
float center_y = scoreboard_zone.y + (scoreboard_zone.h / 2.0F);
|
.x = scoreboard_zone.w / 2.0F,
|
||||||
|
.y = scoreboard_zone.y + (scoreboard_zone.h / 2.0F),
|
||||||
// Renderizar centrat
|
};
|
||||||
text_.renderCentered(text, {.x = center_x, .y = center_y}, SCALE, SPACING);
|
Systems::InitHud::drawScoreboardSegmentsAt(text_, buildScoreboardSegments(), CENTER, SCALE, SPACING);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto GameScene::buildScoreboard() const -> std::string {
|
auto GameScene::buildScoreboardSegments() const -> Systems::InitHud::ScoreboardSegments {
|
||||||
// Puntuación P1 (6 dígits) - mostrar zeros si inactiu
|
Systems::InitHud::ScoreboardSegments out;
|
||||||
std::string score_p1;
|
|
||||||
std::string vides_p1;
|
// Puntuació P1 (6 dígits) - zeros si inactiu
|
||||||
if (match_config_.player1_active) {
|
if (match_config_.player1_active) {
|
||||||
score_p1 = std::to_string(score_per_player_[0]);
|
std::string s = std::to_string(score_per_player_[0]);
|
||||||
score_p1 = std::string(6 - std::min(6, static_cast<int>(score_p1.length())), '0') + score_p1;
|
out.score_p1 = std::string(6 - std::min(6, static_cast<int>(s.length())), '0') + s;
|
||||||
vides_p1 = (lives_per_player_[0] < 10)
|
out.lives_p1 = (lives_per_player_[0] < 10)
|
||||||
? "0" + std::to_string(lives_per_player_[0])
|
? "0" + std::to_string(lives_per_player_[0])
|
||||||
: std::to_string(lives_per_player_[0]);
|
: std::to_string(lives_per_player_[0]);
|
||||||
} else {
|
} else {
|
||||||
score_p1 = "000000";
|
out.score_p1 = "000000";
|
||||||
vides_p1 = "00";
|
out.lives_p1 = "00";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nivel (2 dígits)
|
// Nivell (2 dígits) amb label localitzat
|
||||||
uint8_t stage_num = stage_manager_->getCurrentStage();
|
const uint8_t STAGE_NUM = stage_manager_->getCurrentStage();
|
||||||
std::string stage_str = (stage_num < 10) ? "0" + std::to_string(stage_num)
|
const std::string STAGE_STR = (STAGE_NUM < 10) ? "0" + std::to_string(STAGE_NUM)
|
||||||
: std::to_string(stage_num);
|
: std::to_string(STAGE_NUM);
|
||||||
|
out.level = Locale::get().text("hud.level") + STAGE_STR;
|
||||||
|
|
||||||
// Puntuación P2 (6 dígits) - mostrar zeros si inactiu
|
// Puntuació P2 (6 dígits) - zeros si inactiu
|
||||||
std::string score_p2;
|
|
||||||
std::string vides_p2;
|
|
||||||
if (match_config_.player2_active) {
|
if (match_config_.player2_active) {
|
||||||
score_p2 = std::to_string(score_per_player_[1]);
|
std::string s = std::to_string(score_per_player_[1]);
|
||||||
score_p2 = std::string(6 - std::min(6, static_cast<int>(score_p2.length())), '0') + score_p2;
|
out.score_p2 = std::string(6 - std::min(6, static_cast<int>(s.length())), '0') + s;
|
||||||
vides_p2 = (lives_per_player_[1] < 10)
|
out.lives_p2 = (lives_per_player_[1] < 10)
|
||||||
? "0" + std::to_string(lives_per_player_[1])
|
? "0" + std::to_string(lives_per_player_[1])
|
||||||
: std::to_string(lives_per_player_[1]);
|
: std::to_string(lives_per_player_[1]);
|
||||||
} else {
|
} else {
|
||||||
score_p2 = "000000";
|
out.score_p2 = "000000";
|
||||||
vides_p2 = "00";
|
out.lives_p2 = "00";
|
||||||
}
|
}
|
||||||
|
return out;
|
||||||
// Format: "123456 03 LEVEL 01 654321 02"
|
|
||||||
// Nota: dos espais entre seccions, mantenir ambdós slots siempre visibles
|
|
||||||
return score_p1 + " " + vides_p1 + " " + Locale::get().text("hud.level") + stage_str + " " + score_p2 + " " + vides_p2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// [NEW] Stage system helper methods
|
// [NEW] Stage system helper methods
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
#include "game/entities/ship.hpp"
|
#include "game/entities/ship.hpp"
|
||||||
#include "game/stage_system/stage_config.hpp"
|
#include "game/stage_system/stage_config.hpp"
|
||||||
#include "game/stage_system/stage_manager.hpp"
|
#include "game/stage_system/stage_manager.hpp"
|
||||||
|
#include "game/systems/collision_system.hpp"
|
||||||
|
#include "game/systems/init_hud_animator.hpp"
|
||||||
|
|
||||||
// Game over state machine
|
// Game over state machine
|
||||||
enum class GameOverState : uint8_t {
|
enum class GameOverState : uint8_t {
|
||||||
@@ -67,7 +69,7 @@ class GameScene final : public Scene {
|
|||||||
std::array<Enemy, Constants::MAX_ORNIS> enemies_;
|
std::array<Enemy, Constants::MAX_ORNIS> enemies_;
|
||||||
// 6 balas: P1=[0,1,2], P2=[3,4,5]. El cast a size_t evita la
|
// 6 balas: P1=[0,1,2], P2=[3,4,5]. El cast a size_t evita la
|
||||||
// widening conversion implícita que detecta clang-tidy.
|
// widening conversion implícita que detecta clang-tidy.
|
||||||
std::array<Bullet, static_cast<std::size_t>(Constants::MAX_BULLETS) * 2> bullets_;
|
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS_TOTAL)> bullets_;
|
||||||
std::array<float, 2> hit_timer_per_player_; // Death timers per player (seconds)
|
std::array<float, 2> hit_timer_per_player_; // Death timers per player (seconds)
|
||||||
|
|
||||||
// Lives and game over system
|
// Lives and game over system
|
||||||
@@ -100,7 +102,10 @@ class GameScene final : public Scene {
|
|||||||
bool init_hud_rect_sound_played_{false}; // Flag para evitar repetir sonido del rectángulo
|
bool init_hud_rect_sound_played_{false}; // Flag para evitar repetir sonido del rectángulo
|
||||||
|
|
||||||
// Funciones privades
|
// Funciones privades
|
||||||
void tocado(uint8_t player_id);
|
// bullet_velocity: velocitat de la bala que ha causat la mort (Vec2{} si no
|
||||||
|
// ve d'una bala). Es passa al debris perquè els fragments volin en direcció
|
||||||
|
// de la bala (independent de la inèrcia del cos del ship).
|
||||||
|
void tocado(uint8_t player_id, const Vec2& bullet_velocity = {.x = 0.0F, .y = 0.0F});
|
||||||
void drawScoreboard(); // Dibuixar marcador de puntuación
|
void drawScoreboard(); // Dibuixar marcador de puntuación
|
||||||
void fireBullet(uint8_t player_id); // Shoot bullet from player
|
void fireBullet(uint8_t player_id); // Shoot bullet from player
|
||||||
[[nodiscard]] auto getSpawnPoint(uint8_t player_id) const -> Vec2; // Get spawn position for player
|
[[nodiscard]] auto getSpawnPoint(uint8_t player_id) const -> Vec2; // Get spawn position for player
|
||||||
@@ -124,8 +129,9 @@ class GameScene final : public Scene {
|
|||||||
void drawPlayingState();
|
void drawPlayingState();
|
||||||
void drawLevelCompletedState();
|
void drawLevelCompletedState();
|
||||||
|
|
||||||
// [NEW] Función helper del marcador
|
// [NEW] Helper del marcador: construeix els 5 segments (score_p1, vides_p1,
|
||||||
[[nodiscard]] auto buildScoreboard() const -> std::string;
|
// level, score_p2, vides_p2) per a render colorit per segment.
|
||||||
|
[[nodiscard]] auto buildScoreboardSegments() const -> Systems::InitHud::ScoreboardSegments;
|
||||||
|
|
||||||
// Sub-pasos de update() (descompuestos en Fase 9d para reducir
|
// Sub-pasos de update() (descompuestos en Fase 9d para reducir
|
||||||
// complejidad cognitiva; cada uno es responsable de una sección).
|
// complejidad cognitiva; cada uno es responsable de una sección).
|
||||||
@@ -144,6 +150,10 @@ class GameScene final : public Scene {
|
|||||||
void runStageLevelStart(float delta_time);
|
void runStageLevelStart(float delta_time);
|
||||||
void runStagePlaying(float delta_time);
|
void runStagePlaying(float delta_time);
|
||||||
void runStageLevelCompleted(float delta_time);
|
void runStageLevelCompleted(float delta_time);
|
||||||
|
// Construeix el Collision::Context del frame actual (referències a tots els
|
||||||
|
// pools/managers + on_player_hit). Reutilitzat tant per al tick d'IA com
|
||||||
|
// per a runCollisionDetections.
|
||||||
|
[[nodiscard]] auto buildCollisionContext() -> Systems::Collision::Context;
|
||||||
// Helper: ejecuta colisiones de gameplay con el Context preparado.
|
// Helper: ejecuta colisiones de gameplay con el Context preparado.
|
||||||
void runCollisionDetections();
|
void runCollisionDetections();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ TitleScene::TitleScene(SDLManager& sdl, SceneContext& context)
|
|||||||
|
|
||||||
// Flash que tapa el "pop" final de la nau al VP. Es spawneja al centre
|
// Flash que tapa el "pop" final de la nau al VP. Es spawneja al centre
|
||||||
// de pantalla (= projecció del VP) quan ship_animator avisa.
|
// de pantalla (= projecció del VP) quan ship_animator avisa.
|
||||||
flash_shape_ = Graphics::ShapeLoader::load("title_flash.shp");
|
flash_shape_ = Graphics::ShapeLoader::load("effect/title_flash.shp");
|
||||||
ship_animator_->setOnShipDisappear([this](int /*player_id*/) {
|
ship_animator_->setOnShipDisappear([this](int /*player_id*/) {
|
||||||
triggerFlash(Vec2{
|
triggerFlash(Vec2{
|
||||||
.x = static_cast<float>(Defaults::Window::WIDTH) / 2.0F,
|
.x = static_cast<float>(Defaults::Window::WIDTH) / 2.0F,
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
// spawn_controller.cpp - Implementació del controlador de spawn
|
|
||||||
// © 2026 JailDesigner
|
|
||||||
|
|
||||||
#include "spawn_controller.hpp"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <iostream>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include "core/types.hpp"
|
|
||||||
#include "game/entities/enemy.hpp"
|
|
||||||
#include "stage_config.hpp"
|
|
||||||
|
|
||||||
namespace StageSystem {
|
|
||||||
|
|
||||||
SpawnController::SpawnController() = default;
|
|
||||||
|
|
||||||
void SpawnController::configure(const StageConfig* config) {
|
|
||||||
config_ = config;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SpawnController::start() {
|
|
||||||
if (config_ == nullptr) {
|
|
||||||
std::cerr << "[SpawnController] Error: config_ es null" << '\n';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
reset();
|
|
||||||
generateSpawnEvents();
|
|
||||||
|
|
||||||
std::cout << "[SpawnController] Stage " << static_cast<int>(config_->stage_id)
|
|
||||||
<< ": generats " << spawn_queue_.size() << " spawn events" << '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
void SpawnController::reset() {
|
|
||||||
spawn_queue_.clear();
|
|
||||||
temps_transcorregut_ = 0.0F;
|
|
||||||
index_spawn_actual_ = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SpawnController::update(float delta_time, std::array<Enemy, 15>& orni_array, bool pausar) {
|
|
||||||
if ((config_ == nullptr) || spawn_queue_.empty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increment timer only when not paused
|
|
||||||
if (!pausar) {
|
|
||||||
temps_transcorregut_ += delta_time;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process spawn events
|
|
||||||
while (index_spawn_actual_ < spawn_queue_.size()) {
|
|
||||||
SpawnEvent& event = spawn_queue_[index_spawn_actual_];
|
|
||||||
|
|
||||||
if (event.spawnejat) {
|
|
||||||
index_spawn_actual_++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (temps_transcorregut_ >= event.temps_spawn) {
|
|
||||||
// Find first inactive enemy
|
|
||||||
for (auto& enemy : orni_array) {
|
|
||||||
if (!enemy.isActive()) {
|
|
||||||
spawnEnemy(enemy, event.type, ship_position_);
|
|
||||||
event.spawnejat = true;
|
|
||||||
index_spawn_actual_++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no slot available, try next frame
|
|
||||||
if (!event.spawnejat) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Not yet time for this spawn
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto SpawnController::allEnemiesSpawned() const -> bool {
|
|
||||||
return index_spawn_actual_ >= spawn_queue_.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto SpawnController::allEnemiesDestroyed(const std::array<Enemy, 15>& orni_array) const -> bool {
|
|
||||||
if (!allEnemiesSpawned()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return std::ranges::all_of(orni_array, [](const Enemy& enemy) { return !enemy.isActive(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
auto SpawnController::getAliveEnemyCount(const std::array<Enemy, 15>& orni_array) -> uint8_t {
|
|
||||||
uint8_t count = 0;
|
|
||||||
for (const auto& enemy : orni_array) {
|
|
||||||
if (enemy.isActive()) {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto SpawnController::countSpawnedEnemies() const -> uint8_t {
|
|
||||||
return static_cast<uint8_t>(index_spawn_actual_);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SpawnController::generateSpawnEvents() {
|
|
||||||
if (config_ == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (uint8_t i = 0; i < config_->total_enemies; i++) {
|
|
||||||
float spawn_time = config_->config_spawn.delay_inicial +
|
|
||||||
(i * config_->config_spawn.interval_spawn);
|
|
||||||
|
|
||||||
EnemyType type = selectRandomType();
|
|
||||||
|
|
||||||
spawn_queue_.push_back({spawn_time, type, false});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto SpawnController::selectRandomType() const -> EnemyType {
|
|
||||||
if (config_ == nullptr) {
|
|
||||||
return EnemyType::PENTAGON;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Weighted random selection based on distribution
|
|
||||||
int rand_val = std::rand() % 100;
|
|
||||||
|
|
||||||
if (std::cmp_less(rand_val, config_->distribucio.pentagon)) {
|
|
||||||
return EnemyType::PENTAGON;
|
|
||||||
}
|
|
||||||
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado) {
|
|
||||||
return EnemyType::SQUARE;
|
|
||||||
}
|
|
||||||
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado + config_->distribucio.molinillo) {
|
|
||||||
return EnemyType::PINWHEEL;
|
|
||||||
}
|
|
||||||
return EnemyType::STAR;
|
|
||||||
}
|
|
||||||
|
|
||||||
void SpawnController::spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos) {
|
|
||||||
// Initialize enemy (with safe spawn if ship_pos provided)
|
|
||||||
enemy.init(type, ship_pos);
|
|
||||||
|
|
||||||
// Apply difficulty multipliers
|
|
||||||
applyMultipliers(enemy);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SpawnController::applyMultipliers(Enemy& enemy) const {
|
|
||||||
if (config_ == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply velocity multiplier
|
|
||||||
float base_vel = enemy.getBaseVelocity();
|
|
||||||
enemy.setVelocity(base_vel * config_->multiplicadors.velocity);
|
|
||||||
|
|
||||||
// Apply rotation multiplier
|
|
||||||
float base_rot = enemy.getBaseRotation();
|
|
||||||
enemy.setRotation(base_rot * config_->multiplicadors.rotation);
|
|
||||||
|
|
||||||
// Apply tracking strength (only affects SQUARE)
|
|
||||||
enemy.setTrackingStrength(config_->multiplicadors.tracking_strength);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace StageSystem
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
// spawn_controller.hpp - Controlador de spawn de enemigos
|
|
||||||
// © 2026 JailDesigner
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "core/types.hpp"
|
|
||||||
#include "game/entities/enemy.hpp"
|
|
||||||
#include "stage_config.hpp"
|
|
||||||
|
|
||||||
namespace StageSystem {
|
|
||||||
|
|
||||||
// Informació de spawn planificat
|
|
||||||
struct SpawnEvent {
|
|
||||||
float temps_spawn; // Temps absolut (segons) per spawnejar
|
|
||||||
EnemyType type; // Tipo de enemy
|
|
||||||
bool spawnejat; // Ya s'ha processat?
|
|
||||||
};
|
|
||||||
|
|
||||||
class SpawnController {
|
|
||||||
public:
|
|
||||||
SpawnController();
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
void configure(const StageConfig* config); // Set stage config
|
|
||||||
void start(); // Generate spawn schedule
|
|
||||||
void reset(); // Clear all pending spawns
|
|
||||||
|
|
||||||
// Update
|
|
||||||
void update(float delta_time, std::array<Enemy, 15>& orni_array, bool pausar = false);
|
|
||||||
|
|
||||||
// Status queries
|
|
||||||
[[nodiscard]] auto allEnemiesSpawned() const -> bool;
|
|
||||||
[[nodiscard]] auto allEnemiesDestroyed(const std::array<Enemy, 15>& orni_array) const -> bool;
|
|
||||||
// Estático: solo recorre el array pasado; no consulta estado del controller.
|
|
||||||
[[nodiscard]] static auto getAliveEnemyCount(const std::array<Enemy, 15>& orni_array) -> uint8_t;
|
|
||||||
[[nodiscard]] auto countSpawnedEnemies() const -> uint8_t;
|
|
||||||
|
|
||||||
// [NEW] Set ship position reference for safe spawn
|
|
||||||
void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
const StageConfig* config_{nullptr}; // Non-owning pointer to current stage config
|
|
||||||
std::vector<SpawnEvent> spawn_queue_;
|
|
||||||
float temps_transcorregut_{0.0F}; // Elapsed time since stage start
|
|
||||||
uint8_t index_spawn_actual_{0}; // Next spawn to process
|
|
||||||
|
|
||||||
// Spawn generation
|
|
||||||
void generateSpawnEvents();
|
|
||||||
[[nodiscard]] auto selectRandomType() const -> EnemyType;
|
|
||||||
void spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos = nullptr);
|
|
||||||
void applyMultipliers(Enemy& enemy) const;
|
|
||||||
const Vec2* ship_position_{nullptr}; // [NEW] Non-owning pointer to ship position
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace StageSystem
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// stage_config.hpp - Estructures de dades per configuración de stages
|
// stage_config.hpp - Estructures de dades per configuració de stages
|
||||||
// © 2026 JailDesigner
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -7,69 +7,68 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "game/entities/enemy.hpp"
|
||||||
|
|
||||||
namespace StageSystem {
|
namespace StageSystem {
|
||||||
|
|
||||||
// Tipo de mode de spawn
|
// Multiplicadors de dificultat aplicats a tots els enemics del stage.
|
||||||
enum class ModeSpawn : std::uint8_t {
|
|
||||||
PROGRESSIVE, // Spawn progressiu con intervals
|
|
||||||
IMMEDIATE, // Todos los enemigos de cop
|
|
||||||
WAVE // Onades de 3-5 enemigos (futura extensió)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Configuración de spawn
|
|
||||||
struct ConfigSpawn {
|
|
||||||
ModeSpawn mode;
|
|
||||||
float delay_inicial; // Segons antes del primer spawn
|
|
||||||
float interval_spawn; // Segons entre spawns consecutius
|
|
||||||
};
|
|
||||||
|
|
||||||
// Distribució de type de enemigos (percentatges)
|
|
||||||
struct DistribucioEnemics {
|
|
||||||
uint8_t pentagon; // 0-100
|
|
||||||
uint8_t cuadrado; // 0-100
|
|
||||||
uint8_t molinillo; // 0-100
|
|
||||||
uint8_t star{0}; // 0-100 (opcional al YAML; default 0 per compat amb stages antics)
|
|
||||||
// Suma ha de ser 100, validat en StageLoader
|
|
||||||
};
|
|
||||||
|
|
||||||
// Multiplicadors de dificultat
|
|
||||||
struct MultiplicadorsDificultat {
|
struct MultiplicadorsDificultat {
|
||||||
float velocity; // 0.5-2.0 típic
|
float velocity{1.0F};
|
||||||
float rotation; // 0.5-2.0 típic
|
float rotation{1.0F};
|
||||||
float tracking_strength; // 0.0-1.5 (aplicat a Square)
|
float tracking_strength{0.0F};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Condició de transició a la següent onada.
|
||||||
|
// Pot ser una OR de "tots morts" i "timeout"; en falta tots dos cas, la
|
||||||
|
// wave no avança mai (invàlid: validat al loader).
|
||||||
|
struct WaveNext {
|
||||||
|
bool on_all_dead{false};
|
||||||
|
bool has_timeout{false};
|
||||||
|
float timeout{0.0F};
|
||||||
|
|
||||||
|
[[nodiscard]] auto isValid() const -> bool {
|
||||||
|
return on_all_dead || has_timeout;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Una onada: llista d'enemics a spawnejar i regla per passar a la següent.
|
||||||
|
struct WaveConfig {
|
||||||
|
std::vector<EnemyType> spawn; // Ordre i tipus dels spawns
|
||||||
|
float spawn_interval{0.0F}; // Segons entre spawns interns (0 = simultanis)
|
||||||
|
WaveNext next;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Metadades del file YAML
|
// Metadades del file YAML
|
||||||
struct MetadataStages {
|
struct MetadataStages {
|
||||||
std::string version;
|
std::string version;
|
||||||
uint8_t total_stages;
|
uint8_t total_stages{0};
|
||||||
std::string descripcio;
|
std::string descripcio;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Configuración completa de un stage
|
// Configuració completa d'un stage
|
||||||
struct StageConfig {
|
struct StageConfig {
|
||||||
uint8_t stage_id; // 1-10
|
uint8_t stage_id{0};
|
||||||
uint8_t total_enemies; // 1-200 (el cap simultani en pantalla el marca MAX_ORNIS)
|
|
||||||
ConfigSpawn config_spawn;
|
|
||||||
DistribucioEnemics distribucio;
|
|
||||||
MultiplicadorsDificultat multiplicadors;
|
MultiplicadorsDificultat multiplicadors;
|
||||||
|
std::vector<WaveConfig> waves;
|
||||||
|
|
||||||
// Validació
|
|
||||||
[[nodiscard]] auto isValid() const -> bool {
|
[[nodiscard]] auto isValid() const -> bool {
|
||||||
// stage_id es uint8_t: el rango superior (<=255) está garantizado por
|
if (stage_id == 0 || waves.empty()) {
|
||||||
// el tipo; basta con confirmar que no es 0 (sentinela "sin asignar").
|
return false;
|
||||||
return stage_id >= 1 &&
|
}
|
||||||
total_enemies > 0 && total_enemies <= 200 &&
|
for (const auto& w : waves) {
|
||||||
distribucio.pentagon + distribucio.cuadrado + distribucio.molinillo + distribucio.star == 100;
|
if (!w.next.isValid() || w.spawn.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Configuración completa del sistema (carregada desde YAML)
|
// Configuració completa del sistema (carregada des de YAML)
|
||||||
struct StageSystemConfig {
|
struct StageSystemConfig {
|
||||||
MetadataStages metadata;
|
MetadataStages metadata;
|
||||||
std::vector<StageConfig> stages; // Índex [0] = stage 1
|
std::vector<StageConfig> stages; // Índex [0] = stage 1
|
||||||
|
|
||||||
// Obtenir configuración de un stage específic
|
|
||||||
[[nodiscard]] auto findStage(uint8_t stage_id) const -> const StageConfig* {
|
[[nodiscard]] auto findStage(uint8_t stage_id) const -> const StageConfig* {
|
||||||
if (stage_id < 1 || stage_id > stages.size()) {
|
if (stage_id < 1 || stage_id > stages.size()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
// stage_loader.cpp - Implementació del carregador de configuración YAML
|
// stage_loader.cpp - Implementació del carregador de configuració YAML
|
||||||
// © 2026 JailDesigner
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#include "stage_loader.hpp"
|
#include "stage_loader.hpp"
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdlib>
|
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -15,6 +14,7 @@
|
|||||||
|
|
||||||
#include "core/resources/resource_helper.hpp"
|
#include "core/resources/resource_helper.hpp"
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "game/entities/enemy.hpp"
|
||||||
#include "stage_config.hpp"
|
#include "stage_config.hpp"
|
||||||
|
|
||||||
namespace StageSystem {
|
namespace StageSystem {
|
||||||
@@ -27,22 +27,18 @@ namespace StageSystem {
|
|||||||
normalized = normalized.substr(5);
|
normalized = normalized.substr(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load from resource system
|
|
||||||
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
|
std::vector<uint8_t> data = Resource::Helper::loadFile(normalized);
|
||||||
if (data.empty()) {
|
if (data.empty()) {
|
||||||
std::cerr << "[StageLoader] Error: no es pot load " << normalized << '\n';
|
std::cerr << "[StageLoader] Error: no es pot load " << normalized << '\n';
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to string
|
|
||||||
std::string yaml_content(data.begin(), data.end());
|
std::string yaml_content(data.begin(), data.end());
|
||||||
std::stringstream stream(yaml_content);
|
std::stringstream stream(yaml_content);
|
||||||
|
|
||||||
// Parse YAML
|
|
||||||
fkyaml::node yaml = fkyaml::node::deserialize(stream);
|
fkyaml::node yaml = fkyaml::node::deserialize(stream);
|
||||||
auto config = std::make_unique<StageSystemConfig>();
|
auto config = std::make_unique<StageSystemConfig>();
|
||||||
|
|
||||||
// Parse metadata
|
|
||||||
if (!yaml.contains("metadata")) {
|
if (!yaml.contains("metadata")) {
|
||||||
std::cerr << "[StageLoader] Error: falta camp 'metadata'" << '\n';
|
std::cerr << "[StageLoader] Error: falta camp 'metadata'" << '\n';
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -51,12 +47,10 @@ namespace StageSystem {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse stages
|
|
||||||
if (!yaml.contains("stages")) {
|
if (!yaml.contains("stages")) {
|
||||||
std::cerr << "[StageLoader] Error: falta camp 'stages'" << '\n';
|
std::cerr << "[StageLoader] Error: falta camp 'stages'" << '\n';
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!yaml["stages"].is_sequence()) {
|
if (!yaml["stages"].is_sequence()) {
|
||||||
std::cerr << "[StageLoader] Error: 'stages' ha de ser una list" << '\n';
|
std::cerr << "[StageLoader] Error: 'stages' ha de ser una list" << '\n';
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -67,10 +61,9 @@ namespace StageSystem {
|
|||||||
if (!parseStage(stage_yaml, stage)) {
|
if (!parseStage(stage_yaml, stage)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
config->stages.push_back(stage);
|
config->stages.push_back(std::move(stage));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validar configuración
|
|
||||||
if (!validateConfig(*config)) {
|
if (!validateConfig(*config)) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -107,29 +100,35 @@ namespace StageSystem {
|
|||||||
|
|
||||||
auto StageLoader::parseStage(const fkyaml::node& yaml, StageConfig& stage) -> bool {
|
auto StageLoader::parseStage(const fkyaml::node& yaml, StageConfig& stage) -> bool {
|
||||||
try {
|
try {
|
||||||
if (!yaml.contains("stage_id") || !yaml.contains("total_enemies") ||
|
if (!yaml.contains("stage_id") || !yaml.contains("waves")) {
|
||||||
!yaml.contains("spawn_config") || !yaml.contains("enemy_distribution") ||
|
std::cerr << "[StageLoader] Error: stage incompleta (cal stage_id i waves)" << '\n';
|
||||||
!yaml.contains("difficulty_multipliers")) {
|
|
||||||
std::cerr << "[StageLoader] Error: stage incompleta" << '\n';
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
stage.stage_id = yaml["stage_id"].get_value<uint8_t>();
|
stage.stage_id = yaml["stage_id"].get_value<uint8_t>();
|
||||||
stage.total_enemies = yaml["total_enemies"].get_value<uint8_t>();
|
|
||||||
|
|
||||||
if (!parseSpawnConfig(yaml["spawn_config"], stage.config_spawn)) {
|
// multipliers és opcional: si falta, queda als defaults (1.0/1.0/0.0).
|
||||||
|
if (yaml.contains("multipliers")) {
|
||||||
|
if (!parseMultipliers(yaml["multipliers"], stage.multiplicadors)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!yaml["waves"].is_sequence()) {
|
||||||
|
std::cerr << "[StageLoader] Error: 'waves' ha de ser una list" << '\n';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!parseDistribution(yaml["enemy_distribution"], stage.distribucio)) {
|
for (const auto& wave_yaml : yaml["waves"]) {
|
||||||
return false;
|
WaveConfig wave;
|
||||||
}
|
if (!parseWave(wave_yaml, wave)) {
|
||||||
if (!parseMultipliers(yaml["difficulty_multipliers"], stage.multiplicadors)) {
|
return false;
|
||||||
return false;
|
}
|
||||||
|
stage.waves.push_back(std::move(wave));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stage.isValid()) {
|
if (!stage.isValid()) {
|
||||||
std::cerr << "[StageLoader] Error: stage " << static_cast<int>(stage.stage_id)
|
std::cerr << "[StageLoader] Error: stage " << static_cast<int>(stage.stage_id)
|
||||||
<< " no es vàlid" << '\n';
|
<< " no és vàlid" << '\n';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,75 +139,26 @@ namespace StageSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto StageLoader::parseSpawnConfig(const fkyaml::node& yaml, ConfigSpawn& config) -> bool {
|
|
||||||
try {
|
|
||||||
if (!yaml.contains("mode") || !yaml.contains("initial_delay") ||
|
|
||||||
!yaml.contains("spawn_interval")) {
|
|
||||||
std::cerr << "[StageLoader] Error: spawn_config incompleta" << '\n';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto mode_str = yaml["mode"].get_value<std::string>();
|
|
||||||
config.mode = parseSpawnMode(mode_str);
|
|
||||||
config.delay_inicial = yaml["initial_delay"].get_value<float>();
|
|
||||||
config.interval_spawn = yaml["spawn_interval"].get_value<float>();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
std::cerr << "[StageLoader] Error parsing spawn_config: " << e.what() << '\n';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto StageLoader::parseDistribution(const fkyaml::node& yaml, DistribucioEnemics& dist) -> bool {
|
|
||||||
try {
|
|
||||||
if (!yaml.contains("pentagon") || !yaml.contains("cuadrado") ||
|
|
||||||
!yaml.contains("molinillo")) {
|
|
||||||
std::cerr << "[StageLoader] Error: enemy_distribution incompleta" << '\n';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
dist.pentagon = yaml["pentagon"].get_value<uint8_t>();
|
|
||||||
dist.cuadrado = yaml["cuadrado"].get_value<uint8_t>();
|
|
||||||
dist.molinillo = yaml["molinillo"].get_value<uint8_t>();
|
|
||||||
// 'star' és opcional per compatibilitat amb stages antics (default 0).
|
|
||||||
dist.star = yaml.contains("star") ? yaml["star"].get_value<uint8_t>() : 0;
|
|
||||||
|
|
||||||
// Validar que suma 100
|
|
||||||
int sum = dist.pentagon + dist.cuadrado + dist.molinillo + dist.star;
|
|
||||||
if (sum != 100) {
|
|
||||||
std::cerr << "[StageLoader] Error: distribució no suma 100 (suma=" << sum << ")" << '\n';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (const std::exception& e) {
|
|
||||||
std::cerr << "[StageLoader] Error parsing distribution: " << e.what() << '\n';
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto StageLoader::parseMultipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult) -> bool {
|
auto StageLoader::parseMultipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult) -> bool {
|
||||||
try {
|
try {
|
||||||
if (!yaml.contains("speed_multiplier") || !yaml.contains("rotation_multiplier") ||
|
if (yaml.contains("velocity")) {
|
||||||
!yaml.contains("tracking_strength")) {
|
mult.velocity = yaml["velocity"].get_value<float>();
|
||||||
std::cerr << "[StageLoader] Error: difficulty_multipliers incompleta" << '\n';
|
}
|
||||||
return false;
|
if (yaml.contains("rotation")) {
|
||||||
|
mult.rotation = yaml["rotation"].get_value<float>();
|
||||||
|
}
|
||||||
|
if (yaml.contains("tracking")) {
|
||||||
|
mult.tracking_strength = yaml["tracking"].get_value<float>();
|
||||||
}
|
}
|
||||||
|
|
||||||
mult.velocity = yaml["speed_multiplier"].get_value<float>();
|
|
||||||
mult.rotation = yaml["rotation_multiplier"].get_value<float>();
|
|
||||||
mult.tracking_strength = yaml["tracking_strength"].get_value<float>();
|
|
||||||
|
|
||||||
// Validar rangs raonables
|
|
||||||
if (mult.velocity < 0.1F || mult.velocity > 5.0F) {
|
if (mult.velocity < 0.1F || mult.velocity > 5.0F) {
|
||||||
std::cerr << "[StageLoader] Warning: speed_multiplier fuera de rang (0.1-5.0)" << '\n';
|
std::cerr << "[StageLoader] Warning: velocity fora de rang (0.1-5.0)" << '\n';
|
||||||
}
|
}
|
||||||
if (mult.rotation < 0.1F || mult.rotation > 5.0F) {
|
if (mult.rotation < 0.1F || mult.rotation > 5.0F) {
|
||||||
std::cerr << "[StageLoader] Warning: rotation_multiplier fuera de rang (0.1-5.0)" << '\n';
|
std::cerr << "[StageLoader] Warning: rotation fora de rang (0.1-5.0)" << '\n';
|
||||||
}
|
}
|
||||||
if (mult.tracking_strength < 0.0F || mult.tracking_strength > 2.0F) {
|
if (mult.tracking_strength < 0.0F || mult.tracking_strength > 2.0F) {
|
||||||
std::cerr << "[StageLoader] Warning: tracking_strength fuera de rang (0.0-2.0)" << '\n';
|
std::cerr << "[StageLoader] Warning: tracking fora de rang (0.0-2.0)" << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -218,34 +168,110 @@ namespace StageSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto StageLoader::parseSpawnMode(const std::string& mode_str) -> ModeSpawn {
|
auto StageLoader::parseWave(const fkyaml::node& yaml, WaveConfig& wave) -> bool {
|
||||||
if (mode_str == "progressive") {
|
try {
|
||||||
return ModeSpawn::PROGRESSIVE;
|
if (!yaml.contains("spawn") || !yaml.contains("next")) {
|
||||||
|
std::cerr << "[StageLoader] Error: wave sense 'spawn' o 'next'" << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!yaml["spawn"].is_sequence()) {
|
||||||
|
std::cerr << "[StageLoader] Error: 'spawn' ha de ser una list" << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const auto& type_node : yaml["spawn"]) {
|
||||||
|
auto type_str = type_node.get_value<std::string>();
|
||||||
|
EnemyType type{};
|
||||||
|
if (!parseEnemyType(type_str, type)) {
|
||||||
|
std::cerr << "[StageLoader] Error: tipus d'enemic desconegut '"
|
||||||
|
<< type_str << "'" << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
wave.spawn.push_back(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
wave.spawn_interval = yaml.contains("spawn_interval")
|
||||||
|
? yaml["spawn_interval"].get_value<float>()
|
||||||
|
: 0.0F;
|
||||||
|
|
||||||
|
if (!parseNext(yaml["next"], wave.next)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wave.next.isValid()) {
|
||||||
|
std::cerr << "[StageLoader] Error: wave 'next' sense condició (cal all_dead o timeout)" << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[StageLoader] Error parsing wave: " << e.what() << '\n';
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if (mode_str == "immediate") {
|
}
|
||||||
return ModeSpawn::IMMEDIATE;
|
|
||||||
|
auto StageLoader::parseEnemyType(const std::string& type_str, EnemyType& out) -> bool {
|
||||||
|
if (type_str == "pentagon") {
|
||||||
|
out = EnemyType::PENTAGON;
|
||||||
|
} else if (type_str == "cuadrado" || type_str == "square") {
|
||||||
|
out = EnemyType::SQUARE;
|
||||||
|
} else if (type_str == "molinillo" || type_str == "pinwheel") {
|
||||||
|
out = EnemyType::PINWHEEL;
|
||||||
|
} else if (type_str == "star") {
|
||||||
|
out = EnemyType::STAR;
|
||||||
|
} else if (type_str == "orb") {
|
||||||
|
out = EnemyType::ORB;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
if (mode_str == "wave") {
|
return true;
|
||||||
return ModeSpawn::WAVE;
|
}
|
||||||
|
|
||||||
|
auto StageLoader::parseNext(const fkyaml::node& yaml, WaveNext& next) -> bool {
|
||||||
|
try {
|
||||||
|
// Forma curta: scalar string ("all_dead" o "end").
|
||||||
|
if (yaml.is_string()) {
|
||||||
|
const auto S = yaml.get_value<std::string>();
|
||||||
|
if (S == "all_dead" || S == "end") {
|
||||||
|
next.on_all_dead = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::cerr << "[StageLoader] Error: 'next' string desconegut '" << S << "'" << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forma llarga: mapping amb claus opcionals.
|
||||||
|
if (yaml.is_mapping()) {
|
||||||
|
if (yaml.contains("all_dead")) {
|
||||||
|
next.on_all_dead = yaml["all_dead"].get_value<bool>();
|
||||||
|
}
|
||||||
|
if (yaml.contains("timeout")) {
|
||||||
|
next.has_timeout = true;
|
||||||
|
next.timeout = yaml["timeout"].get_value<float>();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cerr << "[StageLoader] Error: 'next' ha de ser string o mapping" << '\n';
|
||||||
|
return false;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "[StageLoader] Error parsing next: " << e.what() << '\n';
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
std::cerr << "[StageLoader] Warning: mode de spawn desconegut '" << mode_str
|
|
||||||
<< "', usant PROGRESSIVE" << '\n';
|
|
||||||
return ModeSpawn::PROGRESSIVE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto StageLoader::validateConfig(const StageSystemConfig& config) -> bool {
|
auto StageLoader::validateConfig(const StageSystemConfig& config) -> bool {
|
||||||
if (config.stages.empty()) {
|
if (config.stages.empty()) {
|
||||||
std::cerr << "[StageLoader] Error: sin stage carregat" << '\n';
|
std::cerr << "[StageLoader] Error: cap stage carregat" << '\n';
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.stages.size() != config.metadata.total_stages) {
|
if (config.stages.size() != config.metadata.total_stages) {
|
||||||
std::cerr << "[StageLoader] Warning: nombre de stages (" << config.stages.size()
|
std::cerr << "[StageLoader] Warning: nombre de stages (" << config.stages.size()
|
||||||
<< ") no coincideix con metadata.total_stages ("
|
<< ") no coincideix amb metadata.total_stages ("
|
||||||
<< static_cast<int>(config.metadata.total_stages) << ")" << '\n';
|
<< static_cast<int>(config.metadata.total_stages) << ")" << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validar stage_id consecutius
|
|
||||||
for (size_t i = 0; i < config.stages.size(); i++) {
|
for (size_t i = 0; i < config.stages.size(); i++) {
|
||||||
if (config.stages[i].stage_id != i + 1) {
|
if (config.stages[i].stage_id != i + 1) {
|
||||||
std::cerr << "[StageLoader] Error: stage_id no consecutius (esperat "
|
std::cerr << "[StageLoader] Error: stage_id no consecutius (esperat "
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// stage_loader.hpp - Carregador de configuración YAML
|
// stage_loader.hpp - Carregador de configuració YAML
|
||||||
// © 2026 JailDesigner
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
@@ -11,23 +11,21 @@
|
|||||||
|
|
||||||
namespace StageSystem {
|
namespace StageSystem {
|
||||||
|
|
||||||
class StageLoader {
|
class StageLoader {
|
||||||
public:
|
public:
|
||||||
// Carregar configuración desde file YAML
|
// Carregar configuració des de file YAML.
|
||||||
// Retorna nullptr si hay errors
|
// Retorna nullptr si hi ha errors.
|
||||||
static auto load(const std::string& path) -> std::unique_ptr<StageSystemConfig>;
|
static auto load(const std::string& path) -> std::unique_ptr<StageSystemConfig>;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Parsing helpers (implementats en .cpp)
|
|
||||||
static auto parseMetadata(const fkyaml::node& yaml, MetadataStages& meta) -> bool;
|
static auto parseMetadata(const fkyaml::node& yaml, MetadataStages& meta) -> bool;
|
||||||
static auto parseStage(const fkyaml::node& yaml, StageConfig& stage) -> bool;
|
static auto parseStage(const fkyaml::node& yaml, StageConfig& stage) -> bool;
|
||||||
static auto parseSpawnConfig(const fkyaml::node& yaml, ConfigSpawn& config) -> bool;
|
|
||||||
static auto parseDistribution(const fkyaml::node& yaml, DistribucioEnemics& dist) -> bool;
|
|
||||||
static auto parseMultipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult) -> bool;
|
static auto parseMultipliers(const fkyaml::node& yaml, MultiplicadorsDificultat& mult) -> bool;
|
||||||
static auto parseSpawnMode(const std::string& mode_str) -> ModeSpawn;
|
static auto parseWave(const fkyaml::node& yaml, WaveConfig& wave) -> bool;
|
||||||
|
static auto parseEnemyType(const std::string& type_str, EnemyType& out) -> bool;
|
||||||
|
static auto parseNext(const fkyaml::node& yaml, WaveNext& next) -> bool;
|
||||||
|
|
||||||
// Validació
|
|
||||||
static auto validateConfig(const StageSystemConfig& config) -> bool;
|
static auto validateConfig(const StageSystemConfig& config) -> bool;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace StageSystem
|
} // namespace StageSystem
|
||||||
|
|||||||
@@ -127,11 +127,10 @@ namespace StageSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void StageManager::processPlaying(float delta_time, bool pause_spawn) {
|
void StageManager::processPlaying(float delta_time, bool pause_spawn) {
|
||||||
// Update spawn controller (pauses when pause_spawn = true)
|
// No-op: el WaveRunner s'actualitza des de GameScene::runStagePlaying,
|
||||||
// Note: The actual enemy array update happens in GameScene::update()
|
// no des d'ací. La signatura es manté per simetria amb les altres process*.
|
||||||
// This is just for internal timekeeping
|
(void)delta_time;
|
||||||
(void)delta_time; // Spawn controller is updated externally
|
(void)pause_spawn;
|
||||||
(void)pause_spawn; // Passed to spawn_controller_.update() by GameScene
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void StageManager::processLevelCompleted(float delta_time) {
|
void StageManager::processLevelCompleted(float delta_time) {
|
||||||
@@ -162,12 +161,11 @@ namespace StageSystem {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure spawn controller
|
wave_runner_.configure(stage_config);
|
||||||
spawn_controller_.configure(stage_config);
|
wave_runner_.start();
|
||||||
spawn_controller_.start();
|
|
||||||
|
|
||||||
std::cout << "[StageManager] Carregat stage " << static_cast<int>(stage_id) << ": "
|
std::cout << "[StageManager] Carregat stage " << static_cast<int>(stage_id) << ": "
|
||||||
<< static_cast<int>(stage_config->total_enemies) << " enemigos" << '\n';
|
<< stage_config->waves.size() << " onades" << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace StageSystem
|
} // namespace StageSystem
|
||||||
|
|||||||
@@ -6,21 +6,21 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "spawn_controller.hpp"
|
|
||||||
#include "stage_config.hpp"
|
#include "stage_config.hpp"
|
||||||
|
#include "wave_runner.hpp"
|
||||||
|
|
||||||
namespace StageSystem {
|
namespace StageSystem {
|
||||||
|
|
||||||
// Estats del stage system
|
// Estats del stage system
|
||||||
enum class EstatStage : std::uint8_t {
|
enum class EstatStage : std::uint8_t {
|
||||||
INIT_HUD, // Animación inicial del HUD (3s)
|
INIT_HUD, // Animación inicial del HUD (3s)
|
||||||
LEVEL_START, // Pantalla "ENEMY INCOMING" (3s)
|
LEVEL_START, // Pantalla "ENEMY INCOMING" (3s)
|
||||||
PLAYING, // Gameplay normal
|
PLAYING, // Gameplay normal
|
||||||
LEVEL_COMPLETED // Pantalla "GOOD JOB COMMANDER!" (3s)
|
LEVEL_COMPLETED // Pantalla "GOOD JOB COMMANDER!" (3s)
|
||||||
};
|
};
|
||||||
|
|
||||||
class StageManager {
|
class StageManager {
|
||||||
public:
|
public:
|
||||||
explicit StageManager(const StageSystemConfig* config);
|
explicit StageManager(const StageSystemConfig* config);
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
@@ -28,7 +28,7 @@ class StageManager {
|
|||||||
void update(float delta_time, bool pause_spawn = false);
|
void update(float delta_time, bool pause_spawn = false);
|
||||||
|
|
||||||
// Stage progression
|
// Stage progression
|
||||||
void markStageCompleted(); // Call when all enemies destroyed
|
void markStageCompleted(); // Call when all enemies destroyed
|
||||||
[[nodiscard]] auto isGameComplete() const -> bool; // All 10 stages done?
|
[[nodiscard]] auto isGameComplete() const -> bool; // All 10 stages done?
|
||||||
|
|
||||||
// Current state queries
|
// Current state queries
|
||||||
@@ -38,17 +38,17 @@ class StageManager {
|
|||||||
[[nodiscard]] auto getTransitionTimer() const -> float { return timer_transicio_; }
|
[[nodiscard]] auto getTransitionTimer() const -> float { return timer_transicio_; }
|
||||||
[[nodiscard]] auto getLevelStartMessage() const -> const std::string& { return missatge_level_start_actual_; }
|
[[nodiscard]] auto getLevelStartMessage() const -> const std::string& { return missatge_level_start_actual_; }
|
||||||
|
|
||||||
// Spawn control (delegate to SpawnController)
|
// Wave execution (delegated)
|
||||||
auto getSpawnController() -> SpawnController& { return spawn_controller_; }
|
auto getWaveRunner() -> WaveRunner& { return wave_runner_; }
|
||||||
[[nodiscard]] auto getSpawnController() const -> const SpawnController& { return spawn_controller_; }
|
[[nodiscard]] auto getWaveRunner() const -> const WaveRunner& { return wave_runner_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const StageSystemConfig* config_; // Non-owning pointer
|
const StageSystemConfig* config_; // Non-owning pointer
|
||||||
SpawnController spawn_controller_;
|
WaveRunner wave_runner_;
|
||||||
|
|
||||||
EstatStage estat_{EstatStage::LEVEL_START};
|
EstatStage estat_{EstatStage::LEVEL_START};
|
||||||
uint8_t stage_actual_{1}; // 1-10
|
uint8_t stage_actual_{1}; // 1-10
|
||||||
float timer_transicio_{0.0F}; // Timer for LEVEL_START/LEVEL_COMPLETED (3.0s → 0.0s)
|
float timer_transicio_{0.0F}; // Timer for LEVEL_START/LEVEL_COMPLETED (3.0s → 0.0s)
|
||||||
std::string missatge_level_start_actual_; // Missatge seleccionat per al level actual
|
std::string missatge_level_start_actual_; // Missatge seleccionat per al level actual
|
||||||
|
|
||||||
// State transitions
|
// State transitions
|
||||||
@@ -59,6 +59,6 @@ class StageManager {
|
|||||||
static void processPlaying(float delta_time, bool pause_spawn);
|
static void processPlaying(float delta_time, bool pause_spawn);
|
||||||
void processLevelCompleted(float delta_time);
|
void processLevelCompleted(float delta_time);
|
||||||
void loadStage(uint8_t stage_id);
|
void loadStage(uint8_t stage_id);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace StageSystem
|
} // namespace StageSystem
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
// wave_runner.cpp - Implementació de l'executor d'onades
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "wave_runner.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "core/types.hpp"
|
||||||
|
#include "game/entities/enemy.hpp"
|
||||||
|
#include "stage_config.hpp"
|
||||||
|
|
||||||
|
namespace StageSystem {
|
||||||
|
|
||||||
|
void WaveRunner::configure(const StageConfig* config) {
|
||||||
|
config_ = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaveRunner::start() {
|
||||||
|
reset();
|
||||||
|
if ((config_ == nullptr) || config_->waves.empty()) {
|
||||||
|
std::cerr << "[WaveRunner] Error: config null o sense onades" << '\n';
|
||||||
|
all_waves_emitted_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::cout << "[WaveRunner] Stage " << static_cast<int>(config_->stage_id)
|
||||||
|
<< ": " << config_->waves.size() << " onades" << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaveRunner::reset() {
|
||||||
|
wave_index_ = 0;
|
||||||
|
wave_elapsed_ = 0.0F;
|
||||||
|
spawns_emitted_ = 0;
|
||||||
|
all_waves_emitted_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaveRunner::update(float delta_time, std::array<Enemy, 15>& orni_array, bool pausar) {
|
||||||
|
if (pausar || config_ == nullptr || all_waves_emitted_) {
|
||||||
|
// Si ja s'han emès totes, encara hem de poder avaluar stageComplete.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WaveConfig* wave = currentWave();
|
||||||
|
if (wave == nullptr) {
|
||||||
|
all_waves_emitted_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wave_elapsed_ += delta_time;
|
||||||
|
|
||||||
|
emitPendingSpawns(orni_array);
|
||||||
|
|
||||||
|
if (shouldAdvance(orni_array)) {
|
||||||
|
advanceWave();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto WaveRunner::stageComplete(const std::array<Enemy, 15>& orni_array) const -> bool {
|
||||||
|
return all_waves_emitted_ && getAliveEnemyCount(orni_array) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto WaveRunner::getAliveEnemyCount(const std::array<Enemy, 15>& orni_array) -> uint8_t {
|
||||||
|
uint8_t count = 0;
|
||||||
|
for (const auto& enemy : orni_array) {
|
||||||
|
if (enemy.isActive()) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto WaveRunner::currentWave() const -> const WaveConfig* {
|
||||||
|
if (config_ == nullptr || wave_index_ >= config_->waves.size()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
return &config_->waves[wave_index_];
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaveRunner::emitPendingSpawns(std::array<Enemy, 15>& orni_array) {
|
||||||
|
const WaveConfig* wave = currentWave();
|
||||||
|
if (wave == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn[i] toca a t = i * spawn_interval (i=0 → t=0).
|
||||||
|
while (spawns_emitted_ < wave->spawn.size()) {
|
||||||
|
const float SPAWN_T = static_cast<float>(spawns_emitted_) * wave->spawn_interval;
|
||||||
|
if (wave_elapsed_ < SPAWN_T) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Busca un slot lliure a l'arena. Si no n'hi ha, ho intentem el següent frame.
|
||||||
|
bool emitted = false;
|
||||||
|
for (auto& enemy : orni_array) {
|
||||||
|
if (!enemy.isActive()) {
|
||||||
|
spawnEnemy(enemy, wave->spawn[spawns_emitted_]);
|
||||||
|
emitted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!emitted) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
spawns_emitted_++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaveRunner::spawnEnemy(Enemy& enemy, EnemyType type) {
|
||||||
|
enemy.init(type, ship_position_);
|
||||||
|
applyMultipliers(enemy);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaveRunner::applyMultipliers(Enemy& enemy) const {
|
||||||
|
if (config_ == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
enemy.setVelocity(enemy.getBaseVelocity() * config_->multiplicadors.velocity);
|
||||||
|
enemy.setRotation(enemy.getBaseRotation() * config_->multiplicadors.rotation);
|
||||||
|
enemy.setTrackingStrength(config_->multiplicadors.tracking_strength);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto WaveRunner::shouldAdvance(const std::array<Enemy, 15>& orni_array) const -> bool {
|
||||||
|
const WaveConfig* wave = currentWave();
|
||||||
|
if (wave == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Una wave només pot avançar després d'haver emès tots els seus spawns.
|
||||||
|
const bool ALL_SPAWNED = spawns_emitted_ >= wave->spawn.size();
|
||||||
|
|
||||||
|
if (wave->next.has_timeout && wave_elapsed_ >= wave->next.timeout) {
|
||||||
|
// El timeout NO requereix all_spawned: si la wave triga més que el seu
|
||||||
|
// propi timeout (cas patològic amb spawn_interval massa gran), forcem
|
||||||
|
// avançar igualment per no encallar-nos.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wave->next.on_all_dead) {
|
||||||
|
return ALL_SPAWNED && getAliveEnemyCount(orni_array) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WaveRunner::advanceWave() {
|
||||||
|
wave_index_++;
|
||||||
|
wave_elapsed_ = 0.0F;
|
||||||
|
spawns_emitted_ = 0;
|
||||||
|
|
||||||
|
if (config_ == nullptr || wave_index_ >= config_->waves.size()) {
|
||||||
|
all_waves_emitted_ = true;
|
||||||
|
std::cout << "[WaveRunner] Totes les onades emeses" << '\n';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::cout << "[WaveRunner] Avança a onada " << static_cast<int>(wave_index_ + 1)
|
||||||
|
<< "/" << config_->waves.size() << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace StageSystem
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// wave_runner.hpp - Executor d'onades per a un stage
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "core/types.hpp"
|
||||||
|
#include "game/entities/enemy.hpp"
|
||||||
|
#include "stage_config.hpp"
|
||||||
|
|
||||||
|
namespace StageSystem {
|
||||||
|
|
||||||
|
// Estat intern de la wave actual: quants spawns ja emesos i temps elapsed.
|
||||||
|
// Una sola wave "viva" alhora a nivell d'emissió, però els enemics
|
||||||
|
// d'onades anteriors compten per al `all_dead` (model "arena": tot el que
|
||||||
|
// hi ha viu compta).
|
||||||
|
class WaveRunner {
|
||||||
|
public:
|
||||||
|
WaveRunner() = default;
|
||||||
|
|
||||||
|
void configure(const StageConfig* config);
|
||||||
|
void start();
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
// Update per frame. orni_array és l'arena real (max 15 simultanis).
|
||||||
|
void update(float delta_time, std::array<Enemy, 15>& orni_array, bool pausar = false);
|
||||||
|
|
||||||
|
// Stage acabat: totes les waves emeses i arena buida.
|
||||||
|
[[nodiscard]] auto stageComplete(const std::array<Enemy, 15>& orni_array) const -> bool;
|
||||||
|
|
||||||
|
void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; }
|
||||||
|
|
||||||
|
// Comptatge d'enemics vius (utilitat compartida; estàtica per no acoblar).
|
||||||
|
[[nodiscard]] static auto getAliveEnemyCount(const std::array<Enemy, 15>& orni_array) -> uint8_t;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const StageConfig* config_{nullptr};
|
||||||
|
const Vec2* ship_position_{nullptr};
|
||||||
|
|
||||||
|
uint8_t wave_index_{0}; // Índex de la wave actual dins config_->waves
|
||||||
|
float wave_elapsed_{0.0F}; // Segons des de l'inici de la wave actual
|
||||||
|
uint8_t spawns_emitted_{0}; // Spawns ja col·locats a l'arena d'aquesta wave
|
||||||
|
bool all_waves_emitted_{false}; // Ja no queden waves per emetre
|
||||||
|
|
||||||
|
[[nodiscard]] auto currentWave() const -> const WaveConfig*;
|
||||||
|
void emitPendingSpawns(std::array<Enemy, 15>& orni_array);
|
||||||
|
void spawnEnemy(Enemy& enemy, EnemyType type);
|
||||||
|
void applyMultipliers(Enemy& enemy) const;
|
||||||
|
[[nodiscard]] auto shouldAdvance(const std::array<Enemy, 15>& orni_array) const -> bool;
|
||||||
|
void advanceWave();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace StageSystem
|
||||||
@@ -15,7 +15,7 @@ namespace Systems::Collision {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Trenca una bala en debris (8 fragments de l'octàgon) + so HIT + desactiva.
|
// Trenca una bala en debris (8 fragments de l'octàgon) + so BULLET_ZAP + desactiva.
|
||||||
// S'invoca des de qualsevol desactivació de bala (impacte amb enemic, amb jugador,
|
// S'invoca des de qualsevol desactivació de bala (impacte amb enemic, amb jugador,
|
||||||
// o sortida del PLAYAREA) per a un feedback visual i sonor consistent.
|
// o sortida del PLAYAREA) per a un feedback visual i sonor consistent.
|
||||||
void breakBullet(Effects::DebrisManager& debris_manager, Bullet& bullet) {
|
void breakBullet(Effects::DebrisManager& debris_manager, Bullet& bullet) {
|
||||||
@@ -30,7 +30,7 @@ namespace Systems::Collision {
|
|||||||
Vec2{}, // sense herència de velocitat (fragments radials)
|
Vec2{}, // sense herència de velocitat (fragments radials)
|
||||||
0.0F, // sense velocity angular heretada
|
0.0F, // sense velocity angular heretada
|
||||||
0.0F, // sense rotació visual heretada
|
0.0F, // sense rotació visual heretada
|
||||||
Defaults::Sound::HIT,
|
Defaults::Sound::BULLET_ZAP,
|
||||||
bullet.getConfig().colors.normal,
|
bullet.getConfig().colors.normal,
|
||||||
Defaults::Physics::Debris::TEMPS_VIDA,
|
Defaults::Physics::Debris::TEMPS_VIDA,
|
||||||
Defaults::Physics::Debris::ACCELERACIO,
|
Defaults::Physics::Debris::ACCELERACIO,
|
||||||
@@ -41,20 +41,32 @@ namespace Systems::Collision {
|
|||||||
|
|
||||||
void detectBulletEnemy(Context& ctx) {
|
void detectBulletEnemy(Context& ctx) {
|
||||||
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER;
|
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER;
|
||||||
|
constexpr uint8_t NO_SHOOTER = 0xFF;
|
||||||
|
constexpr uint8_t ENEMY_BASE = Defaults::Entities::ENEMY_OWNER_BASE;
|
||||||
|
|
||||||
for (auto& bullet : ctx.bullets) {
|
for (auto& bullet : ctx.bullets) {
|
||||||
if (!bullet.isActive()) {
|
if (!bullet.isActive()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (auto& enemy : ctx.enemies) {
|
const uint8_t BULLET_OWNER = bullet.getOwnerId();
|
||||||
|
const bool IS_ENEMY_BULLET = BULLET_OWNER >= ENEMY_BASE;
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < ctx.enemies.size(); ++i) {
|
||||||
|
Enemy& enemy = ctx.enemies[i];
|
||||||
|
// Self-shot: una bala d'enemic mai no impacta el seu propi creador.
|
||||||
|
if (IS_ENEMY_BULLET && BULLET_OWNER == static_cast<uint8_t>(ENEMY_BASE + i)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), bullet.getCollisionRadius(), enemy, AMPLIFIER)) {
|
if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), bullet.getCollisionRadius(), enemy, AMPLIFIER)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// *** COLISIÓN bullet → enemy ***
|
// *** COLISIÓN bullet → enemy ***
|
||||||
// La cadena d'efectes (impulse, hurt, destroy, debris, score...) viu
|
// La cadena d'efectes (impulse, hurt, destroy, debris, score...) viu
|
||||||
// al YAML de l'enemic via la secció `events:`. Aquí només dispatchem.
|
// al YAML de l'enemic via la secció `events:`. Si la bala és d'enemic,
|
||||||
Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HIT, bullet.getOwnerId(), &bullet);
|
// no atribuïm el kill a cap player (NO_SHOOTER).
|
||||||
|
const uint8_t SHOOTER = IS_ENEMY_BULLET ? NO_SHOOTER : BULLET_OWNER;
|
||||||
|
Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HIT, SHOOTER, &bullet);
|
||||||
breakBullet(ctx.debris_manager, bullet);
|
breakBullet(ctx.debris_manager, bullet);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -71,36 +83,6 @@ namespace Systems::Collision {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void detectWoundedChain(Context& ctx) {
|
|
||||||
const std::size_t N = ctx.enemies.size();
|
|
||||||
for (std::size_t i = 0; i < N; i++) {
|
|
||||||
Enemy& a = ctx.enemies[i];
|
|
||||||
if (!a.isCollidable()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (std::size_t j = i + 1; j < N; j++) {
|
|
||||||
Enemy& b = ctx.enemies[j];
|
|
||||||
if (!b.isCollidable()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const bool A_WOUNDED = a.isWounded();
|
|
||||||
const bool B_WOUNDED = b.isWounded();
|
|
||||||
if (A_WOUNDED == B_WOUNDED) {
|
|
||||||
continue; // ambos sanos o ambos heridos: nada que propagar
|
|
||||||
}
|
|
||||||
if (!Physics::checkCollision(a, b, Defaults::Game::COLLISION_WOUNDED_CHAIN_AMPLIFIER)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// El sano queda herido, propagando el shooter original.
|
|
||||||
if (A_WOUNDED) {
|
|
||||||
b.hurt(a.getLastHitBy());
|
|
||||||
} else {
|
|
||||||
a.hurt(b.getLastHitBy());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void detectShipEnemy(Context& ctx) {
|
void detectShipEnemy(Context& ctx) {
|
||||||
constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER;
|
constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER;
|
||||||
|
|
||||||
@@ -138,7 +120,9 @@ namespace Systems::Collision {
|
|||||||
const float DEATH_FACTOR = ctx.ships[i].getConfig().physics.death_impact_factor;
|
const float DEATH_FACTOR = ctx.ships[i].getConfig().physics.death_impact_factor;
|
||||||
const Vec2 IMPULSE = SHIP_VEL * (ctx.ships[i].getBody().mass * DEATH_FACTOR);
|
const Vec2 IMPULSE = SHIP_VEL * (ctx.ships[i].getBody().mass * DEATH_FACTOR);
|
||||||
touched_enemy->applyImpulse(IMPULSE);
|
touched_enemy->applyImpulse(IMPULSE);
|
||||||
ctx.on_player_hit(i);
|
// Sense bala: cap impuls de bala per als debris (mort per
|
||||||
|
// col·lisió cos-cos). Els debris hereten la inèrcia del ship.
|
||||||
|
ctx.on_player_hit(i, Vec2{});
|
||||||
} else {
|
} else {
|
||||||
// Primer impacte → estat HURT (rebot físic ja resolt per PhysicsWorld;
|
// Primer impacte → estat HURT (rebot físic ja resolt per PhysicsWorld;
|
||||||
// l'enemic no rep dany per decisió de disseny).
|
// l'enemic no rep dany per decisió de disseny).
|
||||||
@@ -185,13 +169,12 @@ namespace Systems::Collision {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// *** TEAMMATE HIT (friendly fire) ***
|
// *** TEAMMATE HIT (friendly fire) ***
|
||||||
// Víctima perd 1 vida, atacant en guanya 1. Apliquem l'impuls
|
// Víctima perd 1 vida, atacant en guanya 1. Friendly fire sempre
|
||||||
// de la bala a la nau ABANS de on_player_hit perquè tocado()
|
// mata: el bullet va als debris (via tocado) i NO al cos del ship
|
||||||
// captura la velocitat per als debris (si no, queden quiets).
|
// — el cos està a punt de desactivar-se, qualsevol impuls seria
|
||||||
const Vec2 BULLET_IMPULSE = bullet.getBody().velocity *
|
// double-count amb la velocitat que ja reben els trossos.
|
||||||
(bullet.getBody().mass * bullet.getConfig().physics.impact_momentum_factor);
|
const Vec2 BULLET_VEL = bullet.getBody().velocity;
|
||||||
ctx.ships[player_id].getBody().applyImpulse(BULLET_IMPULSE);
|
ctx.on_player_hit(player_id, BULLET_VEL);
|
||||||
ctx.on_player_hit(player_id);
|
|
||||||
ctx.lives_per_player[BULLET_OWNER]++;
|
ctx.lives_per_player[BULLET_OWNER]++;
|
||||||
Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME);
|
Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME);
|
||||||
breakBullet(ctx.debris_manager, bullet);
|
breakBullet(ctx.debris_manager, bullet);
|
||||||
@@ -200,16 +183,66 @@ namespace Systems::Collision {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void detectEnemyBulletShip(Context& ctx) {
|
||||||
|
constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER;
|
||||||
|
constexpr uint8_t ENEMY_BASE = Defaults::Entities::ENEMY_OWNER_BASE;
|
||||||
|
|
||||||
|
for (auto& bullet : ctx.bullets) {
|
||||||
|
if (!bullet.isActive() || bullet.getOwnerId() < ENEMY_BASE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (uint8_t player_id = 0; player_id < 2; player_id++) {
|
||||||
|
if (ctx.hit_timer_per_player[player_id] > 0.0F ||
|
||||||
|
!ctx.ships[player_id].isActive() ||
|
||||||
|
ctx.ships[player_id].isInvulnerable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const bool JUGADOR_ACTIU = (player_id == 0)
|
||||||
|
? ctx.match_config.player1_active
|
||||||
|
: ctx.match_config.player2_active;
|
||||||
|
if (!JUGADOR_ACTIU) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), bullet.getCollisionRadius(), ctx.ships[player_id], AMPLIFIER)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// *** BALA D'ENEMIC → SHIP ***
|
||||||
|
// Regla "cos XOR trossos": l'impuls de la bala s'aplica al cos
|
||||||
|
// només si el ship sobreviu (fereix). Si el ship mor, el bullet
|
||||||
|
// va directament als trossos (via tocado) i el cos no rep impuls
|
||||||
|
// — els trossos ja porten la força de la bala, qualsevol impuls
|
||||||
|
// afegit al cos seria double-count.
|
||||||
|
const Vec2 BULLET_VEL = bullet.getBody().velocity;
|
||||||
|
if (ctx.ships[player_id].isHurt()) {
|
||||||
|
// Segon impacte durant HURT → mort.
|
||||||
|
ctx.on_player_hit(player_id, BULLET_VEL);
|
||||||
|
} else {
|
||||||
|
// Fereix: el cos sobreviu, rep l'impuls. No hi ha debris encara.
|
||||||
|
const Vec2 IMPULSE = BULLET_VEL *
|
||||||
|
(bullet.getBody().mass * bullet.getConfig().physics.impact_momentum_factor);
|
||||||
|
ctx.ships[player_id].getBody().applyImpulse(IMPULSE);
|
||||||
|
ctx.ships[player_id].hurt();
|
||||||
|
}
|
||||||
|
breakBullet(ctx.debris_manager, bullet);
|
||||||
|
break; // una bala impacta una vegada per frame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void detectAll(Context& ctx) {
|
void detectAll(Context& ctx) {
|
||||||
processWoundedDeaths(ctx); // expiran ANTES de ser tocadas por bala este frame
|
processWoundedDeaths(ctx); // expiran ANTES de ser tocadas por bala este frame
|
||||||
detectBulletEnemy(ctx);
|
detectBulletEnemy(ctx);
|
||||||
detectWoundedChain(ctx); // un herit pot ferir a un sa al fregar-lo
|
// Wounded chain desactivat: era massa fàcil que un enemic ferit topés
|
||||||
|
// amb l'orb (10 HP) i el matés instantàniament. La regla
|
||||||
|
// "ferit-toca-sa → ferit" queda permanentment fora.
|
||||||
detectShipEnemy(ctx);
|
detectShipEnemy(ctx);
|
||||||
detectBulletPlayer(ctx);
|
detectBulletPlayer(ctx);
|
||||||
|
detectEnemyBulletShip(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
void desactivateOutOfBoundsBullets(
|
void desactivateOutOfBoundsBullets(
|
||||||
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS) * 2>& bullets,
|
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets,
|
||||||
Effects::DebrisManager& debris_manager) {
|
Effects::DebrisManager& debris_manager) {
|
||||||
float min_x;
|
float min_x;
|
||||||
float max_x;
|
float max_x;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ namespace Systems::Collision {
|
|||||||
struct Context {
|
struct Context {
|
||||||
std::array<Ship, 2>& ships;
|
std::array<Ship, 2>& ships;
|
||||||
std::array<Enemy, Defaults::Entities::MAX_ORNIS>& enemies;
|
std::array<Enemy, Defaults::Entities::MAX_ORNIS>& enemies;
|
||||||
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS) * 2>& bullets;
|
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets;
|
||||||
std::array<float, 2>& hit_timer_per_player;
|
std::array<float, 2>& hit_timer_per_player;
|
||||||
std::array<int, 2>& score_per_player;
|
std::array<int, 2>& score_per_player;
|
||||||
std::array<int, 2>& lives_per_player;
|
std::array<int, 2>& lives_per_player;
|
||||||
@@ -40,8 +40,11 @@ namespace Systems::Collision {
|
|||||||
Effects::FireworkManager& firework_manager;
|
Effects::FireworkManager& firework_manager;
|
||||||
Effects::FloatingScoreManager& floating_score_manager;
|
Effects::FloatingScoreManager& floating_score_manager;
|
||||||
const GameConfig::MatchConfig& match_config;
|
const GameConfig::MatchConfig& match_config;
|
||||||
// Trigger de muerte del jugador (GameScene::tocado).
|
// Trigger de muerte del jugador (GameScene::tocado). bullet_velocity es
|
||||||
std::function<void(uint8_t /*player_id*/)> on_player_hit;
|
// la velocitat de la bala que ha causat la mort (Vec2{} si la mort no
|
||||||
|
// ve d'una bala — col·lisió ship-enemy, etc.). Es passa al debris perquè
|
||||||
|
// els trossos volin en direcció de la bala.
|
||||||
|
std::function<void(uint8_t /*player_id*/, const Vec2& /*bullet_velocity*/)> on_player_hit;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Detecta colisiones bullet → enemy. Si hit:
|
// Detecta colisiones bullet → enemy. Si hit:
|
||||||
@@ -53,12 +56,6 @@ namespace Systems::Collision {
|
|||||||
// al `last_hit_by_` del enemy (si está set).
|
// al `last_hit_by_` del enemy (si está set).
|
||||||
void processWoundedDeaths(Context& ctx);
|
void processWoundedDeaths(Context& ctx);
|
||||||
|
|
||||||
// Si un enemy herido colisiona con uno sano (ni herido ni invulnerable),
|
|
||||||
// el sano también queda herido (efecto cadena). Propaga `last_hit_by_` para
|
|
||||||
// que el shooter original siga acreditándose la muerte en cascada. El rebote
|
|
||||||
// físico ya lo resuelve PhysicsWorld; aquí solo propagamos el estado.
|
|
||||||
void detectWoundedChain(Context& ctx);
|
|
||||||
|
|
||||||
// Detecta colisiones ship → enemy. Si hit, llama on_player_hit(player_id).
|
// Detecta colisiones ship → enemy. Si hit, llama on_player_hit(player_id).
|
||||||
void detectShipEnemy(Context& ctx);
|
void detectShipEnemy(Context& ctx);
|
||||||
|
|
||||||
@@ -67,6 +64,10 @@ namespace Systems::Collision {
|
|||||||
// atacante gana 1. En ambos casos, llama on_player_hit y desactiva bullet.
|
// atacante gana 1. En ambos casos, llama on_player_hit y desactiva bullet.
|
||||||
void detectBulletPlayer(Context& ctx);
|
void detectBulletPlayer(Context& ctx);
|
||||||
|
|
||||||
|
// Bales d'enemic (owner_id ≥ ENEMY_OWNER_BASE) impactant un ship: aplica
|
||||||
|
// impulse, primer hit → hurt(), segon hit durant HURT → on_player_hit.
|
||||||
|
void detectEnemyBulletShip(Context& ctx);
|
||||||
|
|
||||||
// Las tres en orden lógico del frame.
|
// Las tres en orden lógico del frame.
|
||||||
void detectAll(Context& ctx);
|
void detectAll(Context& ctx);
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ namespace Systems::Collision {
|
|||||||
// (8 fragments de l'octàgon) i el so HIT. Cal cridar-la després de detectAll()
|
// (8 fragments de l'octàgon) i el so HIT. Cal cridar-la després de detectAll()
|
||||||
// perquè una bala que el mateix frame xoca i alhora surt es comptabilitzi com a impacte.
|
// perquè una bala que el mateix frame xoca i alhora surt es comptabilitzi com a impacte.
|
||||||
void desactivateOutOfBoundsBullets(
|
void desactivateOutOfBoundsBullets(
|
||||||
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS) * 2>& bullets,
|
std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets,
|
||||||
Effects::DebrisManager& debris_manager);
|
Effects::DebrisManager& debris_manager);
|
||||||
|
|
||||||
} // namespace Systems::Collision
|
} // namespace Systems::Collision
|
||||||
|
|||||||
@@ -0,0 +1,275 @@
|
|||||||
|
// enemy_ai_system.cpp - Implementació del dispatcher de moviment d'enemics
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
|
||||||
|
#include "game/systems/enemy_ai_system.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#include "core/defaults.hpp"
|
||||||
|
#include "core/types.hpp"
|
||||||
|
#include "game/constants.hpp"
|
||||||
|
#include "game/entities/bullet.hpp"
|
||||||
|
#include "game/entities/bullet_config.hpp"
|
||||||
|
#include "game/entities/bullet_registry.hpp"
|
||||||
|
#include "game/entities/enemy.hpp"
|
||||||
|
#include "game/entities/enemy_ai.hpp"
|
||||||
|
#include "game/entities/enemy_config.hpp"
|
||||||
|
#include "game/entities/ship.hpp"
|
||||||
|
|
||||||
|
namespace Systems::EnemyAi {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto randFloat01() -> float {
|
||||||
|
return static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto velocityToAngle(const Vec2& velocity) -> float {
|
||||||
|
if (velocity.lengthSquared() < 0.0001F) {
|
||||||
|
return 0.0F;
|
||||||
|
}
|
||||||
|
return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retorna el centre del ship actiu més proper a l'enemic, o nullptr si
|
||||||
|
// no n'hi ha cap viu. Els ships destruïts (is_hit_) i els slots nullptr
|
||||||
|
// (player no participant al match) queden filtrats.
|
||||||
|
auto findNearestShipPosition(const Enemy& enemy) -> const Vec2* {
|
||||||
|
const Vec2& self = enemy.getCenter();
|
||||||
|
const Vec2* best = nullptr;
|
||||||
|
float best_dist_sq = 0.0F;
|
||||||
|
for (const Ship* ship : enemy.getShips()) {
|
||||||
|
if (ship == nullptr || !ship->isActive()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const Vec2& pos = ship->getCenter();
|
||||||
|
const Vec2 DELTA = pos - self;
|
||||||
|
const float DIST_SQ = DELTA.lengthSquared();
|
||||||
|
if (best == nullptr || DIST_SQ < best_dist_sq) {
|
||||||
|
best = &pos;
|
||||||
|
best_dist_sq = DIST_SQ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZIGZAG: canvi de direcció probabilístic. Còpia literal del legacy
|
||||||
|
// Enemy::behaviorPentagon.
|
||||||
|
void moveZigzag(Enemy& enemy, float delta_time) {
|
||||||
|
const auto& mv = enemy.getConfig().ai.movement;
|
||||||
|
EnemyAiState& state = enemy.getAiState();
|
||||||
|
state.direction_change_timer += delta_time;
|
||||||
|
|
||||||
|
if (randFloat01() < mv.zigzag_prob_per_second * delta_time) {
|
||||||
|
const Vec2 VEL = enemy.getBody().velocity;
|
||||||
|
const float CURRENT_ANGLE = velocityToAngle(VEL);
|
||||||
|
const float DELTA = randFloat01() * mv.angle_change_max;
|
||||||
|
const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA);
|
||||||
|
const float SPEED = VEL.length();
|
||||||
|
enemy.setVelocityFromAngle(NEW_ANGLE, SPEED);
|
||||||
|
state.direction_change_timer = 0.0F;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TRACKING: cada N segons, interpola la velocitat actual cap a la
|
||||||
|
// direcció del ship mantenint la mateixa magnitud. Còpia literal del
|
||||||
|
// legacy Enemy::behaviorSquare.
|
||||||
|
void moveTracking(Enemy& enemy, float delta_time) {
|
||||||
|
const auto& mv = enemy.getConfig().ai.movement;
|
||||||
|
EnemyAiState& state = enemy.getAiState();
|
||||||
|
state.tracking_timer += delta_time;
|
||||||
|
|
||||||
|
const Vec2* ship_pos = findNearestShipPosition(enemy);
|
||||||
|
if (state.tracking_timer < mv.tracking_interval || ship_pos == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.tracking_timer = 0.0F;
|
||||||
|
|
||||||
|
const Vec2 TO_SHIP = *ship_pos - enemy.getCenter();
|
||||||
|
const float DIST = TO_SHIP.length();
|
||||||
|
if (DIST <= 0.0F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Vec2 DESIRED_DIR = TO_SHIP / DIST;
|
||||||
|
const float SPEED = enemy.getBody().velocity.length();
|
||||||
|
const Vec2 DESIRED_VEL = DESIRED_DIR * SPEED;
|
||||||
|
const float STRENGTH = state.tracking_strength;
|
||||||
|
|
||||||
|
Vec2 new_vel = (enemy.getBody().velocity * (1.0F - STRENGTH)) +
|
||||||
|
(DESIRED_VEL * STRENGTH);
|
||||||
|
|
||||||
|
const float NEW_SPEED = new_vel.length();
|
||||||
|
if (NEW_SPEED > 0.0F) {
|
||||||
|
new_vel = new_vel * (SPEED / NEW_SPEED);
|
||||||
|
}
|
||||||
|
enemy.getBody().velocity = new_vel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHASE / FLEE comparteixen lògica: steering continu cap a (o lluny de)
|
||||||
|
// la direcció ideal, preservant la magnitud de velocitat. La força és
|
||||||
|
// strength * dt clampejada a 1 (LERP frame-independent simple).
|
||||||
|
void steerTowards(Enemy& enemy, const Vec2& desired_dir, float strength, float delta_time) {
|
||||||
|
const float SPEED = enemy.getBody().velocity.length();
|
||||||
|
if (SPEED <= 0.0F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Vec2 DESIRED_VEL = desired_dir * SPEED;
|
||||||
|
const float T = std::min(1.0F, strength * delta_time);
|
||||||
|
Vec2 new_vel = (enemy.getBody().velocity * (1.0F - T)) + (DESIRED_VEL * T);
|
||||||
|
const float NEW_SPEED = new_vel.length();
|
||||||
|
if (NEW_SPEED > 0.0F) {
|
||||||
|
new_vel = new_vel * (SPEED / NEW_SPEED);
|
||||||
|
}
|
||||||
|
enemy.getBody().velocity = new_vel;
|
||||||
|
}
|
||||||
|
|
||||||
|
void moveChase(Enemy& enemy, float delta_time) {
|
||||||
|
const Vec2* ship_pos = findNearestShipPosition(enemy);
|
||||||
|
if (ship_pos == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Vec2 TO_SHIP = *ship_pos - enemy.getCenter();
|
||||||
|
const float DIST = TO_SHIP.length();
|
||||||
|
if (DIST <= 0.0F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
steerTowards(enemy, TO_SHIP / DIST, enemy.getConfig().ai.movement.chase_strength, delta_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
void moveFlee(Enemy& enemy, float delta_time) {
|
||||||
|
const Vec2* ship_pos = findNearestShipPosition(enemy);
|
||||||
|
if (ship_pos == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Vec2 AWAY = enemy.getCenter() - *ship_pos;
|
||||||
|
const float DIST = AWAY.length();
|
||||||
|
if (DIST <= 0.0F) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
steerTowards(enemy, AWAY / DIST, enemy.getConfig().ai.movement.flee_strength, delta_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// RECTILINEAR_PROXIMITY: rectilini (cap modificació a velocity); boost
|
||||||
|
// de rotació visual quan distància al ship < proximity_distance. Còpia
|
||||||
|
// literal del legacy Enemy::behaviorPinwheel.
|
||||||
|
void moveRectilinearProximity(Enemy& enemy, float /*delta_time*/) {
|
||||||
|
const auto& mv = enemy.getConfig().ai.movement;
|
||||||
|
const Vec2* ship_pos = findNearestShipPosition(enemy);
|
||||||
|
if (ship_pos == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Vec2 TO_SHIP = *ship_pos - enemy.getCenter();
|
||||||
|
const float DIST = TO_SHIP.length();
|
||||||
|
const float BASE = enemy.getRotationBase();
|
||||||
|
if (DIST < mv.proximity_distance) {
|
||||||
|
enemy.setRotationDelta(BASE * mv.rotation_proximity_multiplier);
|
||||||
|
} else {
|
||||||
|
enemy.setRotationDelta(BASE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SHOOT: cerca slot lliure a ctx.bullets i el dispara amb el bullet config
|
||||||
|
// referenciat per nom (lazy-load via BulletRegistry). Angle segons aim_mode +
|
||||||
|
// jitter. owner_id = ENEMY_OWNER_BASE + enemy_index per al filtre d'autoimpacte.
|
||||||
|
void doShoot(Systems::Collision::Context& ctx, const Enemy& enemy, std::size_t enemy_index, const AiTickAction& action) {
|
||||||
|
if (action.bullet_config_name.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const BulletConfig* cfg = BulletRegistry::get(action.bullet_config_name);
|
||||||
|
if (cfg == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Cerca slot dins la zona reservada per a enemics: així no robem
|
||||||
|
// slots als pools de player (que iteren [0, MAX_BULLETS) i [MAX_BULLETS, 2*MAX_BULLETS)).
|
||||||
|
Bullet* slot = nullptr;
|
||||||
|
constexpr std::size_t START = Defaults::Entities::ENEMY_BULLET_START_IDX;
|
||||||
|
constexpr std::size_t END = START + Defaults::Entities::MAX_ENEMY_BULLETS;
|
||||||
|
for (std::size_t i = START; i < END; ++i) {
|
||||||
|
if (!ctx.bullets[i].isActive()) {
|
||||||
|
slot = &ctx.bullets[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (slot == nullptr) {
|
||||||
|
return; // pool d'enemic ple
|
||||||
|
}
|
||||||
|
|
||||||
|
float angle = 0.0F;
|
||||||
|
if (action.aim_mode == AimMode::AIMED) {
|
||||||
|
const Vec2* target = findNearestShipPosition(enemy);
|
||||||
|
if (target == nullptr) {
|
||||||
|
// Sense ship viu: degrada a random per no congelar el dispar.
|
||||||
|
angle = randFloat01() * 2.0F * Constants::PI;
|
||||||
|
} else {
|
||||||
|
const Vec2 TO = *target - enemy.getCenter();
|
||||||
|
// angle=0 apunta amunt (eix Y negatiu SDL): atan2 + PI/2.
|
||||||
|
angle = std::atan2(TO.y, TO.x) + (Constants::PI / 2.0F);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
angle = randFloat01() * 2.0F * Constants::PI;
|
||||||
|
}
|
||||||
|
if (action.jitter_rad > 0.0F) {
|
||||||
|
angle += (randFloat01() - 0.5F) * 2.0F * action.jitter_rad;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto OWNER = static_cast<uint8_t>(Defaults::Entities::ENEMY_OWNER_BASE + enemy_index);
|
||||||
|
slot->fire(enemy.getCenter(), angle, OWNER, action.bullet_speed, cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void runMovement(Enemy& enemy, float delta_time) {
|
||||||
|
switch (enemy.getConfig().ai.movement.type) {
|
||||||
|
case MovementType::ZIGZAG:
|
||||||
|
case MovementType::WANDER:
|
||||||
|
// WANDER reusa la mecànica de canvi de direcció probabilístic;
|
||||||
|
// l'única diferència és semàntica i el tunning dels paràmetres.
|
||||||
|
moveZigzag(enemy, delta_time);
|
||||||
|
break;
|
||||||
|
case MovementType::TRACKING:
|
||||||
|
moveTracking(enemy, delta_time);
|
||||||
|
break;
|
||||||
|
case MovementType::RECTILINEAR_PROXIMITY:
|
||||||
|
moveRectilinearProximity(enemy, delta_time);
|
||||||
|
break;
|
||||||
|
case MovementType::CHASE:
|
||||||
|
moveChase(enemy, delta_time);
|
||||||
|
break;
|
||||||
|
case MovementType::FLEE:
|
||||||
|
moveFlee(enemy, delta_time);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void move(Enemy& enemy, float delta_time) {
|
||||||
|
if (!enemy.isActive() || enemy.isWounded()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runMovement(enemy, delta_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
void tick(Systems::Collision::Context& ctx, Enemy& enemy, std::size_t enemy_index, float delta_time) {
|
||||||
|
if (!enemy.isActive() || enemy.isWounded()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
runMovement(enemy, delta_time);
|
||||||
|
|
||||||
|
// Accions periòdiques: decrementa timer, dispara quan ≤0.
|
||||||
|
auto& timers = enemy.getAiTickTimers();
|
||||||
|
const auto& actions = enemy.getConfig().ai.tick;
|
||||||
|
for (std::size_t i = 0; i < actions.size() && i < timers.size(); ++i) {
|
||||||
|
timers[i] -= delta_time;
|
||||||
|
if (timers[i] > 0.0F) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
timers[i] = actions[i].interval;
|
||||||
|
switch (actions[i].type) {
|
||||||
|
case AiActionType::SHOOT:
|
||||||
|
doShoot(ctx, enemy, enemy_index, actions[i]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Systems::EnemyAi
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// enemy_ai_system.hpp - Executa la IA d'un enemic (moviment + accions tick)
|
||||||
|
// © 2026 JailDesigner
|
||||||
|
//
|
||||||
|
// Llegeix `enemy.getConfig().ai` i aplica la primitiva de moviment activa i,
|
||||||
|
// opcionalment, les accions periòdiques declarades (tick). Reemplaça el switch
|
||||||
|
// per type hardcoded de l'antic Enemy::update.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
#include "game/systems/collision_system.hpp"
|
||||||
|
|
||||||
|
class Enemy;
|
||||||
|
|
||||||
|
namespace Systems::EnemyAi {
|
||||||
|
|
||||||
|
// Aplica només la primitiva de moviment activa de l'enemic (sense disparar
|
||||||
|
// ni cap altra acció tick). S'usa als steps secundaris (continue/gameover/
|
||||||
|
// death) on els enemics han de continuar movent-se de fons però no actuar.
|
||||||
|
void move(Enemy& enemy, float delta_time);
|
||||||
|
|
||||||
|
// Aplica moviment + accions periòdiques (SHOOT, etc.). enemy_index és
|
||||||
|
// l'índex de l'enemic dins ctx.enemies; s'usa per construir el seu
|
||||||
|
// owner_id de bala (ENEMY_OWNER_BASE + index), de manera que el sistema
|
||||||
|
// de col·lisions pugui filtrar el self-shot.
|
||||||
|
void tick(Collision::Context& ctx, Enemy& enemy, std::size_t enemy_index, float delta_time);
|
||||||
|
|
||||||
|
} // namespace Systems::EnemyAi
|
||||||
@@ -3,13 +3,19 @@
|
|||||||
|
|
||||||
#include "game/systems/enemy_event_dispatcher.hpp"
|
#include "game/systems/enemy_event_dispatcher.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
#include "core/defaults.hpp"
|
#include "core/defaults.hpp"
|
||||||
#include "core/types.hpp"
|
#include "core/types.hpp"
|
||||||
|
#include "game/constants.hpp"
|
||||||
#include "game/entities/bullet.hpp"
|
#include "game/entities/bullet.hpp"
|
||||||
#include "game/entities/bullet_config.hpp"
|
#include "game/entities/bullet_config.hpp"
|
||||||
|
#include "game/entities/bullet_registry.hpp"
|
||||||
|
#include "game/entities/enemy_ai.hpp"
|
||||||
#include "game/entities/enemy_config.hpp"
|
#include "game/entities/enemy_config.hpp"
|
||||||
|
#include "game/entities/ship.hpp"
|
||||||
|
|
||||||
namespace Systems::EnemyEvents {
|
namespace Systems::EnemyEvents {
|
||||||
|
|
||||||
@@ -24,10 +30,15 @@ namespace Systems::EnemyEvents {
|
|||||||
ctx.floating_score_manager.crear(POINTS, enemy.getCenter());
|
ctx.floating_score_manager.crear(POINTS, enemy.getCenter());
|
||||||
}
|
}
|
||||||
|
|
||||||
void doCreateDebris(Systems::Collision::Context& ctx, const Enemy& enemy) {
|
// Helper compartit per CREATE_DEBRIS i CREATE_DEBRIS_PARTIAL: única
|
||||||
|
// crida a explode(), paràmetres alineats; canvien piece_scale (1.0
|
||||||
|
// explosió, 0.3 xip), el color (cos vs hit-feedback) i el so
|
||||||
|
// (ENEMY_EXPLOSION en la mort vs ENEMY_HIT en l'impacte parcial).
|
||||||
|
void spawnDebrisForEnemy(Systems::Collision::Context& ctx, const Enemy& enemy, const Bullet* bullet, float piece_scale, SDL_Color color, const char* sound) {
|
||||||
constexpr float SPEED_EXPLOSIO = 80.0F;
|
constexpr float SPEED_EXPLOSIO = 80.0F;
|
||||||
const Vec2 INHERITED_VEL = enemy.getVelocityVector() *
|
const Vec2 INHERITED_VEL = enemy.getVelocityVector() *
|
||||||
Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE;
|
Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE;
|
||||||
|
const Vec2 BULLET_VEL = (bullet != nullptr) ? bullet->getBody().velocity : Vec2{};
|
||||||
ctx.debris_manager.explode(
|
ctx.debris_manager.explode(
|
||||||
enemy.getShape(),
|
enemy.getShape(),
|
||||||
enemy.getCenter(),
|
enemy.getCenter(),
|
||||||
@@ -38,21 +49,26 @@ namespace Systems::EnemyEvents {
|
|||||||
INHERITED_VEL,
|
INHERITED_VEL,
|
||||||
0.0F,
|
0.0F,
|
||||||
0.0F,
|
0.0F,
|
||||||
Defaults::Sound::EXPLOSION,
|
sound,
|
||||||
enemy.getConfig().colors.normal,
|
color,
|
||||||
Defaults::Physics::Debris::ENEMY_LIFETIME,
|
Defaults::Physics::Debris::ENEMY_LIFETIME,
|
||||||
Defaults::Physics::Debris::ENEMY_FRICTION,
|
Defaults::Physics::Debris::ENEMY_FRICTION,
|
||||||
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER);
|
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER,
|
||||||
|
BULLET_VEL,
|
||||||
|
piece_scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
void doCreateFireworks(Systems::Collision::Context& ctx, const Enemy& enemy) {
|
// Helper compartit per CREATE_FIREWORKS i CREATE_FIREWORKS_SMALL:
|
||||||
|
// mateixa crida a spawn(); els callers decideixen line_color, glow_color,
|
||||||
|
// n_points i initial_speed segons el "tamany" del burst (mort vs hit).
|
||||||
|
void spawnFireworksForEnemy(Systems::Collision::Context& ctx, const Enemy& enemy, int n_points, float initial_speed, SDL_Color line_color, SDL_Color glow_color) {
|
||||||
ctx.firework_manager.spawn(enemy.getCenter(),
|
ctx.firework_manager.spawn(enemy.getCenter(),
|
||||||
Defaults::FX::Firework::DEFAULT_COLOR,
|
line_color,
|
||||||
Defaults::FX::Firework::SPEED,
|
initial_speed,
|
||||||
Defaults::FX::Firework::N_POINTS,
|
n_points,
|
||||||
Defaults::FX::Firework::INITIAL_BRIGHTNESS,
|
Defaults::FX::Firework::INITIAL_BRIGHTNESS,
|
||||||
/*glow=*/true,
|
/*glow=*/true,
|
||||||
enemy.getConfig().colors.wounded);
|
glow_color);
|
||||||
}
|
}
|
||||||
|
|
||||||
void doApplyImpulse(Enemy& enemy, const Bullet* bullet) {
|
void doApplyImpulse(Enemy& enemy, const Bullet* bullet) {
|
||||||
@@ -63,11 +79,117 @@ namespace Systems::EnemyEvents {
|
|||||||
(bullet->getBody().mass * bullet->getConfig().physics.impact_momentum_factor);
|
(bullet->getBody().mass * bullet->getConfig().physics.impact_momentum_factor);
|
||||||
enemy.applyImpulse(IMPULSE);
|
enemy.applyImpulse(IMPULSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto randFloat01() -> float {
|
||||||
|
return static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Còpia local de la mateixa primitiva que viu a enemy_ai_system.cpp.
|
||||||
|
// No s'ha extret a un header compartit perquè és l'únic punt de
|
||||||
|
// duplicació; si apareix un tercer consumidor, refactoritzar.
|
||||||
|
auto findNearestShipPosition(const Enemy& enemy) -> const Vec2* {
|
||||||
|
const Vec2& self = enemy.getCenter();
|
||||||
|
const Vec2* best = nullptr;
|
||||||
|
float best_dist_sq = 0.0F;
|
||||||
|
for (const Ship* ship : enemy.getShips()) {
|
||||||
|
if (ship == nullptr || !ship->isActive()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const Vec2& pos = ship->getCenter();
|
||||||
|
const Vec2 DELTA = pos - self;
|
||||||
|
const float DIST_SQ = DELTA.lengthSquared();
|
||||||
|
if (best == nullptr || DIST_SQ < best_dist_sq) {
|
||||||
|
best = &pos;
|
||||||
|
best_dist_sq = DIST_SQ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIRE_BULLET: paral·lel a doShoot() d'enemy_ai_system.cpp, però disparat
|
||||||
|
// per esdeveniment (típicament on_hit per a contra-atacs) en lloc de
|
||||||
|
// periòdicament. owner_id es deriva de l'índex dins ctx.enemies via
|
||||||
|
// aritmètica de punters (l'array és contigu).
|
||||||
|
void doFireBullet(Systems::Collision::Context& ctx, const Enemy& enemy, const EnemyAction& action) {
|
||||||
|
if (action.bullet_config_name.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const BulletConfig* cfg = BulletRegistry::get(action.bullet_config_name);
|
||||||
|
if (cfg == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Bullet* slot = nullptr;
|
||||||
|
constexpr std::size_t START = Defaults::Entities::ENEMY_BULLET_START_IDX;
|
||||||
|
constexpr std::size_t END = START + Defaults::Entities::MAX_ENEMY_BULLETS;
|
||||||
|
for (std::size_t i = START; i < END; ++i) {
|
||||||
|
if (!ctx.bullets[i].isActive()) {
|
||||||
|
slot = &ctx.bullets[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (slot == nullptr) {
|
||||||
|
return; // pool d'enemic ple
|
||||||
|
}
|
||||||
|
|
||||||
|
float angle = 0.0F;
|
||||||
|
if (action.aim_mode == AimMode::AIMED) {
|
||||||
|
const Vec2* target = findNearestShipPosition(enemy);
|
||||||
|
if (target == nullptr) {
|
||||||
|
angle = randFloat01() * 2.0F * Constants::PI;
|
||||||
|
} else {
|
||||||
|
const Vec2 TO = *target - enemy.getCenter();
|
||||||
|
angle = std::atan2(TO.y, TO.x) + (Constants::PI / 2.0F);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
angle = randFloat01() * 2.0F * Constants::PI;
|
||||||
|
}
|
||||||
|
if (action.jitter_rad > 0.0F) {
|
||||||
|
angle += (randFloat01() - 0.5F) * 2.0F * action.jitter_rad;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Localitzem l'índex de l'enemic per construir l'owner_id. Evitem
|
||||||
|
// aritmètica de punters sobre Enemy (tipus polimòrfic — UB si la
|
||||||
|
// jerarquia canvia); cerca lineal a l'array (mida petita, no és hot path).
|
||||||
|
std::size_t enemy_index = 0;
|
||||||
|
for (std::size_t i = 0; i < ctx.enemies.size(); ++i) {
|
||||||
|
if (&ctx.enemies[i] == &enemy) {
|
||||||
|
enemy_index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto OWNER = static_cast<uint8_t>(Defaults::Entities::ENEMY_OWNER_BASE + enemy_index);
|
||||||
|
slot->fire(enemy.getCenter(), angle, OWNER, action.bullet_speed, cfg);
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void dispatchEvent(Systems::Collision::Context& ctx, Enemy& enemy, EnemyEventType event, uint8_t shooter_id, const Bullet* bullet) {
|
void dispatchEvent(Systems::Collision::Context& ctx, Enemy& enemy, EnemyEventType event, uint8_t shooter_id, const Bullet* bullet) {
|
||||||
const auto& actions = enemy.getConfig().events.getActions(event);
|
const auto& actions = enemy.getConfig().events.getActions(event);
|
||||||
|
|
||||||
|
// Pre-scan: aquest event matarà l'enemic? Si sí, l'impuls de la bala
|
||||||
|
// va directament als debris (via doCreateDebris) i NO s'aplica al cos
|
||||||
|
// — així evitem el "double-count" on els trossos hereten la velocitat
|
||||||
|
// del cos (boostat per la bala) I a més el seu propi impuls de bala.
|
||||||
|
// Regla: el bullet impacta al cos O als trossos, mai a tots dos.
|
||||||
|
bool will_die = false;
|
||||||
for (const auto& action : actions) {
|
for (const auto& action : actions) {
|
||||||
|
if (action.type == EnemyActionType::DESTROY) {
|
||||||
|
will_die = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (action.type == EnemyActionType::SET_HURT && enemy.isWounded()) {
|
||||||
|
will_die = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& action : actions) {
|
||||||
|
// Si una acció prèvia d'aquest chain ha destruït l'enemic
|
||||||
|
// (típicament DECREASE_HEALTH→ON_NO_HEALTH→SET_HURT-wounded→DESTROY),
|
||||||
|
// saltem la resta — no té sentit aplicar APPLY_IMPULSE o FLASH a un
|
||||||
|
// cos ja inactiu.
|
||||||
|
if (!enemy.isActive()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case EnemyActionType::SET_HURT:
|
case EnemyActionType::SET_HURT:
|
||||||
if (enemy.isWounded()) {
|
if (enemy.isWounded()) {
|
||||||
@@ -86,13 +208,41 @@ namespace Systems::EnemyEvents {
|
|||||||
doAddScore(ctx, enemy, shooter_id);
|
doAddScore(ctx, enemy, shooter_id);
|
||||||
break;
|
break;
|
||||||
case EnemyActionType::CREATE_DEBRIS:
|
case EnemyActionType::CREATE_DEBRIS:
|
||||||
doCreateDebris(ctx, enemy);
|
// Explosió de mort: trossos en color cos + so ENEMY_EXPLOSION.
|
||||||
|
spawnDebrisForEnemy(ctx, enemy, bullet, 1.0F, enemy.getConfig().colors.normal, Defaults::Sound::ENEMY_EXPLOSION);
|
||||||
|
break;
|
||||||
|
case EnemyActionType::CREATE_DEBRIS_PARTIAL:
|
||||||
|
// Xip d'impacte: trossos en color wounded (daurat) + so
|
||||||
|
// ENEMY_HIT, diferent de l'explosió per marcar damage no letal.
|
||||||
|
spawnDebrisForEnemy(ctx, enemy, bullet, Defaults::Enemies::Debris::PARTIAL_PIECE_SCALE, enemy.getConfig().colors.wounded, Defaults::Sound::ENEMY_HIT);
|
||||||
break;
|
break;
|
||||||
case EnemyActionType::CREATE_FIREWORKS:
|
case EnemyActionType::CREATE_FIREWORKS:
|
||||||
doCreateFireworks(ctx, enemy);
|
// Burst de mort: línia blanca + glow wounded (daurat) per
|
||||||
|
// marcar la mort com a esdeveniment "calent" i lluminós.
|
||||||
|
spawnFireworksForEnemy(ctx, enemy, Defaults::FX::Firework::N_POINTS, Defaults::FX::Firework::SPEED, Defaults::FX::Firework::DEFAULT_COLOR, enemy.getConfig().colors.wounded);
|
||||||
|
break;
|
||||||
|
case EnemyActionType::CREATE_FIREWORKS_SMALL:
|
||||||
|
// Espurna d'impacte: línia + glow tots dos en wounded
|
||||||
|
// (daurat) per contrastar amb el cos i unificar la "tema
|
||||||
|
// de damage" amb el debris parcial.
|
||||||
|
spawnFireworksForEnemy(ctx, enemy, Defaults::Enemies::Fireworks::SMALL_N_POINTS, Defaults::Enemies::Fireworks::SMALL_SPEED, enemy.getConfig().colors.wounded, enemy.getConfig().colors.wounded);
|
||||||
break;
|
break;
|
||||||
case EnemyActionType::APPLY_IMPULSE:
|
case EnemyActionType::APPLY_IMPULSE:
|
||||||
doApplyImpulse(enemy, bullet);
|
if (!will_die) {
|
||||||
|
doApplyImpulse(enemy, bullet);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case EnemyActionType::DECREASE_HEALTH:
|
||||||
|
enemy.decrementHealth(shooter_id);
|
||||||
|
if (enemy.getHealth() <= 0) {
|
||||||
|
dispatchEvent(ctx, enemy, EnemyEventType::ON_NO_HEALTH, shooter_id, bullet);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case EnemyActionType::FLASH:
|
||||||
|
enemy.triggerFlash();
|
||||||
|
break;
|
||||||
|
case EnemyActionType::FIRE_BULLET:
|
||||||
|
doFireBullet(ctx, enemy, action);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "core/defaults.hpp"
|
#include "core/defaults.hpp"
|
||||||
|
#include "core/defaults/hud.hpp"
|
||||||
#include "core/math/easing.hpp"
|
#include "core/math/easing.hpp"
|
||||||
#include "core/rendering/line_renderer.hpp"
|
#include "core/rendering/line_renderer.hpp"
|
||||||
|
|
||||||
@@ -77,8 +78,40 @@ namespace Systems::InitHud {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void drawScoreboardSegmentsAt(const Graphics::VectorText& text,
|
||||||
|
const ScoreboardSegments& segments,
|
||||||
|
const Vec2& center,
|
||||||
|
float scale,
|
||||||
|
float spacing) {
|
||||||
|
// Separadors entre segments (preservant el layout legacy: " ", " ", " ", " ").
|
||||||
|
const float W_SEP1 = Graphics::VectorText::getTextWidth(" ", scale, spacing);
|
||||||
|
const float W_SEP2 = Graphics::VectorText::getTextWidth(" ", scale, spacing);
|
||||||
|
|
||||||
|
const float W_SP1 = Graphics::VectorText::getTextWidth(segments.score_p1, scale, spacing);
|
||||||
|
const float W_LP1 = Graphics::VectorText::getTextWidth(segments.lives_p1, scale, spacing);
|
||||||
|
const float W_LV = Graphics::VectorText::getTextWidth(segments.level, scale, spacing);
|
||||||
|
const float W_SP2 = Graphics::VectorText::getTextWidth(segments.score_p2, scale, spacing);
|
||||||
|
const float W_LP2 = Graphics::VectorText::getTextWidth(segments.lives_p2, scale, spacing);
|
||||||
|
|
||||||
|
const float TOTAL = W_SP1 + W_SEP1 + W_LP1 + W_SEP2 + W_LV + W_SEP2 + W_SP2 + W_SEP1 + W_LP2;
|
||||||
|
|
||||||
|
const float HEIGHT = Graphics::VectorText::getTextHeight(scale);
|
||||||
|
const float TOP_Y = center.y - (HEIGHT / 2.0F);
|
||||||
|
float x = center.x - (TOTAL / 2.0F);
|
||||||
|
|
||||||
|
text.render(segments.score_p1, {.x = x, .y = TOP_Y}, scale, spacing, 1.0F, Defaults::Hud::Colors::SCORE_P1);
|
||||||
|
x += W_SP1 + W_SEP1;
|
||||||
|
text.render(segments.lives_p1, {.x = x, .y = TOP_Y}, scale, spacing, 1.0F, Defaults::Hud::Colors::LIVES);
|
||||||
|
x += W_LP1 + W_SEP2;
|
||||||
|
text.render(segments.level, {.x = x, .y = TOP_Y}, scale, spacing, 1.0F, Defaults::Hud::Colors::LEVEL);
|
||||||
|
x += W_LV + W_SEP2;
|
||||||
|
text.render(segments.score_p2, {.x = x, .y = TOP_Y}, scale, spacing, 1.0F, Defaults::Hud::Colors::SCORE_P2);
|
||||||
|
x += W_SP2 + W_SEP1;
|
||||||
|
text.render(segments.lives_p2, {.x = x, .y = TOP_Y}, scale, spacing, 1.0F, Defaults::Hud::Colors::LIVES);
|
||||||
|
}
|
||||||
|
|
||||||
void drawScoreboardAnimated(const Graphics::VectorText& text,
|
void drawScoreboardAnimated(const Graphics::VectorText& text,
|
||||||
const std::string& scoreboard_text,
|
const ScoreboardSegments& segments,
|
||||||
float progress) {
|
float progress) {
|
||||||
const float EASED = Easing::easeOutQuad(progress);
|
const float EASED = Easing::easeOutQuad(progress);
|
||||||
|
|
||||||
@@ -91,10 +124,7 @@ namespace Systems::InitHud {
|
|||||||
const auto Y_INI = static_cast<float>(Defaults::Game::HEIGHT);
|
const auto Y_INI = static_cast<float>(Defaults::Game::HEIGHT);
|
||||||
const float Y_ANIM = Y_INI + ((Y_FINAL - Y_INI) * EASED);
|
const float Y_ANIM = Y_INI + ((Y_FINAL - Y_INI) * EASED);
|
||||||
|
|
||||||
text.renderCentered(scoreboard_text,
|
drawScoreboardSegmentsAt(text, segments, {.x = CENTRE_X, .y = Y_ANIM}, SCALE, SPACING);
|
||||||
Vec2{.x = CENTRE_X, .y = Y_ANIM},
|
|
||||||
SCALE,
|
|
||||||
SPACING);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Systems::InitHud
|
} // namespace Systems::InitHud
|
||||||
|
|||||||
@@ -21,6 +21,17 @@
|
|||||||
|
|
||||||
namespace Systems::InitHud {
|
namespace Systems::InitHud {
|
||||||
|
|
||||||
|
// Segments del marcador. Cada segment es renderitza amb el seu propi color
|
||||||
|
// (vegeu Defaults::Hud::Colors). El layout final concatena en aquest ordre
|
||||||
|
// amb separadors d'1, 2, 2, 1 espais respectivament (igual que el legacy).
|
||||||
|
struct ScoreboardSegments {
|
||||||
|
std::string score_p1;
|
||||||
|
std::string lives_p1;
|
||||||
|
std::string level; // ex: "NIVELL 01"
|
||||||
|
std::string score_p2;
|
||||||
|
std::string lives_p2;
|
||||||
|
};
|
||||||
|
|
||||||
// Convierte un progreso global 0..1 al sub-progreso de un elemento que solo
|
// Convierte un progreso global 0..1 al sub-progreso de un elemento que solo
|
||||||
// se anima en la ventana [ratio_init, ratio_end].
|
// se anima en la ventana [ratio_init, ratio_end].
|
||||||
// < ratio_init → 0.0 (no empezó)
|
// < ratio_init → 0.0 (no empezó)
|
||||||
@@ -40,10 +51,19 @@ namespace Systems::InitHud {
|
|||||||
// 66..100% → línea inferior crece desde los lados hacia el centro.
|
// 66..100% → línea inferior crece desde los lados hacia el centro.
|
||||||
void drawBordersAnimated(Rendering::Renderer* renderer, float progress);
|
void drawBordersAnimated(Rendering::Renderer* renderer, float progress);
|
||||||
|
|
||||||
// Dibuja el scoreboard centrado, subiendo desde fuera de la pantalla
|
// Dibuixa els 5 segments del scoreboard centrats al voltant de `center`,
|
||||||
// hasta su posición final con easing.
|
// cadascun amb el seu color (Defaults::Hud::Colors). Separadors de 1/2/2/1
|
||||||
|
// espais entre segments per preservar el layout legacy.
|
||||||
|
void drawScoreboardSegmentsAt(const Graphics::VectorText& text,
|
||||||
|
const ScoreboardSegments& segments,
|
||||||
|
const Vec2& center,
|
||||||
|
float scale,
|
||||||
|
float spacing);
|
||||||
|
|
||||||
|
// Dibuixa el scoreboard centrat, pujant des de fora de la pantalla fins a
|
||||||
|
// la seva posició final amb easing. Delega a drawScoreboardSegmentsAt.
|
||||||
void drawScoreboardAnimated(const Graphics::VectorText& text,
|
void drawScoreboardAnimated(const Graphics::VectorText& text,
|
||||||
const std::string& scoreboard_text,
|
const ScoreboardSegments& segments,
|
||||||
float progress);
|
float progress);
|
||||||
|
|
||||||
} // namespace Systems::InitHud
|
} // namespace Systems::InitHud
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ namespace Title {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ShipAnimator::init() {
|
void ShipAnimator::init() {
|
||||||
auto shape_p1 = Graphics::ShapeLoader::load("ship.shp");
|
auto shape_p1 = Graphics::ShapeLoader::load("ship/arrow.shp");
|
||||||
auto shape_p2 = Graphics::ShapeLoader::load("ship2.shp");
|
auto shape_p2 = Graphics::ShapeLoader::load("ship/wedge.shp");
|
||||||
|
|
||||||
ships_[0].player_id = 1;
|
ships_[0].player_id = 1;
|
||||||
if (shape_p1 && shape_p1->isValid()) {
|
if (shape_p1 && shape_p1->isValid()) {
|
||||||
@@ -225,13 +225,21 @@ namespace Title {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto ShipAnimator::isVisible() const -> bool {
|
auto ShipAnimator::isVisible() const -> bool {
|
||||||
return std::ranges::any_of(ships_,
|
for (const auto& s : ships_) {
|
||||||
[](const TitleShip& s) { return s.visible; });
|
if (s.visible) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto ShipAnimator::isAnimationComplete() const -> bool {
|
auto ShipAnimator::isAnimationComplete() const -> bool {
|
||||||
return std::ranges::all_of(ships_,
|
for (const auto& s : ships_) {
|
||||||
[](const TitleShip& s) { return !s.visible; });
|
if (s.visible) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShipAnimator::updateEntering(TitleShip& ship, float delta_time) {
|
void ShipAnimator::updateEntering(TitleShip& ship, float delta_time) {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
//
|
//
|
||||||
// Manté la mateixa màquina d'estats
|
// Manté la mateixa màquina d'estats
|
||||||
// (ENTERING → FLOATING → EXITING) però treballa amb posicions Vec3 i emet
|
// (ENTERING → FLOATING → EXITING) però treballa amb posicions Vec3 i emet
|
||||||
// wireframes a través d'una `Camera3D`. La geometria s'extrau de `ship.shp`
|
// wireframes a través d'una `Camera3D`. La geometria s'extrau de
|
||||||
// (P1) i `ship2.shp` (P2) per extrusió en Z.
|
// `ship/arrow.shp` (P1) i `ship/wedge.shp` (P2) per extrusió en Z.
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user