Compare commits

..

36 Commits

Author SHA1 Message Date
JailDesigner 9c0502eefb feat(enemy): sistema d'events declaratius via YAML 2026-05-25 13:34:48 +02:00
JailDesigner 9b3da3a6e7 Merge branch 'feat/enemy-star': afegir tipus STAR i 3 nous shapes 2026-05-25 12:42:06 +02:00
JailDesigner bc41169176 feat(enemy): afegir tipus STAR (estrella de 5 puntes) i 3 nous shapes
- Nou enemic STAR amb shape star_5.shp, escala 0.7 i color groc pur.
  Reusa el comportament zigzag del Pentagon i carrega via EnemyRegistry.
- DistribucioEnemics estesa amb camp 'star' opcional (default 0) per
  mantenir compat amb stages antics.
- Stage 1 reconfigurat a 25/25/25/25 per mostrar els 4 tipus.
- Afegits també shapes bullet_long.shp i bullet_double.shp (encara no
  utilitzats; preparats per futures variants de bala).
2026-05-25 12:36:26 +02:00
JailDesigner b3a1afce06 Merge branch 'feat/entities-yaml-enemy-shared': paràmetres compartits dels enemics a cada YAML 2026-05-25 11:59:28 +02:00
JailDesigner 4b6dc8a47a feat(entities): migrar paràmetres compartits dels enemics a cada YAML 2026-05-25 11:54:40 +02:00
JailDesigner 3dadd5fc1a Merge branch 'feat/entities-yaml-bullet': migració de la bala a YAML 2026-05-25 11:47:36 +02:00
JailDesigner bea844d51e feat(entities): migrar bullet a data/entities/bullet/bullet.yaml 2026-05-25 11:42:43 +02:00
JailDesigner 5fb6c68df4 Merge branch 'feat/entities-shape-scale': collision_radius derivat del shape + scale al YAML 2026-05-25 11:33:52 +02:00
JailDesigner 866a057704 feat(entities): derivar collision_radius del shape + scale/collision_factor al YAML 2026-05-25 11:29:43 +02:00
JailDesigner da8eab330d Merge branch 'feat/entities-yaml-enemies': configuració dels enemics en YAML 2026-05-25 10:15:34 +02:00
JailDesigner 39bda0775e feat(entities): migrar la configuració dels 3 enemics a data/entities/<type>/*.yaml 2026-05-25 10:01:12 +02:00
JailDesigner ed4d3a3915 Merge branch 'feat/entities-yaml-player': configuració del player en YAML 2026-05-25 09:39:39 +02:00
JailDesigner 6447932212 feat(entities): migrar la configuració del player a data/entities/player/player.yaml 2026-05-25 08:32:49 +02:00
JailDesigner 9f278772bb Merge branch 'feat/pack-resources-align': alinear pack_resources amb projectes germans 2026-05-25 07:55:49 +02:00
JailDesigner 2d073b6055 feat(pack): alinear sortida i build amb projectes germans 2026-05-25 07:55:30 +02:00
JailDesigner 99b18d208d chore: bump version a 0.8.0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:41:25 +02:00
JailDesigner 1321566910 Merge branch 'feature/sistema-gestio-inputs': sistema de gestio d'inputs
Modul DefineInputs per redefinir teclat i mando amb overlay modal,
pagina CONTROLS al menu de servei (picker de mando amb llista, swap
automatic en conflicte, slot SENSE MANDO, rebind per jugador), so
accept en cada captura, navegacio del menu amb dpad/stick i triggers
L2/R2, glyphs ( ) i / al charset, autoassignacio de mando al primer
arranque, i diversos fixes de pipeline d'events.
2026-05-24 22:39:47 +02:00
JailDesigner cefafe99e4 feat(service_menu): triggers L2/R2 navegables + so al rebind
El menu de servei nomes processava AXIS_MOTION dels sticks i descartava
els triggers. Com SDL3 mai emet button events per a L2/R2 (nomes axis),
rebindar FIRE o ACCEL a un trigger feia que no funcionaren al menu, fins
i tot estant correctament al joc per via del poll de Input::checkTriggerInput.
Afegim edge-detect dels dos triggers al handleGamepadAxis i, quan creuen
el llindar, mirem si el codi virtual (100=L2, 101=R2) coincideix amb el
binding de FIRE → activateCurrent, o ACCEL → popPage. Estat held per
trigger per evitar repeticions mentre es mante premut.

DefineInputs ara reprodueix el so accept del menu en cada captura
valida, que estava silent i no donava feedback al rebind.

Tambe extraiem processStickX/Y i processTriggerEdge per mantenir
handleGamepadAxis com a dispatcher i sota el llindar de complexitat
cognitiva del clang-tidy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:38:10 +02:00
JailDesigner daa7eaf811 feat(service_menu): glyphs () + tanca picker al seleccionar mando
Afegim els glyphs ( i ) a VectorText (char_lparen.shp, char_rparen.shp,
arcs de 4 trams dins la caixa 20x40) perque el sufix (P1)/(P2) de la
picker de mando es renderitze net sense warnings.

A mes, al triar un mando o "SENSE MANDO" a la picker fem popPage
automatic, perque l'usuari no haja de tornar enrere a ma després
d'una assignacio.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:20:29 +02:00
JailDesigner 3dcf5c3a99 feat(service_menu): picker de mando per llista i fix SENSE MANDO
El cycle anterior fallava al desasignar perque Input::resolvePlayerGamepad
tenia un fallback per slot que reasignava gamepads_[player_index] quan
name+path eren buits. Això el contradeia el slot "SENSE MANDO" del cycle:
el YAML quedava buit pero el runtime seguia lligant el mando. Treure el
fallback i moure l'autoassignacio inicial al boot (nomes si tots dos
jugadors venen buits) restaura la semàntica: buit vol dir buit.

Sobre el fix, redissenyem la UX dels items MANDO P1/P2: ja no son CYCLE
sino SUBMENU que obrin una pàgina-llista (estil RESOLUCIÓ) amb tots els
mandos detectats. Cada item porta sufix (P1)/(P2) nomes si el mando el
te l'altre jugador, perque sapigues que assignar-lo li'l "robarà".
L'ultim item es "SENSE MANDO" per a desassignar explícitament. La
lògica de swap automatic en conflicte queda extreta a assignPadToPlayer
i es reutilitza des de la picker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:12:53 +02:00
JailDesigner 99d0f62ab5 feat(service_menu): slot 'sense mando' al cycle i swap automatic en conflicte
El CYCLE de la pagina CONTROLS ara inclou un slot virtual al final que
desassigna el mando (gamepad_name + gamepad_path buits → padDisplayName
mostra "SENSE MANDO"). Aixi l'usuari pot recuperar el control teclat
sense haver d'editar el YAML.

A mes, si en assignar un mando l'altre jugador ja el tenia, fem swap
automatic: l'altre jugador rep l'assignacio previa d'aquest, evitant
que dos jugadors comparteixen el mateix dispositiu. La deteccio
prioritza path (mateixa branca que resolvePlayerGamepad).

Extracta tambe reapplyBindings per mantenir cyclePlayerPad llegible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:22:23 +02:00
JailDesigner 85050c8da4 fix(define-inputs): deixa passar QUIT i ESC al pipeline global
L'overlay de redefinicio engolia tots els events mentre estava actiu, fet
que impedia tancar la finestra amb l'aspa (SDL_EVENT_QUIT) i deixava
prendre ESC com a cancel-lacio del rebind. Ara:
- QUIT i WINDOW_CLOSE_REQUESTED passen sempre al global per tancar
  l'aplicacio des de l'aspa.
- ESC ja no cancel-la la sequencia; cau al global on obre el prompt
  d'eixida com a la resta del joc.
- isReservedScancode (ESC/F1-F12/RETURN/BACKSPACE/TAB) deixa passar.

Tambe ajusta DISPAR -> DISPARAR a ca.yaml i treu el hint "ESC PER
CANCEL-LAR" del modal i les claus de locale corresponents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:20:25 +02:00
JailDesigner 120c5502fd feat(vector-text): afegeix el glyph / al charset
El progres "i/n" del modal de redefinicio (ex. 1/4) sortia com a "14"
perque VectorText no tenia shape per a la barra i emetia un warning.
Afegim font/char_slash.shp (diagonal de baix-esquerra a dalt-dreta dins
de la caixa 20x40) i el registrem al loader i al getShapeFilename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:58:52 +02:00
JailDesigner 64a6599e81 fix(title): manten animacions amb menu obert, bloqueja nomes els polls d'input
El fix anterior pausava tot el title quan el menu de servei estava obert,
trencant l'efecte d'animacio de fons. Ara title segueix animant-se i
nomes guardem handleSkipInput/handleStartInput mentre el menu o el modal
de rebind estan actius, per evitar START fantasma sense congelar el render.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:54:04 +02:00
JailDesigner a4b567588f feat(service_menu): navegacio amb mando (dpad, stick, fire = enter, accelerate = back)
ServiceMenu::handleEvent ara accepta tambe SDL_EVENT_GAMEPAD_BUTTON_DOWN
i SDL_EVENT_GAMEPAD_AXIS_MOTION. Mapeig: dpad UP/DOWN/LEFT/RIGHT mouen
el cursor, el boto FIRE configurat per qualsevol jugador equival a ENTER
(activa l'item), ACCELERATE equival a BACK (popPage). El stick esquerre
fa nav amb edge-detect: cal tornar a centre per disparar una altra entrada.
GlobalEvents::forwardToServiceMenu envia tots aquests events al menu
quan esta obert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:42:33 +02:00
JailDesigner 2e74fea2d5 feat(input): stick com a font alternativa de LEFT/RIGHT al mando
LEFT i RIGHT no son redefinibles al mando i s'assumeix dpad O stick.
Input::update() ara llegeix SDL_GAMEPAD_AXIS_LEFTX i fa OR amb l'estat
del dpad: qualsevol de les dos fonts dispara l'accio. Llindar 30000
(coherent amb el constant AXIS_THRESHOLD ja existent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:38:26 +02:00
JailDesigner c4933875dd fix(input): impedeix que els events traspassin al joc en acabar el rebind
El menu de servei queda obert per sota de l'overlay DefineInputs durant
tot el rebind (en lloc de tancar-se al activar la accio), de manera que
absorbeix qualsevol KEY_DOWN que arribi un cop l'overlay s'auto-cancela.
La pantalla de titol tambe pausa la seua logica mentre el menu de servei
esta obert, igual que GameScene, per evitar que detecti un START fantasma
si l'usuari encara te una tecla pulsada al moment de tancar-se el modal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:36:51 +02:00
JailDesigner 10a54aef91 fix(ui): nom del mando en majuscules a la UI sense modificar el config
VectorText nomes admet ASCII en majuscules; els noms dels mandos (i el
git hash) passaven pel toUpperAscii local del service_menu, pero les
notificacions de hot-plug i el text del CYCLE de la pagina CONTROLS
es mostraven amb el case original. Mou el helper a un utils compartit i
l'aplica a tots els punts de display sense tocar gamepad_name al config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:33:01 +02:00
JailDesigner 34be79192c feat(service_menu): pagina CONTROLS amb assignacio de pad i rebind per jugador
Afegeix submenu CONTROLS al menu de servei amb 2 items CYCLE per
seleccionar el mando assignat a cada jugador (persistit per name + path)
i 4 items ACTION per arrancar DefineInputs (teclat/mando per a P1/P2).

Tambe afegeix:
- Director: init/update/draw/destroy del singleton DefineInputs.
- GlobalEvents: routing prioritari de tots els events a DefineInputs
  mentre l'overlay esta actiu.
- Locale ca/en: claus del submenu CONTROLS i de l'overlay de rebind.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:18:49 +02:00
JailDesigner fcf13591be feat(input): modul DefineInputs per redefinir teclat i mando
Singleton inspirat en aee_arcade DefineButtons: pinta una caixa central
modal, captura events SDL i avança per una sequencia fixa d'accions per
jugador. Teclat: LEFT/RIGHT/FIRE/ACCELERATE. Mando: FIRE/ACCELERATE/
START/MENU. ESC cancel-la, duplicats dins la sessio es rebutgen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:17:30 +02:00
JailDesigner 3e8f2f35bf feat(input): accio MENU i assignacio de mando per path + name
Afegeix l'accio MENU a InputAction (obre el menu de servei des del mando,
equivalent a F12 al teclat) i els camps gamepad.button_start i
gamepad.button_menu al config per jugador. Tambe afegeix gamepad_path
per distingir dos mandos del mateix model i prioritza path > name >
slot a applyPlayerNBindings via el nou resolvePlayerGamepad.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:56:59 +02:00
JailDesigner e5a91825b1 feat(input): notifica connexio/desconnexio de mandos via Notifier
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 19:55:42 +02:00
JailDesigner b3271b17a2 Merge branch 'feat/preload-resources': precàrrega completa al boot 2026-05-24 19:32:04 +02:00
JailDesigner d4117e3505 feat(boot): precàrrega de música, sons i shapes a l'arrencada 2026-05-24 19:31:35 +02:00
JailDesigner 73c7e4ea76 Merge branch 'fix/fps-rounding': FPS arrodonit 2026-05-24 19:20:14 +02:00
JailDesigner 23cc5ce68d fix(debug-hud): FPS arrodonit en lloc de truncat 2026-05-24 19:20:06 +02:00
75 changed files with 3908 additions and 1384 deletions
+5 -2
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(orni VERSION 0.7.2 LANGUAGES CXX) project(orni VERSION 0.8.0 LANGUAGES CXX)
# Info del projecte (font de veritat per a project.h) # Info del projecte (font de veritat per a project.h)
set(PROJECT_LONG_NAME "Orni Attack") set(PROJECT_LONG_NAME "Orni Attack")
@@ -110,7 +110,10 @@ add_executable(pack_resources EXCLUDE_FROM_ALL
tools/pack_resources/pack_resources.cpp tools/pack_resources/pack_resources.cpp
source/core/resources/resource_pack.cpp source/core/resources/resource_pack.cpp
) )
target_include_directories(pack_resources PRIVATE "${CMAKE_SOURCE_DIR}/source") target_include_directories(pack_resources PRIVATE
"${CMAKE_SOURCE_DIR}/source"
"${CMAKE_BINARY_DIR}"
)
target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic) target_compile_options(pack_resources PRIVATE -Wall -Wextra -Wpedantic)
# --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack --- # --- REGENERACIÓ AUTOMÀTICA DE build/resources.pack ---
+22
View File
@@ -0,0 +1,22 @@
name: bullet
# Shape de la bala. El bounding_radius del .shp dóna el hitbox base (~3 px);
# scale el modula visualment i pel hitbox.
shape:
path: bullet.shp
scale: 1.0
collision_factor: 1.0
# Cinemàtica pura: la bala no col·lisiona físicament al PhysicsWorld
# (body_.radius = 0 al spawn), però sí participa al gameplay via
# checkCollisionSwept. La mass i l'impact_momentum_factor es fan servir
# només per calcular l'impuls que rep l'enemic en impactar.
physics:
mass: 0.5
restitution: 0.0 # irrelevant (no rebota)
linear_damping: 0.0 # movement rectilini uniforme
angular_damping: 0.0
impact_momentum_factor: 3.0 # factor de transferència de moment bala→enemic
colors:
normal: [155, 255, 175] # verd laser
+68
View File
@@ -0,0 +1,68 @@
name: pentagon
ai_type: pentagon # Validat contra el directori; mapeja a EnemyType::PENTAGON.
shape:
path: enemy_pentagon.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics:
mass: 5.0
speed: 35.0 # px/s (esquivador lent)
rotation_delta_min: 0.75 # rad/s — rotació visual mínima
rotation_delta_max: 3.75 # rad/s — rotació visual màxima
restitution: 1.0 # rebot elàstic perfecte contra parets
linear_damping: 0.0 # manté velocitat (sense fricció)
angular_damping: 0.0
behavior:
# Pentagon: zigzag esquivador (canvi de direcció probabilístic per segon).
angle_change_max: 1.0 # rad — magnitud del canvi de direcció
zigzag_prob_per_second: 0.8
animation:
pulse: # respiració d'escala aleatòria
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel: # acceleració/desacceleració de rotació visual
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0 # segons en estat ferit abans d'explotar
blink_hz: 10.0 # parpelleig color normal ↔ wounded
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0 # px mínim respecte al player al spawn
colors:
normal: [0, 255, 255] # Cyan pur "esquivador"
wounded: [255, 220, 60] # Daurat (parpelleig al rebre impacte)
score: 100
events:
# Comportament clàssic: dos impactes per matar (set_hurt entra wounded;
# el segon hit detecta wounded i destrueix automàticament).
on_hit:
- action: apply_impulse
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+66
View File
@@ -0,0 +1,66 @@
name: pinwheel
ai_type: pinwheel # Validat contra el directori; mapeja a EnemyType::PINWHEEL.
shape:
path: enemy_pinwheel.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics:
mass: 4.0 # Més lleuger — àgil
speed: 50.0 # px/s (el més ràpid)
rotation_delta_min: 3.0 # rad/s — rotació base elevada
rotation_delta_max: 6.0
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
behavior:
# Pinwheel: movement rectilíniauniforme + boost de rotació visual prop de la nau.
rotation_proximity_multiplier: 3.0 # Multiplicador de rotació quan és prop de la nau
proximity_distance: 100.0 # Llindar de distància (px)
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0
colors:
normal: [255, 0, 255] # Magenta pur "agressiu"
wounded: [255, 220, 60]
score: 200
events:
on_hit:
- action: apply_impulse
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+49
View File
@@ -0,0 +1,49 @@
name: player_ship
# Shape de la nau. Resolt per ShapeLoader (busca a "shapes/<path>").
# Nota: el segon jugador rep un override del shape ("ship2.shp") al ctor.
# Quan s'introdueixin variants reals de nau, es crearà un YAML separat
# per cada model.
#
# scale: multiplicador visual i de hitbox sobre la mida nativa del .shp (1.0 = mida del fitxer).
# collision_factor: ajust opcional del hitbox respecte el cercle circumscrit
# automàtic de la shape; tocar només si el feel del hitbox
# no quadra amb la silueta visual (default 1.0).
shape:
path: ship.shp
scale: 1.0
collision_factor: 1.0
physics:
mass: 10.0
restitution: 0.6
linear_damping: 1.5
angular_damping: 0.0
rotation_speed: 3.14 # rad/s (~180 deg/s, input-driven sense inercia)
acceleration: 400.0 # px/s^2 multiplicat per la massa quan THRUST
max_velocity: 180.0 # px/s (clamp post-integració per preservar feel arcade)
# Factor de transferència del moment lineal de la nau a l'enemic en el
# frame exacte que mor per col·lisió (afegit per damunt del rebot natural).
death_impact_factor: 0.3
invulnerability:
duration: 3.0 # segons d'invulnerabilitat post-respawn
blink_visible: 0.1 # segons visible per cicle de parpelleig
blink_invisible: 0.1 # segons invisible per cicle de parpelleig
hurt:
duration: 15.0 # segons en estat "ferit" abans de tornar a normal
blink_hz: 10.0 # freqüència parpelleig color normal <-> color hurt
# Empenta visual: la nau s'escala lleugerament amb la velocitat.
# Manté la sensació del Pascal original (0..MAX_VEL → 1.0..~1.5).
visual_thrust:
push_divisor: 33.33
scale_divisor: 12.0
colors:
normal: [255, 255, 255] # blanc neutre
hurt: [255, 220, 60] # daurat (estat ferit)
weapon:
bullet_speed: 700.0 # velocitat escalar de la bullet (px/s)
+66
View File
@@ -0,0 +1,66 @@
name: square
ai_type: square # Validat contra el directori; mapeja a EnemyType::SQUARE.
shape:
path: enemy_square.shp
scale: 1.0 # multiplicador visual + hitbox sobre la mida nativa del .shp
collision_factor: 1.0 # ajust opcional del hitbox (default 1.0)
physics:
mass: 8.0 # Més pesat — "tanc"
speed: 40.0 # px/s (velocitat mitjana)
rotation_delta_min: 0.3 # rad/s — rotació lenta
rotation_delta_max: 1.5
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
behavior:
# Square: tracking discret cap a la nau cada N segons.
tracking_strength: 0.5 # Interpolació LERP cap a la direcció desitjada (0..1)
tracking_interval: 1.0 # segons entre updates d'angle
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0
colors:
normal: [255, 0, 0] # Roig pur "tanc"
wounded: [255, 220, 60]
score: 150
events:
on_hit:
- action: apply_impulse
- action: set_hurt
on_hurt_end:
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+65
View File
@@ -0,0 +1,65 @@
name: star
ai_type: star # Validat contra el directori; mapeja a EnemyType::STAR.
shape:
path: star_5.shp
scale: 0.7 # Lleugerament més petit que els altres enemics per diferenciar visualment.
collision_factor: 1.0
physics:
mass: 5.0
speed: 35.0 # Mateixos paràmetres que pentagon (esquivador lent).
rotation_delta_min: 0.75
rotation_delta_max: 3.75
restitution: 1.0
linear_damping: 0.0
angular_damping: 0.0
behavior:
# Hereta el comportament de Pentagon (zigzag esquivador).
angle_change_max: 1.0
zigzag_prob_per_second: 0.8
animation:
pulse:
trigger_prob_per_second: 0.01
duration_min: 1.0
duration_max: 3.0
amplitude_min: 0.08
amplitude_max: 0.20
frequency_min: 1.5
frequency_max: 3.0
rotation_accel:
trigger_prob_per_second: 0.02
duration_min: 3.0
duration_max: 8.0
multiplier_min: 0.3
multiplier_max: 4.0
wounded:
duration: 1.0
blink_hz: 10.0
spawn:
invulnerability_duration: 3.0
invulnerability_brightness_start: 0.3
invulnerability_brightness_end: 0.7
invulnerability_scale_start: 0.0
invulnerability_scale_end: 1.0
safety_distance: 36.0
colors:
normal: [255, 255, 0] # Groc estrella
wounded: [255, 220, 60]
score: 100
events:
# STAR: mor al primer impacte, sense passar per wounded.
on_hit:
- action: apply_impulse
- action: destroy
on_destroy:
- action: add_score
- action: create_debris
- action: create_fireworks
+28
View File
@@ -14,6 +14,8 @@ notification:
postfx_on: "POSTPROCESSAT ACTIU" postfx_on: "POSTPROCESSAT ACTIU"
postfx_off: "POSTPROCESSAT INACTIU" postfx_off: "POSTPROCESSAT INACTIU"
locale_switched: "IDIOMA: {lang}" locale_switched: "IDIOMA: {lang}"
gamepad_connected: "{name} CONNECTAT"
gamepad_disconnected: "{name} DESCONNECTAT"
language: language:
ca: "CATALA" ca: "CATALA"
@@ -82,3 +84,29 @@ service_menu:
# Valors comuns # Valors comuns
value_on: "ACTIU" value_on: "ACTIU"
value_off: "INACTIU" value_off: "INACTIU"
# Items del submenu CONTROLS
controls_pad_p1: "MANDO JUGADOR 1"
controls_pad_p2: "MANDO JUGADOR 2"
controls_no_pad: "SENSE MANDO"
controls_define_keyboard_p1: "REDEFINIR TECLES P1"
controls_define_keyboard_p2: "REDEFINIR TECLES P2"
controls_define_gamepad_p1: "REDEFINIR BOTONS P1"
controls_define_gamepad_p2: "REDEFINIR BOTONS P2"
# Overlay modal de redefinicio (DefineInputs)
define:
title_keyboard_p1: "REDEFINIR TECLES P1"
title_keyboard_p2: "REDEFINIR TECLES P2"
title_gamepad_p1: "REDEFINIR BOTONS P1"
title_gamepad_p2: "REDEFINIR BOTONS P2"
press_key: "PREMEU UNA TECLA"
press_button: "PREMEU UN BOTO"
complete: "CONFIGURACIO COMPLETA"
no_gamepad: "CAP MANDO ASSIGNAT AL JUGADOR"
action:
left: "ESQUERRA"
right: "DRETA"
fire: "DISPARAR"
accelerate: "ACCELERAR"
start: "START"
menu: "MENU"
+28
View File
@@ -13,6 +13,8 @@ notification:
postfx_on: "POSTPROCESS ON" postfx_on: "POSTPROCESS ON"
postfx_off: "POSTPROCESS OFF" postfx_off: "POSTPROCESS OFF"
locale_switched: "LANGUAGE: {lang}" locale_switched: "LANGUAGE: {lang}"
gamepad_connected: "{name} CONNECTED"
gamepad_disconnected: "{name} DISCONNECTED"
language: language:
ca: "CATALAN" ca: "CATALAN"
@@ -81,3 +83,29 @@ service_menu:
# Common values # Common values
value_on: "ON" value_on: "ON"
value_off: "OFF" value_off: "OFF"
# Items of CONTROLS submenu
controls_pad_p1: "PLAYER 1 GAMEPAD"
controls_pad_p2: "PLAYER 2 GAMEPAD"
controls_no_pad: "NO GAMEPAD"
controls_define_keyboard_p1: "REDEFINE KEYS P1"
controls_define_keyboard_p2: "REDEFINE KEYS P2"
controls_define_gamepad_p1: "REDEFINE BUTTONS P1"
controls_define_gamepad_p2: "REDEFINE BUTTONS P2"
# Modal overlay for input redefinition (DefineInputs)
define:
title_keyboard_p1: "REDEFINE KEYS P1"
title_keyboard_p2: "REDEFINE KEYS P2"
title_gamepad_p1: "REDEFINE BUTTONS P1"
title_gamepad_p2: "REDEFINE BUTTONS P2"
press_key: "PRESS A KEY"
press_button: "PRESS A BUTTON"
complete: "CONFIGURATION COMPLETE"
no_gamepad: "NO GAMEPAD ASSIGNED TO PLAYER"
action:
left: "LEFT"
right: "RIGHT"
fire: "FIRE"
accelerate: "ACCELERATE"
start: "START"
menu: "MENU"
+17
View File
@@ -0,0 +1,17 @@
# bullet_double.shp - Bala anular (dos cercles concèntrics)
# © 2026 JailDesigner
#
# Dos octàgons concèntrics al centre (0,0):
# - Exterior: radi 4 (lleugerament més gran que la bala estàndard, radi 3)
# - Interior: radi 2 (lleugerament més petit que la bala estàndard)
# Aspecte d'anell / aura de plasma. Bounding radius natiu = 4.
name: bullet_double
scale: 1.0
center: 0, 0
# Cercle exterior (octàgon, radi 4)
polyline: 0,-4 2.83,-2.83 4,0 2.83,2.83 0,4 -2.83,2.83 -4,0 -2.83,-2.83 0,-4
# Cercle interior (octàgon, radi 2)
polyline: 0,-2 1.41,-1.41 2,0 1.41,1.41 0,2 -1.41,1.41 -2,0 -1.41,-1.41 0,-2
+28
View File
@@ -0,0 +1,28 @@
# 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
+9
View File
@@ -0,0 +1,9 @@
# char_lparen.shp - Símbol ( (parèntesi esquerre)
# Dimensions: 20×40 (blocky display)
name: char_lparen
scale: 1.0
center: 10, 20
# Arc cap a l'esquerra aproximat amb 4 trams rectes
polyline: 14,4 8,12 6,20 8,28 14,36
+9
View File
@@ -0,0 +1,9 @@
# char_rparen.shp - Símbol ) (parèntesi dret)
# Dimensions: 20×40 (blocky display)
name: char_rparen
scale: 1.0
center: 10, 20
# Arc cap a la dreta aproximat amb 4 trams rectes
polyline: 6,4 12,12 14,20 12,28 6,36
+9
View File
@@ -0,0 +1,9 @@
# char_slash.shp - Símbol / (barra)
# Dimensions: 20×40 (blocky display)
name: char_slash
scale: 1.0
center: 10, 20
# Línia diagonal de baix-esquerra a dalt-dreta
line: 4,36 16,4
+15
View File
@@ -0,0 +1,15 @@
# star_5.shp - ORNI enemic (estrella de 5 puntes, només perímetre)
# © 2026 JailDesigner
#
# Pentagrama clàssic: 5 vèrtexs exteriors (radi 20) alternant amb 5 vèrtexs
# interiors (radi 7.64 = 20/φ² ≈ proporció àuria) per donar puntes esveltes.
# Vèrtex apuntant amunt (igual que enemy_pentagon).
#
# Sense línies interiors: una única polyline que recorre el perímetre.
# Bounding radius natiu ≈ 20 (alineat amb pentagon/square/pinwheel).
name: star_5
scale: 1.0
center: 0, 0
polyline: 0,-20 4.49,-6.18 19.02,-6.18 7.27,2.36 11.76,16.18 0,7.64 -11.76,16.18 -7.27,2.36 -19.02,-6.18 -4.49,-6.18 0,-20
+5 -4
View File
@@ -7,7 +7,7 @@ metadata:
description: "Progressive difficulty curve from novice to expert" description: "Progressive difficulty curve from novice to expert"
stages: stages:
# STAGE 1: Tutorial - Mix de tots els tipus, velocitat lenta # STAGE 1: Tutorial - Mix de tots 4 tipus al 25% per mostrar-los junts
- stage_id: 1 - stage_id: 1
total_enemies: 50 total_enemies: 50
spawn_config: spawn_config:
@@ -15,9 +15,10 @@ stages:
initial_delay: 0.3 initial_delay: 0.3
spawn_interval: 0.4 spawn_interval: 0.4
enemy_distribution: enemy_distribution:
pentagon: 34 pentagon: 25
cuadrado: 33 cuadrado: 25
molinillo: 33 molinillo: 25
star: 25
difficulty_multipliers: difficulty_multipliers:
speed_multiplier: 0.7 speed_multiplier: 0.7
rotation_multiplier: 0.8 rotation_multiplier: 0.8
+3
View File
@@ -48,12 +48,15 @@ namespace Config {
int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT}; int button_right{SDL_GAMEPAD_BUTTON_DPAD_RIGHT};
int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button int button_thrust{SDL_GAMEPAD_BUTTON_WEST}; // X button
int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A button int button_shoot{SDL_GAMEPAD_BUTTON_SOUTH}; // A button
int button_start{SDL_GAMEPAD_BUTTON_START}; // Start button
int button_menu{SDL_GAMEPAD_BUTTON_BACK}; // Select/Back -> obre menu servei
}; };
struct PlayerBindings { struct PlayerBindings {
KeyboardBindings keyboard{}; KeyboardBindings keyboard{};
GamepadBindings gamepad{}; GamepadBindings gamepad{};
std::string gamepad_name; // Empty = auto-assign by index std::string gamepad_name; // Empty = auto-assign by index
std::string gamepad_path; // Prioritari sobre name per distingir mateixos models
}; };
struct AudioConfig { struct AudioConfig {
-1
View File
@@ -25,7 +25,6 @@
#include "core/defaults/physics.hpp" #include "core/defaults/physics.hpp"
#include "core/defaults/playfield.hpp" #include "core/defaults/playfield.hpp"
#include "core/defaults/rendering.hpp" #include "core/defaults/rendering.hpp"
#include "core/defaults/ship.hpp"
#include "core/defaults/starfield_parallax.hpp" #include "core/defaults/starfield_parallax.hpp"
#include "core/defaults/title.hpp" #include "core/defaults/title.hpp"
#include "core/defaults/trail.hpp" #include "core/defaults/trail.hpp"
+12 -95
View File
@@ -1,101 +1,18 @@
// enemies.hpp - Configuració per tipus d'enemic (Pentagon/Square/Molinillo), spawn i scoring // enemies.hpp - Constants tècniques compartides per al sistema d'enemics.
// © 2026 JailDesigner // © 2026 JailDesigner
//
// Tots els paràmetres jugables (physics, animation, wounded, spawn,
// behavior, colors, scoring) viuen a data/entities/<type>/<type>.yaml i
// s'accedeixen via EnemyRegistry::get(EnemyType). Aquí només queda el
// que no és per personalitzar per tipus.
#pragma once #pragma once
#include "core/defaults/entities.hpp" namespace Defaults::Enemies::Spawn {
namespace Defaults::Enemies { // Sostre de reintents al cercar una posició de spawn que respecti el
// safety_distance del tipus. No és un paràmetre jugable: és el llindar
// tècnic abans de caure a un fallback aleatori amb advertència.
constexpr int MAX_SPAWN_ATTEMPTS = 50;
// Cuerpo físico común (valores por defecto del constructor) } // namespace Defaults::Enemies::Spawn
namespace Body {
constexpr float DEFAULT_MASS = 5.0F; // Más liviano que la nave (10.0)
constexpr float RESTITUTION = 1.0F; // Rebote elástico perfecto contra paredes
constexpr float LINEAR_DAMPING = 0.0F; // Sin fricción: mantienen velocidad
constexpr float ANGULAR_DAMPING = 0.0F;
} // namespace Body
// Pentagon (esquivador - zigzag evasion)
namespace Pentagon {
constexpr float SPEED = 35.0F; // px/s (slightly slower)
constexpr float MASS = 5.0F; // Masa estándar
constexpr float ANGLE_CHANGE_PROB = 0.20F; // 20% per wall hit (frequent zigzag)
constexpr float ANGLE_CHANGE_MAX = 1.0F; // Max random angle change (rad)
constexpr float ZIGZAG_PROB_PER_SECOND = 0.8F; // Probabilidad de zigzag por segundo
constexpr float ROTATION_DELTA_MIN = 0.75F; // Min visual rotation (rad/s) [+50%]
constexpr float ROTATION_DELTA_MAX = 3.75F; // Max visual rotation (rad/s) [+50%]
constexpr const char* SHAPE_FILE = "enemy_pentagon.shp";
} // namespace Pentagon
// Square (perseguidor - tracks player)
namespace Square {
constexpr float SPEED = 40.0F; // px/s (medium speed)
constexpr float MASS = 8.0F; // Más pesado, "tanque"
constexpr float TRACKING_STRENGTH = 0.5F; // Interpolation toward player (0.0-1.0)
constexpr float TRACKING_INTERVAL = 1.0F; // Seconds between angle updates
constexpr float ROTATION_DELTA_MIN = 0.3F; // Slow rotation [+50%]
constexpr float ROTATION_DELTA_MAX = 1.5F; // [+50%]
constexpr const char* SHAPE_FILE = "enemy_square.shp";
} // namespace Square
// Molinillo (agressiu - fast straight lines, proximity spin-up)
namespace Pinwheel {
constexpr float SPEED = 50.0F; // px/s (fastest)
constexpr float MASS = 4.0F; // Más liviano, ágil
constexpr float ANGLE_CHANGE_PROB = 0.05F; // 5% per wall hit (rare direction change)
constexpr float ANGLE_CHANGE_MAX = 0.3F; // Small angle adjustments
constexpr float ROTATION_DELTA_MIN = 3.0F; // Base rotation (rad/s) [+50%]
constexpr float ROTATION_DELTA_MAX = 6.0F; // [+50%]
constexpr float ROTATION_DELTA_PROXIMITY_MULTIPLIER = 3.0F; // Spin-up multiplier when near ship
constexpr float PROXIMITY_DISTANCE = 100.0F; // Distance threshold (px)
constexpr const char* SHAPE_FILE = "enemy_pinwheel.shp";
} // namespace Pinwheel
// Animation parameters (shared)
namespace Animation {
// Palpitation
constexpr float PULSE_TRIGGER_PROB = 0.01F; // 1% chance per second
constexpr float PULSE_DURATION_MIN = 1.0F; // Min duration (seconds)
constexpr float PULSE_DURATION_MAX = 3.0F; // Max duration (seconds)
constexpr float PULSE_AMPLITUD_MIN = 0.08F; // Min scale variation
constexpr float PULSE_AMPLITUD_MAX = 0.20F; // Max scale variation
constexpr float PULSE_FREQ_MIN = 1.5F; // Min frequency (Hz)
constexpr float PULSE_FREQ_MAX = 3.0F; // Max frequency (Hz)
// Rotation acceleration
constexpr float ROTATION_ACCEL_TRIGGER_PROB = 0.02F; // 2% chance per second [4x more frequent]
constexpr float ROTATION_ACCEL_DURATION_MIN = 3.0F; // Min transition time
constexpr float ROTATION_ACCEL_DURATION_MAX = 8.0F; // Max transition time
constexpr float ROTATION_ACCEL_MULTIPLIER_MIN = 0.3F; // Min speed multiplier [more dramatic]
constexpr float ROTATION_ACCEL_MULTIPLIER_MAX = 4.0F; // Max speed multiplier [more dramatic]
} // namespace Animation
// Wounded state (entre primer impacto y explosión)
namespace Wounded {
constexpr float DURATION = 1.0F; // Segundos en estado herido antes de explotar
constexpr float BLINK_HZ = 10.0F; // Frecuencia de parpadeo color tipo ↔ dorado
} // namespace Wounded
// Spawn safety and invulnerability system
namespace Spawn {
// Safe spawn distance from player
constexpr float SAFETY_DISTANCE_MULTIPLIER = 3.0F; // 3x ship radius
constexpr float SAFETY_DISTANCE = Defaults::Entities::SHIP_RADIUS * SAFETY_DISTANCE_MULTIPLIER; // 36.0f px
constexpr int MAX_SPAWN_ATTEMPTS = 50; // Max attempts to find safe position
// Invulnerability system
constexpr float INVULNERABILITY_DURATION = 3.0F; // Seconds
constexpr float INVULNERABILITY_BRIGHTNESS_START = 0.3F; // Dim
constexpr float INVULNERABILITY_BRIGHTNESS_END = 0.7F; // Normal (same as Defaults::Brightness::ENEMIC)
constexpr float INVULNERABILITY_SCALE_START = 0.0F; // Invisible
constexpr float INVULNERABILITY_SCALE_END = 1.0F; // Full size
} // namespace Spawn
// Scoring system (puntuación per type de enemy)
namespace Scoring {
constexpr int PENTAGON_SCORE = 100; // Pentágono (esquivador, 35 px/s)
constexpr int SQUARE_SCORE = 150; // Square (perseguidor, 40 px/s)
constexpr int PINWHEEL_SCORE = 200; // Molinillo (agressiu, 50 px/s)
} // namespace Scoring
} // namespace Defaults::Enemies
+4 -3
View File
@@ -8,8 +8,9 @@ namespace Defaults::Entities {
constexpr int MAX_ORNIS = 15; constexpr int MAX_ORNIS = 15;
constexpr int MAX_BULLETS = 50; constexpr int MAX_BULLETS = 50;
constexpr float SHIP_RADIUS = 12.0F; // SHIP_RADIUS / ENEMY_RADIUS / BULLET_RADIUS han migrat: ara cada entitat
constexpr float ENEMY_RADIUS = 20.0F; // calcula el seu collision_radius com a
constexpr float BULLET_RADIUS = 3.0F; // shape.bounding_radius × shape.scale × shape.collision_factor
// a partir del seu YAML (data/entities/<name>/<name>.yaml).
} // namespace Defaults::Entities } // namespace Defaults::Entities
+1 -1
View File
@@ -29,7 +29,7 @@ namespace Defaults::Game {
// Friendly fire system // Friendly fire system
constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire constexpr bool FRIENDLY_FIRE_ENABLED = true; // Activar friendly fire
constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%) constexpr float COLLISION_BULLET_PLAYER_AMPLIFIER = 1.0F; // Hitbox exacto (100%)
constexpr float BULLET_SPEED = 700.0F; // Velocidad escalar (px/s). Pascal: 7 px/frame × 20 FPS // BULLET_SPEED migrat a data/entities/player/player.yaml (weapon.bullet_speed).
// Transición LEVEL_START (mensajes aleatorios PRE-level) // Transición LEVEL_START (mensajes aleatorios PRE-level)
constexpr float LEVEL_START_DURATION = 3.0F; // Duración total constexpr float LEVEL_START_DURATION = 3.0F; // Duración total
+5 -6
View File
@@ -14,11 +14,10 @@ namespace Defaults::Palette {
// brillantor perceptual sota el bloom (sense alterar la identitat de color). // brillantor perceptual sota el bloom (sense alterar la identitat de color).
// El canal dominant es manté a 255 a cada color per maximitzar la saturació // El canal dominant es manté a 255 a cada color per maximitzar la saturació
// visible quan el halo s'expandeix. // visible quan el halo s'expandeix.
constexpr SDL_Color SHIP = {.r = 255, .g = 255, .b = 255, .a = 255}; // Blanco neutro // Tots els colors d'entitats han migrat al seu YAML respectiu
constexpr SDL_Color BULLET = {.r = 155, .g = 255, .b = 175, .a = 255}; // Verde laser // (data/entities/<name>/<name>.yaml, secció `colors`):
constexpr SDL_Color PENTAGON = {.r = 0, .g = 255, .b = 255, .a = 255}; // Cyan pur "esquivador" // - SHIP → player.yaml
constexpr SDL_Color SQUARE = {.r = 255, .g = 0, .b = 0, .a = 255}; // Roig pur "tank" // - PENTAGON / SQUARE / PINWHEEL / WOUNDED → cada enemy.yaml
constexpr SDL_Color PINWHEEL = {.r = 255, .g = 0, .b = 255, .a = 255}; // Magenta pur "agressiu" // - BULLET → bullet.yaml
constexpr SDL_Color WOUNDED = {.r = 255, .g = 220, .b = 60, .a = 255}; // Dorado: enemigo herido
} // namespace Defaults::Palette } // namespace Defaults::Palette
+8 -25
View File
@@ -3,31 +3,15 @@
#pragma once #pragma once
namespace Defaults::Physics { // NOTA: els paràmetres del player (rotation_speed, acceleration,
// max_velocity, death_impact_factor) viuen a data/entities/player/player.yaml.
// Els paràmetres específics de la bala (mass, restitution, damping,
// impact_momentum_factor) viuen a data/entities/bullet/bullet.yaml.
// Aquest fitxer només conté els paràmetres compartits del subsistema de
// debris (explosions visuals).
constexpr float ROTATION_SPEED = 3.14F; // rad/s (~180°/s) namespace Defaults::Physics::Debris {
constexpr float ACCELERATION = 400.0F; // px/s²
constexpr float MAX_VELOCITY = 180.0F; // px/s
constexpr float FRICTION = 20.0F; // px/s²
// Bullet — impacto físico contra enemigo (impulse mass-aware).
// Model: el impulse és el moment lineal de la bala (m·v) multiplicat per
// un factor de transferència [0..1]. 1.0 = transfereix tot el moment
// (col·lisió perfectament inelàstica), 0.5 = transfereix la meitat.
namespace Bullet {
constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic
} // namespace Bullet
// Ship → enemy: impuls explícit aplicat a l'enemic en el moment exacte
// que la nau mor per col·lisió amb ell (afegit per damunt del rebot
// natural de PhysicsWorld, que ja és present però subtil amb la
// damping de la nau).
namespace Ship {
constexpr float DEATH_IMPACT_MOMENTUM_FACTOR = 0.3F;
} // namespace Ship
// Explosions (debris physics)
namespace Debris {
constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s) constexpr float SPEED_BASE = 80.0F; // Velocidad inicial (px/s)
constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s) constexpr float VARIACIO_SPEED = 40.0F; // ±variació aleatòria (px/s)
constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²) constexpr float ACCELERACIO = -60.0F; // Fricció/desacceleració (px/s²)
@@ -69,6 +53,5 @@ namespace Defaults::Physics {
// Excess above this threshold is converted to tangential linear velocity // Excess above this threshold is converted to tangential linear velocity
// Prevents "vortex trap" problem with high-rotation enemies // Prevents "vortex trap" problem with high-rotation enemies
constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s) constexpr float SPEED_ROT_MAX = 1.5F; // rad/s (~86°/s)
} // namespace Debris
} // namespace Defaults::Physics } // namespace Defaults::Physics::Debris
-33
View File
@@ -1,33 +0,0 @@
// ship.hpp - Configuració de la nau (invulnerabilitat, parpelleig)
// © 2026 JailDesigner
#pragma once
namespace Defaults::Ship {
// Invulnerabilidad post-respawn
constexpr float INVULNERABILITY_DURATION = 3.0F; // Segundos de invulnerabilidad
// Parpadeo visual durante invulnerabilidad
constexpr float BLINK_VISIBLE_TIME = 0.1F; // Tiempo visible (segundos)
constexpr float BLINK_INVISIBLE_TIME = 0.1F; // Tiempo invisible (segundos)
// Frecuencia total: 0.2s/ciclo = 5 Hz (~15 parpadeos en 3s)
// Cuerpo físico
constexpr float MASS = 10.0F; // Masa de referencia para choques
constexpr float RESTITUTION = 0.6F; // Rebote moderado contra paredes
constexpr float LINEAR_DAMPING = 1.5F; // Fricción exponencial (s⁻¹)
constexpr float ANGULAR_DAMPING = 0.0F; // Rotación 100% por input (no inercial)
// Empuje visual: escala proporcional a la velocidad (0..200 px/s → 1.0..1.5)
// Mantiene la sensación del Pascal original.
constexpr float VISUAL_PUSH_DIVISOR = 33.33F; // SPEED / DIVISOR = empuje visual
constexpr float VISUAL_SCALE_DIVISOR = 12.0F; // SCALE = 1 + (PUSH / DIVISOR)
// Estat "ferit": entre primera col·lisió amb enemic i recuperació o segona col·lisió mortal.
namespace Hurt {
constexpr float DURATION = 15.0F; // Segons en estat ferit (provisional)
constexpr float BLINK_HZ = 10.0F; // Freqüència parpelleig color normal ↔ ferit
} // namespace Hurt
} // namespace Defaults::Ship
+1 -1
View File
@@ -7,7 +7,7 @@ namespace Defaults::Trail {
constexpr int POOL_SIZE = 200; constexpr int POOL_SIZE = 200;
constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de Physics::MAX_VELOCITY (180) constexpr float SPEED_THRESHOLD_PX_S = 54.0F; // 30% de player.yaml::physics.max_velocity (180 px/s)
constexpr float EMIT_INTERVAL_S = 0.04F; // ~25 Hz nominal constexpr float EMIT_INTERVAL_S = 0.04F; // ~25 Hz nominal
constexpr float EMIT_JITTER_S = 0.015F; // ±15 ms al cooldown constexpr float EMIT_JITTER_S = 0.015F; // ±15 ms al cooldown
constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement constexpr float POSITION_JITTER_PX = 2.5F; // jitter al punt de naixement
+56
View File
@@ -0,0 +1,56 @@
// entity_loader.cpp - Implementació del carregador d'entitats YAML
// © 2026 JailDesigner
#include "core/entities/entity_loader.hpp"
#include <cstdint>
#include <exception>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include "core/resources/resource_helper.hpp"
namespace Entities {
std::unordered_map<std::string, std::shared_ptr<fkyaml::node>> EntityLoader::cache;
auto EntityLoader::load(const std::string& name) -> std::shared_ptr<fkyaml::node> {
// Cache hit
auto it = cache.find(name);
if (it != cache.end()) {
std::cout << "[EntityLoader] Cache hit: " << name << '\n';
return it->second;
}
const std::string PATH = "entities/" + name + "/" + name + ".yaml";
std::vector<uint8_t> data = Resource::Helper::loadFile(PATH);
if (data.empty()) {
std::cerr << "[EntityLoader] Error: no s'ha pogut load " << PATH << '\n';
return nullptr;
}
try {
std::string yaml_content(data.begin(), data.end());
std::stringstream stream(yaml_content);
auto node = std::make_shared<fkyaml::node>(fkyaml::node::deserialize(stream));
std::cout << "[EntityLoader] Carregat: " << PATH << '\n';
cache[name] = node;
return node;
} catch (const std::exception& e) {
std::cerr << "[EntityLoader] Excepció parsejant " << PATH << ": " << e.what() << '\n';
return nullptr;
}
}
void EntityLoader::clearCache() {
std::cout << "[EntityLoader] Netejant caché (" << cache.size() << " entitats)" << '\n';
cache.clear();
}
auto EntityLoader::getCacheSize() -> size_t { return cache.size(); }
} // namespace Entities
+38
View File
@@ -0,0 +1,38 @@
// entity_loader.hpp - Carregador genèric de descriptors d'entitats en YAML
// © 2026 JailDesigner
//
// Cada entitat viu a `data/entities/<name>/<name>.yaml` (mateix patró que el
// projecte germà aee_arcade). Aquest loader resol el path, llegeix del
// resource pack via Resource::Helper, parseja amb fkyaml i cacheja el node
// per evitar relectures. Retorna nullptr en cas d'error (el caller decideix
// si abortar).
#pragma once
#include <memory>
#include <string>
#include <unordered_map>
#include "external/fkyaml_node.hpp"
namespace Entities {
class EntityLoader {
public:
EntityLoader() = delete; // tot estàtic
// Carrega el descriptor d'una entitat per nom (ex. "player" →
// "entities/player/player.yaml"). Retorna nullptr si no es pot
// carregar o parsejar. Cachejat per nom.
static auto load(const std::string& name) -> std::shared_ptr<fkyaml::node>;
// Buidar caché (útil per debug/recàrrega).
static void clearCache();
[[nodiscard]] static auto getCacheSize() -> size_t;
private:
static std::unordered_map<std::string, std::shared_ptr<fkyaml::node>> cache;
};
} // namespace Entities
+2 -1
View File
@@ -53,7 +53,8 @@ namespace Graphics {
// Cache and return // Cache and return
std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName() std::cout << "[ShapeLoader] Carregat: " << normalized << " (" << shape->getName()
<< ", " << shape->getNumPrimitives() << " primitives)" << '\n'; << ", " << shape->getNumPrimitives() << " primitives, bounding_radius="
<< shape->getBoundingRadius() << ")" << '\n';
cache[filename] = shape; cache[filename] = shape;
return shape; return shape;
+7 -1
View File
@@ -47,7 +47,7 @@ namespace Graphics {
} }
// Cargar símbolos // Cargar símbolos
const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?"}; const std::string SYMBOLS[] = {".", ",", "-", ":", "!", "?", "/", "(", ")"};
for (const auto& sym : SYMBOLS) { for (const auto& sym : SYMBOLS) {
char c = sym[0]; char c = sym[0];
std::string filename = getShapeFilename(c); std::string filename = getShapeFilename(c);
@@ -164,6 +164,12 @@ namespace Graphics {
return "font/char_exclamation.shp"; return "font/char_exclamation.shp";
case '?': case '?':
return "font/char_question.shp"; return "font/char_question.shp";
case '/':
return "font/char_slash.shp";
case '(':
return "font/char_lparen.shp";
case ')':
return "font/char_rparen.shp";
case ' ': case ' ':
return ""; // Espai es maneja sin load shape return ""; // Espai es maneja sin load shape
+1 -1
View File
@@ -21,7 +21,7 @@ namespace Graphics {
// Renderizar string completo // Renderizar string completo
// - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':', // - text: cadena a renderizar (soporta: A-Z, a-z, 0-9, '.', ',', '-', ':',
// '!', '?', ' ') // '!', '?', '/', '(', ')', ' ')
// - position: posición inicial (esquina superior izquierda) // - position: posición inicial (esquina superior izquierda)
// - scale: factor de scale (1.0 = 20×40 px por carácter) // - scale: factor de scale (1.0 = 20×40 px por carácter)
// - spacing: espacio entre caracteres en píxeles (a scale 1.0) // - spacing: espacio entre caracteres en píxeles (a scale 1.0)
+405
View File
@@ -0,0 +1,405 @@
// define_inputs.cpp - Implementacio de l'overlay modal de redefinicio
// © 2026 JailDesigner
#include "core/input/define_inputs.hpp"
#include <algorithm>
#include <format>
#include <string>
#include <vector>
#include "core/audio/audio.hpp"
#include "core/defaults/service_menu.hpp"
#include "core/input/input.hpp"
#include "core/locale/locale.hpp"
#include "core/types.hpp"
#include "game/config_yaml.hpp"
namespace {
constexpr float CANVAS_W = 1280.0F;
constexpr float CANVAS_H = 720.0F;
// Llindar de trigger per a edge-detect L2/R2 com a boto virtual.
constexpr Sint16 TRIGGER_THRESHOLD = 16384;
// Codis virtuals per als triggers (consistents amb input_types.cpp).
constexpr int TRIGGER_L2_VIRTUAL = 100;
constexpr int TRIGGER_R2_VIRTUAL = 101;
// Durada del missatge de confirmacio abans de tancar-se.
constexpr float COMPLETE_DISPLAY_S = 1.5F;
// Llindar dpad als axis sticks: no es captura per evitar conflicte amb el
// moviment LEFT/RIGHT/UP/DOWN (que es presuposen no redefinibles al mando).
constexpr Sint16 STICK_THRESHOLD = 16384;
// Crida pushRect amb un SDL_Color (les components s'escalen a [0..1]).
void fillRect(Rendering::Renderer* renderer, float x, float y, float w, float h, SDL_Color color) {
renderer->pushRect(x, y, w, h, static_cast<float>(color.r) / 255.0F, static_cast<float>(color.g) / 255.0F, static_cast<float>(color.b) / 255.0F, static_cast<float>(color.a) / 255.0F);
}
auto titleKey(System::DefineInputs::Mode mode, System::DefineInputs::Player player) -> std::string {
const bool IS_KB = (mode == System::DefineInputs::Mode::KEYBOARD);
const bool IS_P1 = (player == System::DefineInputs::Player::P1);
if (IS_KB && IS_P1) {
return "define.title_keyboard_p1";
}
if (IS_KB) {
return "define.title_keyboard_p2";
}
if (IS_P1) {
return "define.title_gamepad_p1";
}
return "define.title_gamepad_p2";
}
// Scancodes que MAI capturem com a binding (reservats per a navegacio o
// global hotkeys). Tornen true → handleEvent les deixa passar al pipeline
// global perque facin la seua feina (ESC obre el prompt d'eixida, F1-F12
// son hotkeys de sistema, RETURN/BACKSPACE/TAB son navegacio).
auto isReservedScancode(SDL_Scancode sc) -> bool {
if (sc == SDL_SCANCODE_ESCAPE) {
return true;
}
if (sc >= SDL_SCANCODE_F1 && sc <= SDL_SCANCODE_F12) {
return true;
}
if (sc == SDL_SCANCODE_RETURN || sc == SDL_SCANCODE_KP_ENTER) {
return true;
}
if (sc == SDL_SCANCODE_BACKSPACE || sc == SDL_SCANCODE_TAB) {
return true;
}
return false;
}
// Conversio sense pèrdua de SDL_Scancode → int per a comparacions
// homogenies dins de sequence_ (que guarda codis de tots dos modes).
auto scancodeToInt(SDL_Scancode sc) -> int {
return static_cast<int>(sc);
}
} // namespace
namespace System {
std::unique_ptr<DefineInputs> DefineInputs::instance;
void DefineInputs::init(Rendering::Renderer* renderer) {
if (!instance) {
instance = std::unique_ptr<DefineInputs>(new DefineInputs(renderer));
}
}
void DefineInputs::destroy() { instance.reset(); }
auto DefineInputs::get() -> DefineInputs* { return instance.get(); }
DefineInputs::DefineInputs(Rendering::Renderer* renderer)
: renderer_(renderer),
text_(renderer) {}
auto DefineInputs::isActive() const -> bool {
return phase_ != Phase::INACTIVE;
}
auto DefineInputs::begin(Mode mode, Player player) -> bool {
if (mode == Mode::GAMEPAD) {
// Requereix un pad assignat al jugador.
const auto* input = Input::get();
if (input == nullptr) {
return false;
}
const int IDX = (player == Player::P1) ? 0 : 1;
if (input->getPlayerGamepad(IDX) == nullptr) {
return false;
}
}
mode_ = mode;
player_ = player;
index_ = 0;
complete_timer_s_ = 0.0F;
l2_was_pressed_ = false;
r2_was_pressed_ = false;
buildSequence();
phase_ = Phase::CAPTURING;
return true;
}
void DefineInputs::cancel() {
phase_ = Phase::INACTIVE;
sequence_.clear();
index_ = 0;
complete_timer_s_ = 0.0F;
}
void DefineInputs::buildSequence() {
sequence_.clear();
if (mode_ == Mode::KEYBOARD) {
// Teclat: LEFT, RIGHT, FIRE (SHOOT), ACCELERATE (THRUST)
sequence_.push_back({.action_label_key = "define.action.left", .action = InputAction::LEFT, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.right", .action = InputAction::RIGHT, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1});
} else {
// Mando: FIRE, ACCELERATE, START, MENU
sequence_.push_back({.action_label_key = "define.action.fire", .action = InputAction::SHOOT, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.accelerate", .action = InputAction::THRUST, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.start", .action = InputAction::START, .captured = -1});
sequence_.push_back({.action_label_key = "define.action.menu", .action = InputAction::MENU, .captured = -1});
}
}
auto DefineInputs::isInUse(int code) const -> bool {
return std::ranges::any_of(sequence_, [code](const Step& s) {
return s.captured == code;
});
}
void DefineInputs::captureAndAdvance(int code) {
if (index_ >= sequence_.size()) {
return;
}
if (isInUse(code)) {
return; // Duplicat dins de la sessio: rebutgem silenciosament
}
sequence_[index_].captured = code;
++index_;
if (auto* audio = Audio::get(); audio != nullptr) {
audio->playSound(Defaults::ServiceMenu::ACCEPT_SOUND, Audio::Group::INTERFACE);
}
if (index_ >= sequence_.size()) {
persistAndComplete();
}
}
void DefineInputs::persistAndComplete() {
auto& cfg = (player_ == Player::P1)
? ConfigYaml::engine_config.player1
: ConfigYaml::engine_config.player2;
if (mode_ == Mode::KEYBOARD) {
for (const Step& s : sequence_) {
switch (s.action) {
case InputAction::LEFT:
cfg.keyboard.key_left = static_cast<SDL_Scancode>(s.captured);
break;
case InputAction::RIGHT:
cfg.keyboard.key_right = static_cast<SDL_Scancode>(s.captured);
break;
case InputAction::SHOOT:
cfg.keyboard.key_shoot = static_cast<SDL_Scancode>(s.captured);
break;
case InputAction::THRUST:
cfg.keyboard.key_thrust = static_cast<SDL_Scancode>(s.captured);
break;
default:
break; // START / MENU no es redefineixen al teclat
}
}
} else {
for (const Step& s : sequence_) {
switch (s.action) {
case InputAction::SHOOT:
cfg.gamepad.button_shoot = s.captured;
break;
case InputAction::THRUST:
cfg.gamepad.button_thrust = s.captured;
break;
case InputAction::START:
cfg.gamepad.button_start = s.captured;
break;
case InputAction::MENU:
cfg.gamepad.button_menu = s.captured;
break;
default:
break; // LEFT / RIGHT no es redefineixen al mando
}
}
}
// Aplicar canvis al runtime de l'Input i persistir a disc.
if (auto* input = Input::get(); input != nullptr) {
if (player_ == Player::P1) {
input->applyPlayer1Bindings(ConfigYaml::engine_config.player1);
} else {
input->applyPlayer2Bindings(ConfigYaml::engine_config.player2);
}
}
ConfigYaml::saveToFile();
phase_ = Phase::COMPLETE;
complete_timer_s_ = COMPLETE_DISPLAY_S;
}
void DefineInputs::update(float delta_time) {
if (phase_ != Phase::COMPLETE) {
return;
}
complete_timer_s_ -= delta_time;
if (complete_timer_s_ <= 0.0F) {
cancel();
}
}
void DefineInputs::processTrigger(int virtual_button, bool& was_pressed, bool now) {
if (now && !was_pressed) {
captureAndAdvance(virtual_button);
}
was_pressed = now;
}
auto DefineInputs::handleKeyboardEvent(const SDL_Event& event) -> bool {
if (event.type != SDL_EVENT_KEY_DOWN) {
return true; // Empassem la resta sense fer res
}
const SDL_Scancode SC = event.key.scancode;
if (isReservedScancode(SC)) {
// ESC, F1-F12, RETURN, BACKSPACE, TAB es deixen passar al pipeline
// global (ESC obre el prompt d'eixida; F1-F12 hotkeys, etc.).
return false;
}
captureAndAdvance(scancodeToInt(SC));
return true;
}
auto DefineInputs::handleGamepadEvent(const SDL_Event& event) -> bool {
// KEY_DOWN no es per al rebind de mando: deixem que el global el
// gestioni (ex. ESC → prompt d'eixida, F12 → tanca menu, etc.).
if (event.type == SDL_EVENT_KEY_DOWN) {
return false;
}
// Filtrar events al pad del jugador actiu.
const auto* input = Input::get();
if (input == nullptr) {
return true;
}
const int IDX = (player_ == Player::P1) ? 0 : 1;
auto pad = input->getPlayerGamepad(IDX);
if (!pad) {
return true;
}
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
if (event.gbutton.which != pad->instance_id) {
return true;
}
captureAndAdvance(static_cast<int>(event.gbutton.button));
return true;
}
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
if (event.gaxis.which != pad->instance_id) {
return true;
}
const auto AXIS = static_cast<SDL_GamepadAxis>(event.gaxis.axis);
const Sint16 VAL = event.gaxis.value;
if (AXIS == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) {
processTrigger(TRIGGER_L2_VIRTUAL, l2_was_pressed_, VAL >= TRIGGER_THRESHOLD);
} else if (AXIS == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
processTrigger(TRIGGER_R2_VIRTUAL, r2_was_pressed_, VAL >= TRIGGER_THRESHOLD);
}
// Sticks LEFTX/LEFTY/RIGHTX/RIGHTY: ignorats (no son redefinibles).
(void)STICK_THRESHOLD;
return true;
}
return true;
}
auto DefineInputs::handleEvent(const SDL_Event& event) -> bool {
if (phase_ == Phase::INACTIVE) {
return false;
}
// SDL_EVENT_QUIT i WINDOW_CLOSE_REQUESTED han de poder tancar la
// finestra encara que el modal estiga obert; els passem al pipeline.
if (event.type == SDL_EVENT_QUIT ||
event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
return false;
}
if (phase_ == Phase::COMPLETE) {
// Mentre mostrem el missatge OK, empassem la resta d'events sense
// capturar perque l'usuari no puga avançar accions sense voler.
return true;
}
if (mode_ == Mode::KEYBOARD) {
return handleKeyboardEvent(event);
}
return handleGamepadEvent(event);
}
void DefineInputs::draw() const {
if (phase_ == Phase::INACTIVE) {
return;
}
using namespace Defaults::ServiceMenu;
// Caixa centrada, dimensions fixes (no depen del contingut a redefinir).
constexpr float BOX_W = 560.0F;
constexpr float BOX_H = 280.0F;
const float BOX_X = (CANVAS_W - BOX_W) * 0.5F;
const float BOX_Y = (CANVAS_H - BOX_H) * 0.5F;
// Fons + brackets als 4 cantons (estil HUD del menu de servei).
fillRect(renderer_, BOX_X, BOX_Y, BOX_W, BOX_H, BG_COLOR);
const auto T = static_cast<float>(CORNER_THICKNESS);
const auto AH = static_cast<float>(CORNER_ARM_H);
const auto AV = static_cast<float>(CORNER_ARM_V);
fillRect(renderer_, BOX_X, BOX_Y, AH, T, CORNER_COLOR);
fillRect(renderer_, BOX_X, BOX_Y, T, AV, CORNER_COLOR);
fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y, AH, T, CORNER_COLOR);
fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y, T, AV, CORNER_COLOR);
fillRect(renderer_, BOX_X, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR);
fillRect(renderer_, BOX_X, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR);
fillRect(renderer_, BOX_X + BOX_W - AH, BOX_Y + BOX_H - T, AH, T, CORNER_COLOR);
fillRect(renderer_, BOX_X + BOX_W - T, BOX_Y + BOX_H - AV, T, AV, CORNER_COLOR);
const std::string TITLE = Locale::get().text(titleKey(mode_, player_));
const float TITLE_W = Graphics::VectorText::getTextWidth(TITLE, TITLE_SCALE, TEXT_SPACING);
const float TITLE_X = BOX_X + ((BOX_W - TITLE_W) * 0.5F);
const float TITLE_Y = BOX_Y + 26.0F;
text_.render(TITLE, Vec2{.x = TITLE_X, .y = TITLE_Y}, TITLE_SCALE, TEXT_SPACING, 1.0F, TITLE_COLOR);
if (phase_ == Phase::COMPLETE) {
const std::string OK = Locale::get().text("define.complete");
constexpr float OK_SCALE = 0.7F;
const float OK_W = Graphics::VectorText::getTextWidth(OK, OK_SCALE, TEXT_SPACING);
const float OK_X = BOX_X + ((BOX_W - OK_W) * 0.5F);
const float OK_Y = BOX_Y + (BOX_H * 0.5F) - 10.0F;
constexpr SDL_Color OK_COLOR{.r = 120, .g = 255, .b = 140, .a = 255};
text_.render(OK, Vec2{.x = OK_X, .y = OK_Y}, OK_SCALE, TEXT_SPACING, 1.0F, OK_COLOR);
return;
}
// Instruccio (premeu tecla / boto) + accio actual + progres.
const std::string PROMPT = Locale::get().text(
mode_ == Mode::KEYBOARD ? "define.press_key" : "define.press_button");
const float PROMPT_W = Graphics::VectorText::getTextWidth(PROMPT, ITEM_SCALE, TEXT_SPACING);
const float PROMPT_X = BOX_X + ((BOX_W - PROMPT_W) * 0.5F);
const float PROMPT_Y = BOX_Y + 86.0F;
text_.render(PROMPT, Vec2{.x = PROMPT_X, .y = PROMPT_Y}, ITEM_SCALE, TEXT_SPACING, 1.0F, SUBTITLE_COLOR);
if (index_ < sequence_.size()) {
const std::string ACTION = Locale::get().text(sequence_[index_].action_label_key);
constexpr float ACTION_SCALE = 0.9F;
const float ACTION_W = Graphics::VectorText::getTextWidth(ACTION, ACTION_SCALE, TEXT_SPACING);
const float ACTION_X = BOX_X + ((BOX_W - ACTION_W) * 0.5F);
const float ACTION_Y = BOX_Y + 130.0F;
text_.render(ACTION, Vec2{.x = ACTION_X, .y = ACTION_Y}, ACTION_SCALE, TEXT_SPACING, 1.0F, CURSOR_COLOR);
}
const std::string PROGRESS = std::format("{}/{}", index_ + 1, sequence_.size());
constexpr float PROG_SCALE = 0.4F;
const float PROG_W = Graphics::VectorText::getTextWidth(PROGRESS, PROG_SCALE, TEXT_SPACING);
const float PROG_X = BOX_X + ((BOX_W - PROG_W) * 0.5F);
const float PROG_Y = BOX_Y + 200.0F;
text_.render(PROGRESS, Vec2{.x = PROG_X, .y = PROG_Y}, PROG_SCALE, TEXT_SPACING, 1.0F, LABEL_COLOR);
}
} // namespace System
+107
View File
@@ -0,0 +1,107 @@
// define_inputs.hpp - Overlay modal de redefinici de controls (singleton)
// © 2026 JailDesigner
//
// Sub-mòdul inspirat en aee_arcade/source/core/input/define_buttons. Quan el
// menú de servei dispara una acció "Redefinir tecles/botons P1/P2", aquest
// singleton pren el control: pinta una caixa central, captura events SDL i
// avança per una seqüència fixa d'accions, persistint les noves assignacions
// a config.yaml en acabar.
//
// Cicle de vida:
// 1. begin(mode, player) → construeix la seqüència (4 passos) i activa
// l'overlay. Per a GAMEPAD, retorna false si el jugador no té pad.
// 2. handleEvent() captura el següent event vàlid; ESC cancel·la sense
// desar; duplicats dins de la sessió es rebutgen silenciosament.
// 3. Quan la seqüència es completa, persistim a engine_config + saveToFile,
// reapliquem els bindings i mostrem un missatge "OK" durant 1.5 s
// abans d'auto-tancar-se.
//
// El routing d'events es fa des de GlobalEvents::handle: mentre isActive()
// retorna true, tots els events SDL es desvien aquí i no arriben al joc ni
// al menú de servei.
#pragma once
#include <SDL3/SDL.h>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "core/graphics/vector_text.hpp"
#include "core/input/input_types.hpp"
#include "core/rendering/render_context.hpp"
namespace System {
class DefineInputs {
public:
enum class Mode : std::uint8_t { KEYBOARD,
GAMEPAD };
enum class Player : std::uint8_t { P1,
P2 };
static void init(Rendering::Renderer* renderer);
static void destroy();
[[nodiscard]] static auto get() -> DefineInputs*;
// Comença la sessió. Retorna false per a GAMEPAD si el jugador no té
// cap pad assignat (el caller hauria de notificar a l'usuari abans).
auto begin(Mode mode, Player player) -> bool;
void cancel();
[[nodiscard]] auto isActive() const -> bool;
void update(float delta_time);
void draw() const;
// Retorna true si l'event s'ha consumit (és a dir, mentre l'overlay
// és actiu sempre consumeix tot per evitar passages al joc o menú).
auto handleEvent(const SDL_Event& event) -> bool;
private:
explicit DefineInputs(Rendering::Renderer* renderer);
enum class Phase : std::uint8_t {
INACTIVE,
CAPTURING,
COMPLETE, // mostra missatge OK breu abans d'auto-cancel
};
struct Step {
std::string action_label_key; // p.ex. "define.action.left"
InputAction action; // mapeig a la struct PlayerBindings
int captured{-1}; // scancode o button code; -1 = sense capturar
};
void buildSequence();
[[nodiscard]] auto isInUse(int code) const -> bool;
void captureAndAdvance(int code);
void persistAndComplete();
// Handlers especialitzats segons mode_.
auto handleKeyboardEvent(const SDL_Event& event) -> bool;
auto handleGamepadEvent(const SDL_Event& event) -> bool;
// Edge-detect per als triggers L2/R2 com a botons virtuals.
void processTrigger(int virtual_button, bool& was_pressed, bool now);
Rendering::Renderer* renderer_;
Graphics::VectorText text_;
Phase phase_{Phase::INACTIVE};
Mode mode_{Mode::KEYBOARD};
Player player_{Player::P1};
std::vector<Step> sequence_;
std::size_t index_{0};
float complete_timer_s_{0.0F};
// Estat d'edge-detect dels triggers durant la sessió GAMEPAD.
bool l2_was_pressed_{false};
bool r2_was_pressed_{false};
static std::unique_ptr<DefineInputs> instance;
};
} // namespace System
+84 -18
View File
@@ -8,6 +8,10 @@
#include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator #include <unordered_map> // Para unordered_map, _Node_iterator, operator==, _Node_iterator_base, _Node_const_iterator
#include <utility> // Para move #include <utility> // Para move
#include "core/locale/locale.hpp"
#include "core/system/notifier.hpp"
#include "core/utils/string_utils.hpp"
// Singleton // Singleton
Input* Input::instance = nullptr; Input* Input::instance = nullptr;
@@ -373,9 +377,25 @@ void Input::update() {
// --- MANDOS --- // --- MANDOS ---
for (const auto& gamepad : gamepads_) { for (const auto& gamepad : gamepads_) {
// LEFT i RIGHT NO son redefinibles al mando (assumits dpad o stick).
// Llegim el left stick X i el fusionem amb l'estat del dpad: qualsevol
// de les dos fonts activa l'accio. Llindar AXIS_THRESHOLD (30000).
const Sint16 STICK_X = SDL_GetGamepadAxis(gamepad->pad, SDL_GAMEPAD_AXIS_LEFTX);
const bool STICK_LEFT = STICK_X < -AXIS_THRESHOLD;
const bool STICK_RIGHT = STICK_X > AXIS_THRESHOLD;
for (auto& binding : gamepad->bindings) { for (auto& binding : gamepad->bindings) {
bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0; bool button_is_down_now = static_cast<int>(SDL_GetGamepadButton(gamepad->pad, static_cast<SDL_GamepadButton>(binding.second.button))) != 0;
// Per a LEFT/RIGHT, fer un OR amb el stick X. La resta d'accions
// (THRUST/SHOOT/START/MENU) ignoren el stick aqui — si es vol
// dispar amb trigger L2/R2 cal binding amb codi 100/101.
if (binding.first == Action::LEFT) {
button_is_down_now = button_is_down_now || STICK_LEFT;
} else if (binding.first == Action::RIGHT) {
button_is_down_now = button_is_down_now || STICK_RIGHT;
}
// El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo // El estado .is_held del fotograma anterior nos sirve para saber si es un pulso nuevo
binding.second.just_pressed = button_is_down_now && !binding.second.is_held; binding.second.just_pressed = button_is_down_now && !binding.second.is_held;
binding.second.is_held = button_is_down_now; binding.second.is_held = button_is_down_now;
@@ -407,6 +427,16 @@ auto Input::addGamepad(int device_index) -> std::string {
auto name = gamepad->name; auto name = gamepad->name;
std::cout << "Gamepad connected (" << name << ")" << '\n'; std::cout << "Gamepad connected (" << name << ")" << '\n';
gamepads_.push_back(std::move(gamepad)); gamepads_.push_back(std::move(gamepad));
// Toast a pantalla. Pot ser nullptr durant discoverGamepads() inicial
// (l'Input::init() es crida abans que el Director instanciï el Notifier).
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(localeSubstitute(
Locale::get().text("notification.gamepad_connected"),
"{name}",
Utils::toUpperAscii(name)));
}
return name + " CONNECTED"; return name + " CONNECTED";
} }
@@ -419,6 +449,14 @@ auto Input::removeGamepad(SDL_JoystickID id) -> std::string {
std::string name = (*it)->name; std::string name = (*it)->name;
std::cout << "Gamepad disconnected (" << name << ")" << '\n'; std::cout << "Gamepad disconnected (" << name << ")" << '\n';
gamepads_.erase(it); gamepads_.erase(it);
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(localeSubstitute(
Locale::get().text("notification.gamepad_disconnected"),
"{name}",
Utils::toUpperAscii(name)));
}
return name + " DISCONNECTED"; return name + " DISCONNECTED";
} }
std::cerr << "No se encontró el gamepad con ID " << id << '\n'; std::cerr << "No se encontró el gamepad con ID " << id << '\n';
@@ -465,6 +503,33 @@ auto Input::findAvailableGamepadByName(const std::string& gamepad_name) -> std::
// ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ========== // ========== MÉTODOS ESPECÍFICOS POR JUGADOR (ORNI) ==========
// Cerca el gamepad assignat a un jugador. Prioritat path > name. Si els
// dos camps venen buits o no n'hi ha cap match retornem nullptr (sense
// mando explicit). L'autoassignacio inicial es resol al boot.
auto Input::resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad> {
if (gamepads_.empty()) {
return nullptr;
}
if (!bindings.gamepad_path.empty()) {
for (const auto& pad : gamepads_) {
if (pad && pad->path == bindings.gamepad_path) {
return pad;
}
}
}
if (!bindings.gamepad_name.empty()) {
for (const auto& pad : gamepads_) {
if (pad && pad->name == bindings.gamepad_name) {
return pad;
}
}
}
return nullptr;
}
// Aplica configuración de controles del player 1 // Aplica configuración de controles del player 1
void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) { void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
// 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico) // 1. Aplicar bindings de teclado (NO usar bindKey, llenar mapa específico)
@@ -474,15 +539,8 @@ void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
player1_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot; player1_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start; player1_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
// 2. Encontrar gamepad por nombre (o usar primer gamepad como fallback) // 2. Resoldre gamepad per path/name
std::shared_ptr<Gamepad> gamepad = nullptr; std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
if (bindings.gamepad_name.empty()) {
// Fallback: usar primer gamepad disponible
gamepad = (!gamepads_.empty()) ? gamepads_[0] : nullptr;
} else {
// Buscar por nombre
gamepad = findAvailableGamepadByName(bindings.gamepad_name);
}
if (!gamepad) { if (!gamepad) {
player1_gamepad_ = nullptr; player1_gamepad_ = nullptr;
@@ -494,6 +552,8 @@ void Input::applyPlayer1Bindings(const Config::PlayerBindings& bindings) {
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right; gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust; gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot; gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot;
gamepad->bindings[Action::START].button = bindings.gamepad.button_start;
gamepad->bindings[Action::MENU].button = bindings.gamepad.button_menu;
// 4. Cachear referencia // 4. Cachear referencia
player1_gamepad_ = gamepad; player1_gamepad_ = gamepad;
@@ -508,15 +568,8 @@ void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) {
player2_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot; player2_keyboard_bindings_[Action::SHOOT].scancode = bindings.keyboard.key_shoot;
player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start; player2_keyboard_bindings_[Action::START].scancode = bindings.keyboard.key_start;
// 2. Encontrar gamepad por nombre (o usar segundo gamepad como fallback) // 2. Resoldre gamepad per path/name
std::shared_ptr<Gamepad> gamepad = nullptr; std::shared_ptr<Gamepad> gamepad = resolvePlayerGamepad(bindings);
if (bindings.gamepad_name.empty()) {
// Fallback: usar segundo gamepad disponible
gamepad = (gamepads_.size() > 1) ? gamepads_[1] : nullptr;
} else {
// Buscar por nombre
gamepad = findAvailableGamepadByName(bindings.gamepad_name);
}
if (!gamepad) { if (!gamepad) {
player2_gamepad_ = nullptr; player2_gamepad_ = nullptr;
@@ -528,6 +581,8 @@ void Input::applyPlayer2Bindings(const Config::PlayerBindings& bindings) {
gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right; gamepad->bindings[Action::RIGHT].button = bindings.gamepad.button_right;
gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust; gamepad->bindings[Action::THRUST].button = bindings.gamepad.button_thrust;
gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot; gamepad->bindings[Action::SHOOT].button = bindings.gamepad.button_shoot;
gamepad->bindings[Action::START].button = bindings.gamepad.button_start;
gamepad->bindings[Action::MENU].button = bindings.gamepad.button_menu;
// 4. Cachear referencia // 4. Cachear referencia
player2_gamepad_ = gamepad; player2_gamepad_ = gamepad;
@@ -555,6 +610,17 @@ auto Input::checkActionPlayer1(Action action, bool repeat) -> bool {
return keyboard_active || gamepad_active; return keyboard_active || gamepad_active;
} }
// Retorna el pad assignat (0=P1, 1=P2). Pot ser nullptr.
auto Input::getPlayerGamepad(int player_index) const -> std::shared_ptr<Input::Gamepad> {
if (player_index == 0) {
return player1_gamepad_;
}
if (player_index == 1) {
return player2_gamepad_;
}
return nullptr;
}
// Consulta de input para player 2 // Consulta de input para player 2
auto Input::checkActionPlayer2(Action action, bool repeat) -> bool { auto Input::checkActionPlayer2(Action action, bool repeat) -> bool {
// Comprobar teclado con el mapa específico de P2 // Comprobar teclado con el mapa específico de P2
+8 -1
View File
@@ -62,7 +62,9 @@ class Input {
{Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}}, {Action::LEFT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_LEFT)}},
{Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}}, {Action::RIGHT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_DPAD_RIGHT)}},
{Action::THRUST, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}}, {Action::THRUST, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_WEST)}},
{Action::SHOOT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH)}}} {} {Action::SHOOT, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_SOUTH)}},
{Action::START, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_START)}},
{Action::MENU, ButtonState{.button = static_cast<int>(SDL_GAMEPAD_BUTTON_BACK)}}} {}
~Gamepad() { ~Gamepad() {
if (pad != nullptr) { if (pad != nullptr) {
@@ -107,6 +109,10 @@ class Input {
auto checkActionPlayer1(Action action, bool repeat = true) -> bool; auto checkActionPlayer1(Action action, bool repeat = true) -> bool;
auto checkActionPlayer2(Action action, bool repeat = true) -> bool; auto checkActionPlayer2(Action action, bool repeat = true) -> bool;
// Accés al gamepad assignat per jugador (0=P1, 1=P2). nullptr si no n'hi
// ha cap d'assignat o connectat. Usat per la UI de redefinició de botons.
[[nodiscard]] auto getPlayerGamepad(int player_index) const -> std::shared_ptr<Gamepad>;
// Check if any player pressed any action from a list // Check if any player pressed any action from a list
auto checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool; auto checkAnyPlayerAction(const std::span<const InputAction>& actions, bool repeat = DO_NOT_ALLOW_REPEAT) -> bool;
@@ -142,6 +148,7 @@ class Input {
auto removeGamepad(SDL_JoystickID id) -> std::string; auto removeGamepad(SDL_JoystickID id) -> std::string;
void addGamepadMappingsFromFile(); void addGamepadMappingsFromFile();
void discoverGamepads(); void discoverGamepads();
auto resolvePlayerGamepad(const Config::PlayerBindings& bindings) -> std::shared_ptr<Gamepad>;
// --- Variables miembro --- // --- Variables miembro ---
static Input* instance; // Instancia única del singleton static Input* instance; // Instancia única del singleton
+4
View File
@@ -6,6 +6,8 @@ const std::unordered_map<InputAction, std::string> ACTION_TO_STRING = {
{InputAction::RIGHT, "RIGHT"}, {InputAction::RIGHT, "RIGHT"},
{InputAction::THRUST, "THRUST"}, {InputAction::THRUST, "THRUST"},
{InputAction::SHOOT, "SHOOT"}, {InputAction::SHOOT, "SHOOT"},
{InputAction::START, "START"},
{InputAction::MENU, "MENU"},
{InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"}, {InputAction::WINDOW_INC_ZOOM, "WINDOW_INC_ZOOM"},
{InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"}, {InputAction::WINDOW_DEC_ZOOM, "WINDOW_DEC_ZOOM"},
{InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"}, {InputAction::TOGGLE_FULLSCREEN, "TOGGLE_FULLSCREEN"},
@@ -18,6 +20,8 @@ const std::unordered_map<std::string, InputAction> STRING_TO_ACTION = {
{"RIGHT", InputAction::RIGHT}, {"RIGHT", InputAction::RIGHT},
{"THRUST", InputAction::THRUST}, {"THRUST", InputAction::THRUST},
{"SHOOT", InputAction::SHOOT}, {"SHOOT", InputAction::SHOOT},
{"START", InputAction::START},
{"MENU", InputAction::MENU},
{"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM}, {"WINDOW_INC_ZOOM", InputAction::WINDOW_INC_ZOOM},
{"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM}, {"WINDOW_DEC_ZOOM", InputAction::WINDOW_DEC_ZOOM},
{"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN}, {"TOGGLE_FULLSCREEN", InputAction::TOGGLE_FULLSCREEN},
+1
View File
@@ -15,6 +15,7 @@ enum class InputAction : std::uint8_t { // Acciones de entrada posibles en el j
THRUST, // Acelerar THRUST, // Acelerar
SHOOT, // Disparar SHOOT, // Disparar
START, // Empezar match START, // Empezar match
MENU, // Abrir/cerrar menu de servicio (equivalent a F12)
// Inputs de sistema (globales) // Inputs de sistema (globales)
WINDOW_INC_ZOOM, // F2 WINDOW_INC_ZOOM, // F2
+24 -19
View File
@@ -9,29 +9,34 @@
namespace Resource::Helper { namespace Resource::Helper {
// Inicialitzar el sistema de recursos // Inicialitzar el sistema de recursos
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool { auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool {
return Loader::get().initialize(pack_file, fallback); return Loader::get().initialize(pack_file, fallback);
} }
// Carregar un file // Carregar un file
auto loadFile(const std::string& filepath) -> std::vector<uint8_t> { auto loadFile(const std::string& filepath) -> std::vector<uint8_t> {
// Normalitzar la ruta // Normalitzar la ruta
std::string normalized = normalizePath(filepath); std::string normalized = normalizePath(filepath);
// Carregar del sistema de recursos // Carregar del sistema de recursos
return Loader::get().loadResource(normalized); return Loader::get().loadResource(normalized);
} }
// Comprovar si existeix un file // Llistar recursos amb un prefix donat
auto fileExists(const std::string& filepath) -> bool { auto listResources(const std::string& prefix) -> std::vector<std::string> {
return Loader::get().listResources(prefix);
}
// Comprovar si existeix un file
auto fileExists(const std::string& filepath) -> bool {
std::string normalized = normalizePath(filepath); std::string normalized = normalizePath(filepath);
return Loader::get().resourceExists(normalized); return Loader::get().resourceExists(normalized);
} }
// Obtenir ruta normalitzada per al paquet // Obtenir ruta normalitzada per al paquet
// Elimina prefixos "data/", rutes absolutes, etc. // Elimina prefixos "data/", rutes absolutes, etc.
auto getPackPath(const std::string& asset_path) -> std::string { auto getPackPath(const std::string& asset_path) -> std::string {
std::string path = asset_path; std::string path = asset_path;
// Eliminar rutes absolutes (detectar / o C:\ al principi) // Eliminar rutes absolutes (detectar / o C:\ al principi)
@@ -65,16 +70,16 @@ auto getPackPath(const std::string& asset_path) -> std::string {
std::ranges::replace(path, '\\', '/'); std::ranges::replace(path, '\\', '/');
return path; return path;
} }
// Normalitzar ruta (alias de getPackPath) // Normalitzar ruta (alias de getPackPath)
auto normalizePath(const std::string& path) -> std::string { auto normalizePath(const std::string& path) -> std::string {
return getPackPath(path); return getPackPath(path);
} }
// Comprovar si hay paquet carregat // Comprovar si hay paquet carregat
auto isPackLoaded() -> bool { auto isPackLoaded() -> bool {
return Loader::get().isPackLoaded(); return Loader::get().isPackLoaded();
} }
} // namespace Resource::Helper } // namespace Resource::Helper
+13 -10
View File
@@ -10,18 +10,21 @@
namespace Resource::Helper { namespace Resource::Helper {
// Inicialización del sistema // Inicialización del sistema
auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool; auto initializeResourceSystem(const std::string& pack_file, bool fallback) -> bool;
// Càrrega de archivos // Càrrega de archivos
auto loadFile(const std::string& filepath) -> std::vector<uint8_t>; auto loadFile(const std::string& filepath) -> std::vector<uint8_t>;
auto fileExists(const std::string& filepath) -> bool; auto fileExists(const std::string& filepath) -> bool;
// Normalització de rutes // Llistat de recursos disponibles amb un prefix (ex. "shapes/", "sounds/").
auto getPackPath(const std::string& asset_path) -> std::string; auto listResources(const std::string& prefix) -> std::vector<std::string>;
auto normalizePath(const std::string& path) -> std::string;
// Estat // Normalització de rutes
auto isPackLoaded() -> bool; auto getPackPath(const std::string& asset_path) -> std::string;
auto normalizePath(const std::string& path) -> std::string;
// Estat
auto isPackLoaded() -> bool;
} // namespace Resource::Helper } // namespace Resource::Helper
+64 -27
View File
@@ -3,20 +3,21 @@
#include "resource_loader.hpp" #include "resource_loader.hpp"
#include <algorithm>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
namespace Resource { namespace Resource {
// Singleton // Singleton
auto Loader::get() -> Loader& { auto Loader::get() -> Loader& {
static Loader instance_; static Loader instance_;
return instance_; return instance_;
} }
// Inicialitzar el sistema de recursos // Inicialitzar el sistema de recursos
auto Loader::initialize(const std::string& pack_file, bool enable_fallback) -> bool { auto Loader::initialize(const std::string& pack_file, bool enable_fallback) -> bool {
fallback_enabled_ = enable_fallback; fallback_enabled_ = enable_fallback;
// Intentar load el paquet // Intentar load el paquet
@@ -36,10 +37,10 @@ auto Loader::initialize(const std::string& pack_file, bool enable_fallback) -> b
std::cout << "[ResourceLoader] Paquet carregat: " << pack_file << "\n"; std::cout << "[ResourceLoader] Paquet carregat: " << pack_file << "\n";
return true; return true;
} }
// Carregar un recurs // Carregar un recurs
auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> { auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
// Intentar load del paquet primer // Intentar load del paquet primer
if (pack_) { if (pack_) {
if (pack_->hasResource(filename)) { if (pack_->hasResource(filename)) {
@@ -65,10 +66,46 @@ auto Loader::loadResource(const std::string& filename) -> std::vector<uint8_t> {
} }
return {}; return {};
} }
// Comprovar si existeix un recurs auto Loader::listResources(const std::string& prefix) -> std::vector<std::string> {
auto Loader::resourceExists(const std::string& filename) -> bool { std::vector<std::string> result;
if (pack_) {
for (const auto& path : pack_->getResourceList()) {
if (path.starts_with(prefix)) {
result.push_back(path);
}
}
return result;
}
if (!fallback_enabled_) {
return result;
}
std::string root = base_path_.empty() ? "data/" + prefix : base_path_ + "/data/" + prefix;
if (!std::filesystem::exists(root)) {
return result;
}
for (const auto& entry : std::filesystem::recursive_directory_iterator(root)) {
if (!entry.is_regular_file()) {
continue;
}
std::string full = entry.path().generic_string();
if (auto pos = full.find("/data/"); pos != std::string::npos) {
result.push_back(full.substr(pos + 6));
} else if (full.starts_with("data/")) {
result.push_back(full.substr(5));
}
}
std::ranges::sort(result);
return result;
}
// Comprovar si existeix un recurs
auto Loader::resourceExists(const std::string& filename) -> bool {
// Comprovar al paquet // Comprovar al paquet
if (pack_ && pack_->hasResource(filename)) { if (pack_ && pack_->hasResource(filename)) {
return true; return true;
@@ -81,36 +118,36 @@ auto Loader::resourceExists(const std::string& filename) -> bool {
} }
return false; return false;
} }
// Validar el paquet // Validar el paquet
auto Loader::validatePack() -> bool { auto Loader::validatePack() -> bool {
if (!pack_) { if (!pack_) {
std::cerr << "[ResourceLoader] Advertència: no hay paquet carregat per validar\n"; std::cerr << "[ResourceLoader] Advertència: no hay paquet carregat per validar\n";
return false; return false;
} }
return pack_->validatePack(); return pack_->validatePack();
} }
// Comprovar si hay paquet carregat // Comprovar si hay paquet carregat
auto Loader::isPackLoaded() const -> bool { auto Loader::isPackLoaded() const -> bool {
return pack_ != nullptr; return pack_ != nullptr;
} }
// Establir la ruta base // Establir la ruta base
void Loader::setBasePath(const std::string& path) { void Loader::setBasePath(const std::string& path) {
base_path_ = path; base_path_ = path;
std::cout << "[ResourceLoader] Ruta base establerta: " << base_path_ << "\n"; std::cout << "[ResourceLoader] Ruta base establerta: " << base_path_ << "\n";
} }
// Obtenir la ruta base // Obtenir la ruta base
auto Loader::getBasePath() const -> const std::string& { auto Loader::getBasePath() const -> const std::string& {
return base_path_; return base_path_;
} }
// Carregar des del sistema de archivos (fallback) // Carregar des del sistema de archivos (fallback)
auto Loader::loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t> { auto Loader::loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t> {
// The filename is already normalized (e.g., "shapes/logo/letra_j.shp") // The filename is already normalized (e.g., "shapes/logo/letra_j.shp")
// We need to prepend base_path + "data/" // We need to prepend base_path + "data/"
std::string fullpath; std::string fullpath;
@@ -138,6 +175,6 @@ auto Loader::loadFromFilesystem(const std::string& filename) -> std::vector<uint
std::cout << "[ResourceLoader] Carregat des del sistema de archivos: " << fullpath << "\n"; std::cout << "[ResourceLoader] Carregat des del sistema de archivos: " << fullpath << "\n";
return data; return data;
} }
} // namespace Resource } // namespace Resource
+8 -3
View File
@@ -12,8 +12,8 @@
namespace Resource { namespace Resource {
// Singleton per gestionar la càrrega de recursos // Singleton per gestionar la càrrega de recursos
class Loader { class Loader {
public: public:
// Singleton // Singleton
static auto get() -> Loader&; static auto get() -> Loader&;
@@ -25,6 +25,11 @@ class Loader {
auto loadResource(const std::string& filename) -> std::vector<uint8_t>; auto loadResource(const std::string& filename) -> std::vector<uint8_t>;
auto resourceExists(const std::string& filename) -> bool; auto resourceExists(const std::string& filename) -> bool;
// Llistat de recursos amb prefix (ex. "shapes/", "sounds/"). Si hi ha
// pack, retorna els fitxers del pack filtrats; si no, escaneja el
// sistema de fitxers recursivament a `data/<prefix>`.
auto listResources(const std::string& prefix) -> std::vector<std::string>;
// Validació // Validació
auto validatePack() -> bool; auto validatePack() -> bool;
[[nodiscard]] auto isPackLoaded() const -> bool; [[nodiscard]] auto isPackLoaded() const -> bool;
@@ -48,6 +53,6 @@ class Loader {
// Funciones auxiliars // Funciones auxiliars
auto loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t>; auto loadFromFilesystem(const std::string& filename) -> std::vector<uint8_t>;
}; };
} // namespace Resource } // namespace Resource
+35 -48
View File
@@ -10,32 +10,32 @@
namespace Resource { namespace Resource {
// Calcular checksum CRC32 simplificat // Calcular checksum CRC32 simplificat
auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t { auto Pack::calculateChecksum(const std::vector<uint8_t>& data) -> uint32_t {
uint32_t checksum = 0x12345678; uint32_t checksum = 0x12345678;
for (unsigned char byte : data) { for (unsigned char byte : data) {
checksum = ((checksum << 5) + checksum) + byte; checksum = ((checksum << 5) + checksum) + byte;
} }
return checksum; return checksum;
} }
// Encriptació XOR (simètrica) // Encriptació XOR (simètrica)
void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) { void Pack::encryptData(std::vector<uint8_t>& data, const std::string& key) {
if (key.empty()) { if (key.empty()) {
return; return;
} }
for (size_t i = 0; i < data.size(); ++i) { for (size_t i = 0; i < data.size(); ++i) {
data[i] ^= key[i % key.length()]; data[i] ^= key[i % key.length()];
} }
} }
void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) { void Pack::decryptData(std::vector<uint8_t>& data, const std::string& key) {
// XOR es simètric // XOR es simètric
encryptData(data, key); encryptData(data, key);
} }
// Llegir file complet a memòria // Llegir file complet a memòria
auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> { auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> {
std::ifstream file(filepath, std::ios::binary | std::ios::ate); std::ifstream file(filepath, std::ios::binary | std::ios::ate);
if (!file) { if (!file) {
std::cerr << "[ResourcePack] Error: no es pot obrir " << filepath << '\n'; std::cerr << "[ResourcePack] Error: no es pot obrir " << filepath << '\n';
@@ -52,10 +52,10 @@ auto Pack::readFile(const std::string& filepath) -> std::vector<uint8_t> {
} }
return data; return data;
} }
// Añadir un file individual al paquet // Añadir un file individual al paquet
auto Pack::addFile(const std::string& filepath, const std::string& pack_name) -> bool { auto Pack::addFile(const std::string& filepath, const std::string& pack_name) -> bool {
auto file_data = readFile(filepath); auto file_data = readFile(filepath);
if (file_data.empty()) { if (file_data.empty()) {
return false; return false;
@@ -72,13 +72,11 @@ auto Pack::addFile(const std::string& filepath, const std::string& pack_name) ->
resources_[pack_name] = entry; resources_[pack_name] = entry;
std::cout << "[ResourcePack] Añadido: " << pack_name << " (" << file_data.size()
<< " bytes)\n";
return true; return true;
} }
// Añadir todos los archivos de un directori recursivament // Añadir todos los archivos de un directori recursivament
auto Pack::addDirectory(const std::string& dir_path, auto Pack::addDirectory(const std::string& dir_path,
const std::string& base_path) -> bool { const std::string& base_path) -> bool {
namespace fs = std::filesystem; namespace fs = std::filesystem;
@@ -105,7 +103,6 @@ auto Pack::addDirectory(const std::string& dir_path,
relative_path.find(".tsx") != std::string::npos || relative_path.find(".tsx") != std::string::npos ||
relative_path.find(".DS_Store") != std::string::npos || relative_path.find(".DS_Store") != std::string::npos ||
relative_path.find(".git") != std::string::npos) { relative_path.find(".git") != std::string::npos) {
std::cout << "[ResourcePack] Saltant: " << relative_path << '\n';
continue; continue;
} }
@@ -114,10 +111,10 @@ auto Pack::addDirectory(const std::string& dir_path,
} }
return true; return true;
} }
// Guardar paquet a disc // Guardar paquet a disc
auto Pack::savePack(const std::string& pack_file) -> bool { auto Pack::savePack(const std::string& pack_file) -> bool {
std::ofstream file(pack_file, std::ios::binary); std::ofstream file(pack_file, std::ios::binary);
if (!file) { if (!file) {
std::cerr << "[ResourcePack] Error: no es pot crear " << pack_file << '\n'; std::cerr << "[ResourcePack] Error: no es pot crear " << pack_file << '\n';
@@ -154,14 +151,11 @@ auto Pack::savePack(const std::string& pack_file) -> bool {
file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size)); file.write(reinterpret_cast<const char*>(&data_size), sizeof(data_size));
file.write(reinterpret_cast<const char*>(encrypted_data.data()), encrypted_data.size()); file.write(reinterpret_cast<const char*>(encrypted_data.data()), encrypted_data.size());
std::cout << "[ResourcePack] Guardat: " << pack_file << " (" << resources_.size()
<< " recursos, " << data_size << " bytes)\n";
return true; return true;
} }
// Carregar paquet desde disc // Carregar paquet desde disc
auto Pack::loadPack(const std::string& pack_file) -> bool { auto Pack::loadPack(const std::string& pack_file) -> bool {
std::ifstream file(pack_file, std::ios::binary); std::ifstream file(pack_file, std::ios::binary);
if (!file) { if (!file) {
std::cerr << "[ResourcePack] Error: no es pot obrir " << pack_file << '\n'; std::cerr << "[ResourcePack] Error: no es pot obrir " << pack_file << '\n';
@@ -219,14 +213,11 @@ auto Pack::loadPack(const std::string& pack_file) -> bool {
// Desencriptar // Desencriptar
decryptData(data_, DEFAULT_ENCRYPT_KEY); decryptData(data_, DEFAULT_ENCRYPT_KEY);
std::cout << "[ResourcePack] Carregat: " << pack_file << " (" << resources_.size()
<< " recursos)\n";
return true; return true;
} }
// Obtenir un recurs del paquet // Obtenir un recurs del paquet
auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> { auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> {
auto it = resources_.find(filename); auto it = resources_.find(filename);
if (it == resources_.end()) { if (it == resources_.end()) {
std::cerr << "[ResourcePack] Error: recurs no trobat: " << filename << '\n'; std::cerr << "[ResourcePack] Error: recurs no trobat: " << filename << '\n';
@@ -254,15 +245,15 @@ auto Pack::getResource(const std::string& filename) -> std::vector<uint8_t> {
} }
return resource_data; return resource_data;
} }
// Comprovar si existeix un recurs // Comprovar si existeix un recurs
auto Pack::hasResource(const std::string& filename) const -> bool { auto Pack::hasResource(const std::string& filename) const -> bool {
return resources_.contains(filename); return resources_.contains(filename);
} }
// Obtenir list de todos los recursos // Obtenir list de todos los recursos
auto Pack::getResourceList() const -> std::vector<std::string> { auto Pack::getResourceList() const -> std::vector<std::string> {
std::vector<std::string> list; std::vector<std::string> list;
list.reserve(resources_.size()); list.reserve(resources_.size());
@@ -272,10 +263,10 @@ auto Pack::getResourceList() const -> std::vector<std::string> {
std::ranges::sort(list); std::ranges::sort(list);
return list; return list;
} }
// Validar integritat del paquet // Validar integritat del paquet
auto Pack::validatePack() const -> bool { auto Pack::validatePack() const -> bool {
bool valid = true; bool valid = true;
for (const auto& [name, entry] : resources_) { for (const auto& [name, entry] : resources_) {
@@ -299,11 +290,7 @@ auto Pack::validatePack() const -> bool {
} }
} }
if (valid) { return valid;
std::cout << "[ResourcePack] Validació OK (" << resources_.size() << " recursos)\n";
} }
return valid;
}
} // namespace Resource } // namespace Resource
+2 -1
View File
@@ -5,6 +5,7 @@
#include <SDL3/SDL.h> #include <SDL3/SDL.h>
#include <cctype> #include <cctype>
#include <cmath>
#include <string> #include <string>
#include "core/defaults.hpp" #include "core/defaults.hpp"
@@ -35,7 +36,7 @@ namespace System {
fps_frame_count_++; fps_frame_count_++;
if (fps_accumulator_ >= Cfg::FPS_UPDATE_INTERVAL) { if (fps_accumulator_ >= Cfg::FPS_UPDATE_INTERVAL) {
fps_display_ = static_cast<int>(fps_frame_count_ / fps_accumulator_); fps_display_ = static_cast<int>(std::lround(static_cast<float>(fps_frame_count_) / fps_accumulator_));
fps_frame_count_ = 0; fps_frame_count_ = 0;
fps_accumulator_ = 0.0F; fps_accumulator_ = 0.0F;
} }
+58 -4
View File
@@ -12,6 +12,8 @@
#include "core/audio/audio.hpp" #include "core/audio/audio.hpp"
#include "core/audio/audio_adapter.hpp" #include "core/audio/audio_adapter.hpp"
#include "core/defaults/window.hpp" #include "core/defaults/window.hpp"
#include "core/graphics/shape_loader.hpp"
#include "core/input/define_inputs.hpp"
#include "core/input/input.hpp" #include "core/input/input.hpp"
#include "core/input/mouse.hpp" #include "core/input/mouse.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
@@ -107,6 +109,34 @@ Director::Director(int argc, char* argv[])
// Inicialitzar sistema de input // Inicialitzar sistema de input
Input::init("data/gamecontrollerdb.txt"); Input::init("data/gamecontrollerdb.txt");
// Autoassignacio de primer arranque: si cap dels dos jugadors te mando
// assignat al config, repartim els que hi haja detectats (P1 = pad 0,
// P2 = pad 1 si existeix) i ho persistim. Aixo nomes dispara amb tots
// dos buits perque un "SENSE MANDO" explicit ha de sobreviure entre
// arrancades.
{
auto& p1 = cfg_->player1;
auto& p2 = cfg_->player2;
const bool BOTH_EMPTY = p1.gamepad_name.empty() && p1.gamepad_path.empty() && p2.gamepad_name.empty() && p2.gamepad_path.empty();
if (BOTH_EMPTY) {
const auto& pads = Input::get()->getGamepads();
bool changed = false;
if (!pads.empty() && pads[0]) {
p1.gamepad_name = pads[0]->name;
p1.gamepad_path = pads[0]->path;
changed = true;
}
if (pads.size() > 1 && pads[1]) {
p2.gamepad_name = pads[1]->name;
p2.gamepad_path = pads[1]->path;
changed = true;
}
if (changed) {
ConfigYaml::saveToFile();
}
}
}
// Aplicar configuración de controls dels jugadors // Aplicar configuración de controls dels jugadors
Input::get()->applyPlayer1Bindings(cfg_->player1); Input::get()->applyPlayer1Bindings(cfg_->player1);
Input::get()->applyPlayer2Bindings(cfg_->player2); Input::get()->applyPlayer2Bindings(cfg_->player2);
@@ -147,10 +177,21 @@ Director::Director(int argc, char* argv[])
Audio::init(AUDIO_CONFIG); Audio::init(AUDIO_CONFIG);
Audio::get()->applySettings(AUDIO_CONFIG); Audio::get()->applySettings(AUDIO_CONFIG);
AudioResource::getMusic("title.ogg"); // Precàrrega blocant de tots els recursos al boot per evitar hits d'I/O i
AudioResource::getMusic("game.ogg"); // de decodificació en transicions (TITLE → GAME, primera explosió, etc.).
// Mateix patró que aee_arcade: iterem `listResources` i forcem la càrrega
// al cache de cada subsistema.
for (const auto& path : Resource::Helper::listResources("music/")) {
AudioResource::getMusic(path.substr(std::string_view{"music/"}.size()));
}
for (const auto& path : Resource::Helper::listResources("sounds/")) {
AudioResource::getSound(path.substr(std::string_view{"sounds/"}.size()));
}
for (const auto& path : Resource::Helper::listResources("shapes/")) {
Graphics::ShapeLoader::load(path.substr(std::string_view{"shapes/"}.size()));
}
if (cfg_->console) { if (cfg_->console) {
std::cout << "Música precacheada\n"; std::cout << "Recursos precachejats (música, sons, shapes)\n";
} }
context_ = std::make_unique<SceneContext>(); context_ = std::make_unique<SceneContext>();
@@ -166,6 +207,7 @@ Director::Director(int argc, char* argv[])
System::Notifier::init(sdl_->getRenderer()); System::Notifier::init(sdl_->getRenderer());
System::ServiceMenu::init(sdl_->getRenderer(), sdl_.get(), debug_overlay_.get()); System::ServiceMenu::init(sdl_->getRenderer(), sdl_.get(), debug_overlay_.get());
System::DefineInputs::init(sdl_->getRenderer());
last_ticks_ms_ = SDL_GetTicks(); last_ticks_ms_ = SDL_GetTicks();
} }
@@ -180,6 +222,7 @@ Director::~Director() {
// l'hem de cridar nosaltres. // l'hem de cridar nosaltres.
current_scene_.reset(); current_scene_.reset();
debug_overlay_.reset(); debug_overlay_.reset();
System::DefineInputs::destroy();
System::ServiceMenu::destroy(); System::ServiceMenu::destroy();
System::Notifier::destroy(); System::Notifier::destroy();
context_.reset(); context_.reset();
@@ -364,6 +407,9 @@ auto Director::iterate() -> SDL_AppResult {
if (auto* menu = System::ServiceMenu::get(); menu != nullptr) { if (auto* menu = System::ServiceMenu::get(); menu != nullptr) {
menu->update(delta_time); menu->update(delta_time);
} }
if (auto* di = System::DefineInputs::get(); di != nullptr) {
di->update(delta_time);
}
Audio::update(); Audio::update();
// Si la swapchain no està disponible (finestra minimitzada, etc.), // Si la swapchain no està disponible (finestra minimitzada, etc.),
@@ -377,9 +423,17 @@ auto Director::iterate() -> SDL_AppResult {
if (const auto* notifier = System::Notifier::get(); notifier != nullptr) { if (const auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->draw(); // toast: per damunt de tot notifier->draw(); // toast: per damunt de tot
} }
if (const auto* menu = System::ServiceMenu::get(); menu != nullptr) { // Mentre l'overlay de redefinicio esta actiu, amaguem el menu de servei
// (encara queda "open" per a absorbir events un cop el modal s'auto-tanqui,
// pero no es pinta per no confondre's visualment amb el modal).
const auto* di = System::DefineInputs::get();
const bool DEFINE_ACTIVE = (di != nullptr) && di->isActive();
if (const auto* menu = System::ServiceMenu::get(); menu != nullptr && !DEFINE_ACTIVE) {
menu->draw(); // service menu: per damunt fins i tot dels toasts menu->draw(); // service menu: per damunt fins i tot dels toasts
} }
if (di != nullptr) {
di->draw(); // overlay de rebind: per damunt de tot
}
sdl_->present(); sdl_->present();
return SDL_APP_CONTINUE; return SDL_APP_CONTINUE;
} }
+68 -6
View File
@@ -5,6 +5,7 @@
#include <iostream> #include <iostream>
#include "core/input/define_inputs.hpp"
#include "core/input/input.hpp" #include "core/input/input.hpp"
#include "core/input/mouse.hpp" #include "core/input/mouse.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
@@ -22,17 +23,17 @@ namespace GlobalEvents {
namespace { namespace {
// Reenvia el KEY_DOWN al menu de servei si esta obert i la tecla no // Reenvia events al menu de servei si esta obert. Accepta:
// es F1-F12 ni ESC (que sempre passen com a globals). Retorna true si // - KEY_DOWN (excepte F1-F12 i ESC, que sempre passen com a globals)
// el menu l'ha consumit. // - GAMEPAD_BUTTON_DOWN (per navegacio amb dpad + FIRE/ACCELERATE)
// - GAMEPAD_AXIS_MOTION (per navegacio amb stick)
// Retorna true si l'event s'ha entregat al menu.
auto forwardToServiceMenu(const SDL_Event& event) -> bool { auto forwardToServiceMenu(const SDL_Event& event) -> bool {
if (event.type != SDL_EVENT_KEY_DOWN) {
return false;
}
auto* menu = System::ServiceMenu::get(); auto* menu = System::ServiceMenu::get();
if (menu == nullptr || !menu->isOpen()) { if (menu == nullptr || !menu->isOpen()) {
return false; return false;
} }
if (event.type == SDL_EVENT_KEY_DOWN) {
const SDL_Scancode SC = event.key.scancode; const SDL_Scancode SC = event.key.scancode;
const bool PASSTHROUGH = (SC == SDL_SCANCODE_ESCAPE) || const bool PASSTHROUGH = (SC == SDL_SCANCODE_ESCAPE) ||
(SC >= SDL_SCANCODE_F1 && SC <= SDL_SCANCODE_F12); (SC >= SDL_SCANCODE_F1 && SC <= SDL_SCANCODE_F12);
@@ -42,6 +43,56 @@ namespace GlobalEvents {
menu->handleEvent(event); menu->handleEvent(event);
return true; return true;
} }
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN ||
event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
menu->handleEvent(event);
return true;
}
return false;
}
// Engoleix els events que DefineInputs vol consumir mentre l'overlay
// es actiu. Els events que el modul torna a passar (QUIT, ESC) cauen
// cap al pipeline normal i poden tancar la finestra o obrir el prompt
// d'eixida sense haver de completar la sequencia.
auto consumeIfDefineActive(const SDL_Event& event) -> bool {
auto* di = System::DefineInputs::get();
if (di == nullptr || !di->isActive()) {
return false;
}
return di->handleEvent(event);
}
// Botó MENU al mando d'algun jugador → alterna el menú de servei
// (mateix comportament que F12 al teclat). Retorna true si l'event és
// un GAMEPAD_BUTTON_DOWN consumit.
auto handleGamepadMenuButton(const SDL_Event& event) -> bool {
if (event.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
return false;
}
auto* input = Input::get();
if (input == nullptr) {
return false;
}
auto match_player = [&](int player_index) {
auto pad = input->getPlayerGamepad(player_index);
if (!pad || pad->instance_id != event.gbutton.which) {
return false;
}
auto it = pad->bindings.find(InputAction::MENU);
if (it == pad->bindings.end()) {
return false;
}
return it->second.button == static_cast<int>(event.gbutton.button);
};
if (!match_player(0) && !match_player(1)) {
return false;
}
if (auto* menu = System::ServiceMenu::get(); menu != nullptr) {
menu->toggle();
}
return true;
}
} // namespace } // namespace
@@ -52,6 +103,12 @@ namespace GlobalEvents {
std::cout << "[Input] " << event_msg << '\n'; std::cout << "[Input] " << event_msg << '\n';
} }
// 1b. Si l'overlay de redefinicio esta actiu, engoleix tots els events
// (cap arriba al joc, al menu de servei ni als hotkeys F1-F12).
if (consumeIfDefineActive(event)) {
return true;
}
// 2. Procesar SDL_EVENT_QUIT directamente (no es input de juego) // 2. Procesar SDL_EVENT_QUIT directamente (no es input de juego)
if (event.type == SDL_EVENT_QUIT) { if (event.type == SDL_EVENT_QUIT) {
context.setNextScene(SceneType::EXIT); context.setNextScene(SceneType::EXIT);
@@ -62,6 +119,11 @@ namespace GlobalEvents {
// 3. Gestió del ratolí (auto-ocultar) // 3. Gestió del ratolí (auto-ocultar)
Mouse::handleEvent(event); Mouse::handleEvent(event);
// 3b. Botó MENU al mando (equivalent a F12)
if (handleGamepadMenuButton(event)) {
return true;
}
// 4. Service Menu (F12): consumeix tot KEY_DOWN excepte tecles de // 4. Service Menu (F12): consumeix tot KEY_DOWN excepte tecles de
// funció (F1-F12) i ESC, que continuen sent globals (zoom, fullscreen, // funció (F1-F12) i ESC, que continuen sent globals (zoom, fullscreen,
// vsync, AA, postfx, locale, exit prompt). Aixi el menu captura // vsync, AA, postfx, locale, exit prompt). Aixi el menu captura
+374 -14
View File
@@ -15,11 +15,15 @@
#include "core/defaults/audio.hpp" #include "core/defaults/audio.hpp"
#include "core/defaults/rendering.hpp" #include "core/defaults/rendering.hpp"
#include "core/defaults/service_menu.hpp" #include "core/defaults/service_menu.hpp"
#include "core/input/define_inputs.hpp"
#include "core/input/input.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/sdl_manager.hpp" #include "core/rendering/sdl_manager.hpp"
#include "core/system/debug_overlay.hpp" #include "core/system/debug_overlay.hpp"
#include "core/system/notifier.hpp"
#include "core/system/relaunch.hpp" #include "core/system/relaunch.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "core/utils/string_utils.hpp"
#include "game/config_yaml.hpp" #include "game/config_yaml.hpp"
#include "project.h" #include "project.h"
@@ -54,17 +58,6 @@ namespace {
} }
} }
// VectorText nomes admet ASCII en majuscules. El git hash sortit de git
// rev-parse es lowercase (a-f), aixi que el passem a uppercase per al
// display sense modificar Project::GIT_HASH.
auto toUpperAscii(const std::string& s) -> std::string {
std::string result = s;
for (char& c : result) {
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
}
return result;
}
// Resol el text del label d'un item: prioritza label_text (literal) sobre // Resol el text del label d'un item: prioritza label_text (literal) sobre
// label_key (locale). Retorna cadena buida si tots dos son buits. // label_key (locale). Retorna cadena buida si tots dos son buits.
auto resolveLabel(const System::ServiceMenu::Item& item) -> std::string { auto resolveLabel(const System::ServiceMenu::Item& item) -> std::string {
@@ -77,6 +70,107 @@ namespace {
return Locale::get().text(item.label_key); return Locale::get().text(item.label_key);
} }
// ---- Helpers de la pagina CONTROLS ----
auto padDisplayName(int player_index) -> std::string {
const auto* input = Input::get();
if (input == nullptr) {
return Locale::get().text("service_menu.controls_no_pad");
}
auto pad = input->getPlayerGamepad(player_index);
if (!pad) {
return Locale::get().text("service_menu.controls_no_pad");
}
return Utils::toUpperAscii(pad->name);
}
// Index del pad assignat dins de la llista. Retorna pads.size() per
// representar el slot virtual SENSE MANDO (al final del cycle).
auto findAssignedIndex(const std::vector<std::shared_ptr<Input::Gamepad>>& pads,
const Config::PlayerBindings& pcfg) -> std::size_t {
for (std::size_t i = 0; i < pads.size(); ++i) {
if (pads[i] && !pcfg.gamepad_path.empty() && pads[i]->path == pcfg.gamepad_path) {
return i;
}
}
for (std::size_t i = 0; i < pads.size(); ++i) {
if (pads[i] && !pcfg.gamepad_name.empty() && pads[i]->name == pcfg.gamepad_name) {
return i;
}
}
return pads.size(); // Slot virtual "sense mando"
}
// Aplica les noves assignacions a Input. Si ha hagut swap, refresca els
// dos jugadors; en cas contrari nomes el que ha canviat.
void reapplyBindings(int player_index, bool swap_other) {
auto* input = Input::get();
if (input == nullptr) {
return;
}
if (player_index == 0) {
input->applyPlayer1Bindings(ConfigYaml::engine_config.player1);
if (swap_other) {
input->applyPlayer2Bindings(ConfigYaml::engine_config.player2);
}
} else {
input->applyPlayer2Bindings(ConfigYaml::engine_config.player2);
if (swap_other) {
input->applyPlayer1Bindings(ConfigYaml::engine_config.player1);
}
}
}
// Assigna un pad concret (per nom+path) a un jugador i ho persisteix.
// Si l'altre jugador ja tenia eixe pad, fa swap: l'altre rep
// l'assignacio prèvia d'aquest. Per desasignar, passar new_name i
// new_path buits.
void assignPadToPlayer(int player_index, const std::string& new_name, const std::string& new_path) {
auto& pcfg = (player_index == 0)
? ConfigYaml::engine_config.player1
: ConfigYaml::engine_config.player2;
auto& other = (player_index == 0)
? ConfigYaml::engine_config.player2
: ConfigYaml::engine_config.player1;
// Detecta conflicte amb l'altre jugador per fer swap. Prioritzem
// path (mateix criteri que resolvePlayerGamepad); si nomes tenim
// nom (mando reconegut sense path), comparem per nom.
const bool CONFLICT = !new_path.empty() && other.gamepad_path == new_path;
const bool CONFLICT_BY_NAME = !new_name.empty() && new_path.empty() &&
other.gamepad_name == new_name;
const bool DO_SWAP = CONFLICT || CONFLICT_BY_NAME;
const std::string PREV_NAME = pcfg.gamepad_name;
const std::string PREV_PATH = pcfg.gamepad_path;
pcfg.gamepad_name = new_name;
pcfg.gamepad_path = new_path;
if (DO_SWAP) {
other.gamepad_name = PREV_NAME;
other.gamepad_path = PREV_PATH;
}
reapplyBindings(player_index, DO_SWAP);
ConfigYaml::saveToFile();
}
// Arranca un rebind o avisa si el jugador no te pad. Retorna true si el
// rebind ha començat (el caller ha de tancar el menu).
auto startDefine(System::DefineInputs::Mode mode, System::DefineInputs::Player pl) -> bool {
auto* di = System::DefineInputs::get();
if (di == nullptr) {
return false;
}
if (!di->begin(mode, pl)) {
if (auto* n = System::Notifier::get(); n != nullptr) {
n->notifyWarn(Locale::get().text("define.no_gamepad"));
}
return false;
}
return true;
}
} // namespace } // namespace
namespace System { namespace System {
@@ -153,6 +247,7 @@ namespace System {
makeSubmenu("service_menu.video", [this] { pushPage(buildVideoPage()); }), makeSubmenu("service_menu.video", [this] { pushPage(buildVideoPage()); }),
makeSubmenu("service_menu.audio", [this] { pushPage(buildAudioPage()); }), makeSubmenu("service_menu.audio", [this] { pushPage(buildAudioPage()); }),
makeSubmenu("service_menu.options", [this] { pushPage(buildOptionsPage()); }), makeSubmenu("service_menu.options", [this] { pushPage(buildOptionsPage()); }),
makeSubmenu("service_menu.controls", [this] { pushPage(buildControlsPage()); }),
makeSubmenu("service_menu.system", [this] { pushPage(buildSystemPage()); }), makeSubmenu("service_menu.system", [this] { pushPage(buildSystemPage()); }),
}; };
stack_.clear(); stack_.clear();
@@ -437,13 +532,138 @@ namespace System {
return page; return page;
} }
namespace {
auto makeDefineItem(const char* label_key, DefineInputs::Mode mode, DefineInputs::Player pl) -> ServiceMenu::Item {
return ServiceMenu::Item{
.kind = ServiceMenu::Kind::ACTION,
.label_key = label_key,
.label_text = {},
.selectable = true,
// El menu de servei NO es tanca: queda obert per sota de
// l'overlay i absorbira qualsevol event que arribi un cop
// l'overlay s'haja auto-cancel·lat.
.on_activate = [mode, pl] { startDefine(mode, pl); },
.get_value_text = {},
.on_change = {},
};
}
} // namespace
auto ServiceMenu::buildControlsPage() -> Page {
Page page;
page.title_key = "service_menu.controls";
page.items = {
Item{
.kind = Kind::SUBMENU,
.label_key = "service_menu.controls_pad_p1",
.label_text = {},
.selectable = true,
.on_activate = [this] { pushPage(buildPadPickerPage(0)); },
.get_value_text = [] { return padDisplayName(0); },
.on_change = {},
},
Item{
.kind = Kind::SUBMENU,
.label_key = "service_menu.controls_pad_p2",
.label_text = {},
.selectable = true,
.on_activate = [this] { pushPage(buildPadPickerPage(1)); },
.get_value_text = [] { return padDisplayName(1); },
.on_change = {},
},
makeDefineItem("service_menu.controls_define_keyboard_p1",
DefineInputs::Mode::KEYBOARD,
DefineInputs::Player::P1),
makeDefineItem("service_menu.controls_define_keyboard_p2",
DefineInputs::Mode::KEYBOARD,
DefineInputs::Player::P2),
makeDefineItem("service_menu.controls_define_gamepad_p1",
DefineInputs::Mode::GAMEPAD,
DefineInputs::Player::P1),
makeDefineItem("service_menu.controls_define_gamepad_p2",
DefineInputs::Mode::GAMEPAD,
DefineInputs::Player::P2),
};
return page;
}
auto ServiceMenu::buildPadPickerPage(int player_index) -> Page {
Page page;
page.title_key = (player_index == 0)
? "service_menu.controls_pad_p1"
: "service_menu.controls_pad_p2";
const auto* input = Input::get();
if (input == nullptr) {
return page;
}
const auto& pads = input->getGamepads();
const auto& pcfg = (player_index == 0)
? ConfigYaml::engine_config.player1
: ConfigYaml::engine_config.player2;
const auto& other = (player_index == 0)
? ConfigYaml::engine_config.player2
: ConfigYaml::engine_config.player1;
// Cursor inicial sobre el pad assignat, o sobre "SENSE MANDO"
// (ultim item) si el jugador no en te cap.
page.cursor = findAssignedIndex(pads, pcfg);
for (const auto& pad : pads) {
if (!pad) {
continue;
}
std::string label = Utils::toUpperAscii(pad->name);
// Sufix (PX) nomes si el mando el te l'altre jugador, perque
// l'usuari sapiga que assignar-lo li'l "robarà".
const bool OTHER_HAS_BY_PATH = !other.gamepad_path.empty() && other.gamepad_path == pad->path;
const bool OTHER_HAS_BY_NAME = other.gamepad_path.empty() && !other.gamepad_name.empty() && other.gamepad_name == pad->name;
if (OTHER_HAS_BY_PATH || OTHER_HAS_BY_NAME) {
label += (player_index == 0) ? " (P2)" : " (P1)";
}
const std::string PAD_NAME = pad->name;
const std::string PAD_PATH = pad->path;
const int PI = player_index;
page.items.push_back(Item{
.kind = Kind::ACTION,
.label_key = {},
.label_text = std::move(label),
.selectable = true,
.on_activate = [this, PI, PAD_NAME, PAD_PATH] {
assignPadToPlayer(PI, PAD_NAME, PAD_PATH);
popPage();
},
.get_value_text = {},
.on_change = {},
});
}
// Item final: desasignar.
const int PI = player_index;
page.items.push_back(Item{
.kind = Kind::ACTION,
.label_key = "service_menu.controls_no_pad",
.label_text = {},
.selectable = true,
.on_activate = [this, PI] {
assignPadToPlayer(PI, {}, {});
popPage();
},
.get_value_text = {},
.on_change = {},
});
return page;
}
auto ServiceMenu::buildSystemPage() -> Page { auto ServiceMenu::buildSystemPage() -> Page {
Page page; Page page;
page.title_key = "service_menu.system"; page.title_key = "service_menu.system";
// Versio + hash com a subtitol sota el titol (apagat, mes petit). // Versio + hash com a subtitol sota el titol (apagat, mes petit).
// Uppercase del hash perque VectorText nomes admet majuscules. // Uppercase del hash perque VectorText nomes admet majuscules.
page.subtitle_provider = [] { page.subtitle_provider = [] {
return std::format("V{} - {}", Project::VERSION, toUpperAscii(Project::GIT_HASH)); return std::format("V{} - {}", Project::VERSION, Utils::toUpperAscii(Project::GIT_HASH));
}; };
page.items = { page.items = {
// REINICIAR (amb confirmacio). // REINICIAR (amb confirmacio).
@@ -593,10 +813,55 @@ namespace System {
} }
} }
auto ServiceMenu::handleEvent(const SDL_Event& event) -> bool { namespace {
if (!open_ || stack_.empty() || event.type != SDL_EVENT_KEY_DOWN) {
// Llindar de stick per a navegacio de menu (mig camp del rang ±32767).
// Mes baix que el del joc (30000) per a una resposta mes agil al menu.
constexpr Sint16 MENU_STICK_THRESHOLD = 16384;
// Llindar de trigger (mateix valor que MENU_TRIGGER_THRESHOLD, que
// és private). Edge a partir del 50% del rang.
constexpr Sint16 MENU_TRIGGER_THRESHOLD = 16384;
// Retorna true si el codi de boto SDL coincideix amb l'accio
// configurada per algun dels dos jugadors (es a dir, el boto te el
// mateix codi al binding de FIRE o ACCELERATE del pad emissor).
auto buttonMatchesAction(SDL_JoystickID which, int button, InputAction action) -> bool {
const auto* input = Input::get();
if (input == nullptr) {
return false; return false;
} }
for (int i = 0; i < 2; ++i) {
auto pad = input->getPlayerGamepad(i);
if (!pad || pad->instance_id != which) {
continue;
}
auto it = pad->bindings.find(action);
if (it != pad->bindings.end() && it->second.button == button) {
return true;
}
}
return false;
}
} // namespace
auto ServiceMenu::handleEvent(const SDL_Event& event) -> bool {
if (!open_ || stack_.empty()) {
return false;
}
if (event.type == SDL_EVENT_KEY_DOWN) {
return handleKeyDown(event);
}
if (event.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
return handleGamepadButton(event);
}
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
return handleGamepadAxis(event);
}
return false;
}
auto ServiceMenu::handleKeyDown(const SDL_Event& event) -> bool {
switch (event.key.scancode) { switch (event.key.scancode) {
case SDL_SCANCODE_UP: case SDL_SCANCODE_UP:
moveCursor(-1); moveCursor(-1);
@@ -622,6 +887,101 @@ namespace System {
} }
} }
auto ServiceMenu::handleGamepadButton(const SDL_Event& event) -> bool {
const int BTN = static_cast<int>(event.gbutton.button);
if (BTN == SDL_GAMEPAD_BUTTON_DPAD_UP) {
moveCursor(-1);
return true;
}
if (BTN == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
moveCursor(+1);
return true;
}
if (BTN == SDL_GAMEPAD_BUTTON_DPAD_LEFT) {
changeValue(-1);
return true;
}
if (BTN == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {
changeValue(+1);
return true;
}
// Botons d'accio per al pad emissor: FIRE = ENTER, ACCELERATE = BACK.
if (buttonMatchesAction(event.gbutton.which, BTN, InputAction::SHOOT)) {
activateCurrent();
return true;
}
if (buttonMatchesAction(event.gbutton.which, BTN, InputAction::THRUST)) {
popPage();
return true;
}
return false;
}
void ServiceMenu::processStickX(Sint16 val) {
const bool LEFT_NOW = val < -MENU_STICK_THRESHOLD;
const bool RIGHT_NOW = val > MENU_STICK_THRESHOLD;
if (LEFT_NOW && !stick_left_held_) {
changeValue(-1);
}
if (RIGHT_NOW && !stick_right_held_) {
changeValue(+1);
}
stick_left_held_ = LEFT_NOW;
stick_right_held_ = RIGHT_NOW;
}
void ServiceMenu::processStickY(Sint16 val) {
const bool UP_NOW = val < -MENU_STICK_THRESHOLD;
const bool DOWN_NOW = val > MENU_STICK_THRESHOLD;
if (UP_NOW && !stick_up_held_) {
moveCursor(-1);
}
if (DOWN_NOW && !stick_down_held_) {
moveCursor(+1);
}
stick_up_held_ = UP_NOW;
stick_down_held_ = DOWN_NOW;
}
// Edge-detect d'un trigger: si creua el llindar amunt, despatxa
// ENTER/BACK segons el binding (FIRE/ACCEL) que apunta al codi
// virtual del trigger (100 = L2, 101 = R2).
void ServiceMenu::processTriggerEdge(SDL_JoystickID which, Sint16 val, int virtual_button, bool& held) {
const bool NOW = val > MENU_TRIGGER_THRESHOLD;
if (NOW && !held) {
if (buttonMatchesAction(which, virtual_button, InputAction::SHOOT)) {
activateCurrent();
} else if (buttonMatchesAction(which, virtual_button, InputAction::THRUST)) {
popPage();
}
}
held = NOW;
}
auto ServiceMenu::handleGamepadAxis(const SDL_Event& event) -> bool {
const auto AXIS = static_cast<SDL_GamepadAxis>(event.gaxis.axis);
const Sint16 VAL = event.gaxis.value;
switch (AXIS) {
case SDL_GAMEPAD_AXIS_LEFTX:
processStickX(VAL);
return true;
case SDL_GAMEPAD_AXIS_LEFTY:
processStickY(VAL);
return true;
// Triggers L2/R2: SDL3 nomes emet AXIS_MOTION, no button events.
// Per poder rebindar FIRE/ACCEL als triggers, sintetitzem aqui
// la pulsacio amb edge-detect i la passem pel mateix flux.
case SDL_GAMEPAD_AXIS_LEFT_TRIGGER:
processTriggerEdge(event.gaxis.which, VAL, Input::TRIGGER_L2_AS_BUTTON, trigger_l2_held_);
return true;
case SDL_GAMEPAD_AXIS_RIGHT_TRIGGER:
processTriggerEdge(event.gaxis.which, VAL, Input::TRIGGER_R2_AS_BUTTON, trigger_r2_held_);
return true;
default:
return false;
}
}
auto ServiceMenu::computeTargetHeight() const -> float { auto ServiceMenu::computeTargetHeight() const -> float {
if (stack_.empty()) { if (stack_.empty()) {
return 0.0F; return 0.0F;
+36 -2
View File
@@ -85,19 +85,39 @@ namespace System {
void update(float delta_time); void update(float delta_time);
void draw() const; void draw() const;
// Processa el KEY_DOWN. Retorna true si l'ha consumit (UP/DOWN/ENTER/ // Processa events de navegacio. Retorna true si l'event s'ha consumit.
// RIGHT/BACKSPACE/LEFT mentre esta obert). false en qualsevol altre cas. // Accepta:
// - SDL_EVENT_KEY_DOWN: UP/DOWN/ENTER/RIGHT/LEFT/BACKSPACE.
// - SDL_EVENT_GAMEPAD_BUTTON_DOWN: DPAD per nav, FIRE = ENTER,
// ACCELERATE = BACK. La resta de botons s'ignoren.
// - SDL_EVENT_GAMEPAD_AXIS_MOTION: stick X/Y amb edge-detect.
auto handleEvent(const SDL_Event& event) -> bool; auto handleEvent(const SDL_Event& event) -> bool;
private: private:
ServiceMenu(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay); ServiceMenu(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay);
// Sub-handlers de handleEvent. Privats, no son part de l'API publica.
auto handleKeyDown(const SDL_Event& event) -> bool;
auto handleGamepadButton(const SDL_Event& event) -> bool;
auto handleGamepadAxis(const SDL_Event& event) -> bool;
// Helpers per a cada eix; permeten que handleGamepadAxis es quedi
// com a dispatcher i no bote el llindar de complexitat.
void processStickX(Sint16 val);
void processStickY(Sint16 val);
void processTriggerEdge(SDL_JoystickID which, Sint16 val, int virtual_button, bool& held);
void buildRootPage(); void buildRootPage();
[[nodiscard]] auto buildVideoPage() -> Page; [[nodiscard]] auto buildVideoPage() -> Page;
[[nodiscard]] auto buildResolutionPage() const -> Page; [[nodiscard]] auto buildResolutionPage() const -> Page;
[[nodiscard]] static auto buildAudioPage() -> Page; [[nodiscard]] static auto buildAudioPage() -> Page;
[[nodiscard]] auto buildOptionsPage() const -> Page; [[nodiscard]] auto buildOptionsPage() const -> Page;
[[nodiscard]] auto buildSystemPage() -> Page; [[nodiscard]] auto buildSystemPage() -> Page;
[[nodiscard]] auto buildControlsPage() -> Page;
// Llista de mandos detectats per a un jugador. Cada item assigna el
// pad triat (amb swap automatic si l'altre jugador ja el tenia) i
// tanca la picker amb popPage. L'ultim item es "SENSE MANDO" per a
// desasignar.
[[nodiscard]] auto buildPadPickerPage(int player_index) -> Page;
// Pagina de confirmacio "ESTAS SEGUR? NO/SI". on_yes s'executa si // Pagina de confirmacio "ESTAS SEGUR? NO/SI". on_yes s'executa si
// l'usuari selecciona SI; el cursor per defecte apunta a NO. // l'usuari selecciona SI; el cursor per defecte apunta a NO.
void pushConfirmPage(const std::string& title_key, std::function<void()> on_yes); void pushConfirmPage(const std::string& title_key, std::function<void()> on_yes);
@@ -139,6 +159,20 @@ namespace System {
float highlight_h_ = 0.0F; float highlight_h_ = 0.0F;
bool highlight_snap_ = true; bool highlight_snap_ = true;
// Edge-detect de stick analogic per a navegacio. Una sola activacio
// per direccio: cal tornar a centre (sota el llindar) per disparar
// una altra. Compartit entre tots els pads — qualsevol jugador pot
// navegar el menu.
bool stick_left_held_ = false;
bool stick_right_held_ = false;
bool stick_up_held_ = false;
bool stick_down_held_ = false;
// Edge-detect dels triggers L2/R2 com a botons virtuals. SDL3 no
// emet button events per als triggers; els llegim com a axis i
// sintetitzem una pulsacio quan creuen el llindar.
bool trigger_l2_held_ = false;
bool trigger_r2_held_ = false;
static std::unique_ptr<ServiceMenu> instance; static std::unique_ptr<ServiceMenu> instance;
}; };
+23
View File
@@ -0,0 +1,23 @@
// string_utils.hpp - Utilitats genèriques de cadenes
// © 2026 JailDesigner
//
// VectorText només admet ASCII en majúscules; les notificacions, el menú
// de servei i l'overlay de rebind passen els textos dinàmics per aquest
// helper abans de pintar-los.
#pragma once
#include <cctype>
#include <string>
namespace Utils {
inline auto toUpperAscii(const std::string& s) -> std::string {
std::string result = s;
for (char& c : result) {
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
}
return result;
}
} // namespace Utils
+28 -4
View File
@@ -373,12 +373,21 @@ namespace ConfigYaml {
if (gp.contains("button_shoot")) { if (gp.contains("button_shoot")) {
player1.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>()); player1.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
} }
if (gp.contains("button_start")) {
player1.gamepad.button_start = stringToButton(gp["button_start"].get_value<std::string>());
}
if (gp.contains("button_menu")) {
player1.gamepad.button_menu = stringToButton(gp["button_menu"].get_value<std::string>());
}
} }
// Carregar nom del gamepad // Carregar nom i path del gamepad assignat
if (p1.contains("gamepad_name")) { if (p1.contains("gamepad_name")) {
player1.gamepad_name = p1["gamepad_name"].get_value<std::string>(); player1.gamepad_name = p1["gamepad_name"].get_value<std::string>();
} }
if (p1.contains("gamepad_path")) {
player1.gamepad_path = p1["gamepad_path"].get_value<std::string>();
}
} }
// Carregar controls del player 2 desde YAML // Carregar controls del player 2 desde YAML
@@ -421,12 +430,21 @@ namespace ConfigYaml {
if (gp.contains("button_shoot")) { if (gp.contains("button_shoot")) {
player2.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>()); player2.gamepad.button_shoot = stringToButton(gp["button_shoot"].get_value<std::string>());
} }
if (gp.contains("button_start")) {
player2.gamepad.button_start = stringToButton(gp["button_start"].get_value<std::string>());
}
if (gp.contains("button_menu")) {
player2.gamepad.button_menu = stringToButton(gp["button_menu"].get_value<std::string>());
}
} }
// Carregar nom del gamepad // Carregar nom i path del gamepad assignat
if (p2.contains("gamepad_name")) { if (p2.contains("gamepad_name")) {
player2.gamepad_name = p2["gamepad_name"].get_value<std::string>(); player2.gamepad_name = p2["gamepad_name"].get_value<std::string>();
} }
if (p2.contains("gamepad_path")) {
player2.gamepad_path = p2["gamepad_path"].get_value<std::string>();
}
} }
// Carregar configuración des del file YAML // Carregar configuración des del file YAML
@@ -531,7 +549,10 @@ namespace ConfigYaml {
file << " button_right: " << buttonToString(player1.gamepad.button_right) << "\n"; file << " button_right: " << buttonToString(player1.gamepad.button_right) << "\n";
file << " button_thrust: " << buttonToString(player1.gamepad.button_thrust) << "\n"; file << " button_thrust: " << buttonToString(player1.gamepad.button_thrust) << "\n";
file << " button_shoot: " << buttonToString(player1.gamepad.button_shoot) << "\n"; file << " button_shoot: " << buttonToString(player1.gamepad.button_shoot) << "\n";
file << " gamepad_name: \"" << player1.gamepad_name << "\" # Buit = primer disponible\n\n"; file << " button_start: " << buttonToString(player1.gamepad.button_start) << "\n";
file << " button_menu: " << buttonToString(player1.gamepad.button_menu) << "\n";
file << " gamepad_name: \"" << player1.gamepad_name << "\" # Buit = primer disponible\n";
file << " gamepad_path: \"" << player1.gamepad_path << "\" # Prioritari sobre name\n\n";
} }
// Guardar controls del player 2 a YAML // Guardar controls del player 2 a YAML
@@ -548,7 +569,10 @@ namespace ConfigYaml {
file << " button_right: " << buttonToString(player2.gamepad.button_right) << "\n"; file << " button_right: " << buttonToString(player2.gamepad.button_right) << "\n";
file << " button_thrust: " << buttonToString(player2.gamepad.button_thrust) << "\n"; file << " button_thrust: " << buttonToString(player2.gamepad.button_thrust) << "\n";
file << " button_shoot: " << buttonToString(player2.gamepad.button_shoot) << "\n"; file << " button_shoot: " << buttonToString(player2.gamepad.button_shoot) << "\n";
file << " gamepad_name: \"" << player2.gamepad_name << "\" # Buit = segon disponible\n\n"; file << " button_start: " << buttonToString(player2.gamepad.button_start) << "\n";
file << " button_menu: " << buttonToString(player2.gamepad.button_menu) << "\n";
file << " gamepad_name: \"" << player2.gamepad_name << "\" # Buit = segon disponible\n";
file << " gamepad_path: \"" << player2.gamepad_path << "\" # Prioritari sobre name\n\n";
} }
// Guardar configuración al file YAML // Guardar configuración al file YAML
+1
View File
@@ -28,6 +28,7 @@ namespace ConfigYaml {
.key_start = SDL_SCANCODE_2, .key_start = SDL_SCANCODE_2,
}, },
.gamepad_name = "", .gamepad_name = "",
.gamepad_path = "",
}, },
}; };
+24 -34
View File
@@ -14,38 +14,39 @@
#include "core/rendering/shape_renderer.hpp" #include "core/rendering/shape_renderer.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/constants.hpp" #include "game/constants.hpp"
#include "game/entities/bullet_config.hpp"
#include "game/entities/bullet_registry.hpp"
Bullet::Bullet(Rendering::Renderer* renderer) Bullet::Bullet(Rendering::Renderer* renderer)
: Entity(renderer) { : Entity(renderer),
// Brightness específico para balas config_(&BulletRegistry::get()) {
brightness_ = Defaults::Brightness::BALA; brightness_ = Defaults::Brightness::BALA;
// Configuración del cuerpo físico. // Cinemàtiques pures: no col·lisionen al PhysicsWorld (body_.radius = 0).
// Las balas son cinemáticas: no colisionan con otros bodies ni paredes. // El gameplay (GameScene) gestiona els hits via checkCollisionSwept i la
// El gameplay (GameScene) gestiona los hits con check_collision y la // sortida del PLAYAREA.
// salida del PLAYAREA. Por eso radius=0 en el world (no participa en body_.setMass(config_->physics.mass);
// resolveBodyCollisions ni resolveBoundsCollisions). body_.radius = 0.0F;
body_.setMass(0.5F); // Ligera (no afecta a nadie, pero por consistencia) body_.restitution = config_->physics.restitution;
body_.radius = 0.0F; // Sin colisión física (cinemática pura) body_.linear_damping = config_->physics.linear_damping;
body_.restitution = 0.0F; // Irrelevante (no rebota) body_.angular_damping = config_->physics.angular_damping;
body_.linear_damping = 0.0F; // Sin fricción (movimiento rectilíneo uniforme)
body_.angular_damping = 0.0F;
// Cargar shape compartida desde archivo shape_ = Graphics::ShapeLoader::load(config_->shape.path);
shape_ = Graphics::ShapeLoader::load("bullet.shp");
if (!shape_ || !shape_->isValid()) { if (!shape_ || !shape_->isValid()) {
std::cerr << "[Bullet] Error: no s'ha pogut load bullet.shp" << '\n'; std::cerr << "[Bullet] Error: no s'ha pogut carregar " << config_->shape.path << '\n';
} }
// Radi de col·lisió derivat del cercle circumscrit de la shape.
const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F;
collision_radius_ = BOUNDING * config_->shape.scale * config_->shape.collision_factor;
} }
void Bullet::init() { void Bullet::init() {
// Inicialment inactiva
is_active_ = false; is_active_ = false;
center_ = {.x = 0.0F, .y = 0.0F}; center_ = {.x = 0.0F, .y = 0.0F};
prev_position_ = {.x = 0.0F, .y = 0.0F}; prev_position_ = {.x = 0.0F, .y = 0.0F};
angle_ = 0.0F; angle_ = 0.0F;
// Reset del cuerpo físico
body_.position = Vec2{}; body_.position = Vec2{};
body_.velocity = Vec2{}; body_.velocity = Vec2{};
body_.angle = 0.0F; body_.angle = 0.0F;
@@ -53,29 +54,24 @@ void Bullet::init() {
body_.clearAccumulators(); body_.clearAccumulators();
} }
void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id) { void Bullet::fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed) {
// Activar bullet
is_active_ = true; is_active_ = true;
// Almacenar propietario (0=P1, 1=P2)
owner_id_ = owner_id; owner_id_ = owner_id;
// Posición y orientación iniciales = ship
center_ = position; center_ = position;
prev_position_ = position; // Al spawn no hi ha moviment encara: swept degenera a punt-cercle prev_position_ = position; // spawn: swept degenera a punt-cercle
angle_ = angle; angle_ = angle;
// Sincronizar el body físico: posición + velocidad cartesiana // Sincronizar el body físic: posició + velocitat cartesiana.
// angle - PI/2 porque angle=0 apunta hacia arriba (eje Y negativo SDL) // angle - PI/2 perquè angle=0 apunta cap amunt (eje Y negatiu SDL).
body_.position = position; body_.position = position;
body_.angle = angle; body_.angle = angle;
const float DIR_X = std::cos(angle - (Constants::PI / 2.0F)); const float DIR_X = std::cos(angle - (Constants::PI / 2.0F));
const float DIR_Y = std::sin(angle - (Constants::PI / 2.0F)); const float DIR_Y = std::sin(angle - (Constants::PI / 2.0F));
body_.velocity = Vec2{.x = DIR_X * Defaults::Game::BULLET_SPEED, .y = DIR_Y * Defaults::Game::BULLET_SPEED}; body_.velocity = Vec2{.x = DIR_X * bullet_speed, .y = DIR_Y * bullet_speed};
body_.angular_velocity = 0.0F; body_.angular_velocity = 0.0F;
body_.clearAccumulators(); body_.clearAccumulators();
// Reproducir sonido de disparo láser
Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME); Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME);
} }
@@ -87,24 +83,18 @@ void Bullet::update(float /*delta_time*/) {
} }
void Bullet::postUpdate(float /*delta_time*/) { void Bullet::postUpdate(float /*delta_time*/) {
// Captura la posició al final del frame anterior abans de sobreescriure center_;
// així el sistema de col·lisions pot fer swept (segment-vs-cercle) entre prev_position_
// i la nova center_, evitant tunneling a velocitats altes.
prev_position_ = center_; prev_position_ = center_;
center_ = body_.position; center_ = body_.position;
// angle_ no cambia (las balas no rotan visualmente).
} }
void Bullet::desactivar() { void Bullet::desactivar() {
is_active_ = false; is_active_ = false;
// Detener el cuerpo físico para que no acumule deriva mientras inactiva.
body_.velocity = Vec2{}; body_.velocity = Vec2{};
body_.angular_velocity = 0.0F; body_.angular_velocity = 0.0F;
} }
void Bullet::draw() const { void Bullet::draw() const {
if (is_active_ && shape_) { if (is_active_ && shape_) {
// Les bales roten segons l'angle de trayectòria (estático tras disparo) Rendering::renderShape(renderer_, shape_, center_, angle_, 1.0F, 1.0F, brightness_, config_->colors.normal);
Rendering::renderShape(renderer_, shape_, center_, angle_, 1.0F, 1.0F, brightness_, Defaults::Palette::BULLET);
} }
} }
+13 -11
View File
@@ -6,10 +6,12 @@
#include <cstdint> #include <cstdint>
#include "core/defaults.hpp"
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/types.hpp" #include "core/types.hpp"
// Forward declaration — la definició completa s'inclou només al .cpp.
struct BulletConfig;
class Bullet : public Entities::Entity { class Bullet : public Entities::Entity {
public: public:
Bullet() Bullet()
@@ -17,7 +19,7 @@ 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); void fire(const Vec2& position, float angle, uint8_t owner_id, float bullet_speed);
void update(float delta_time) override; void update(float delta_time) override;
void postUpdate(float delta_time) override; void postUpdate(float delta_time) override;
void draw() const override; void draw() const override;
@@ -25,25 +27,25 @@ class Bullet : public Entities::Entity {
// Override: Interfaz de Entity // Override: Interfaz de Entity
[[nodiscard]] auto isActive() const -> bool override { return is_active_; } [[nodiscard]] auto isActive() const -> bool override { return is_active_; }
// Override: Interfaz de colisión (gameplay-level: PLAYAREA bounds-check) // Override: Interfaz de colisión (radi derivat al ctor des del shape).
[[nodiscard]] auto getCollisionRadius() const -> float override { [[nodiscard]] auto getCollisionRadius() const -> float override { return collision_radius_; }
return Defaults::Entities::BULLET_RADIUS;
}
[[nodiscard]] auto isCollidable() const -> bool override { [[nodiscard]] auto isCollidable() const -> bool override {
return is_active_; return is_active_;
} }
// Configuració associada (vàlida des del ctor — la bala l'agafa del BulletRegistry).
[[nodiscard]] auto getConfig() const -> const BulletConfig& { return *config_; }
// Getters (API pública sin cambios) // Getters (API pública sin cambios)
[[nodiscard]] auto getOwnerId() const -> uint8_t { return owner_id_; } [[nodiscard]] auto getOwnerId() const -> uint8_t { return owner_id_; }
// Posició al final del frame anterior, per a CCD segment-vs-cercle.
[[nodiscard]] auto getPrevPosition() const -> const Vec2& { return prev_position_; } [[nodiscard]] auto getPrevPosition() const -> const Vec2& { return prev_position_; }
void desactivar(); void desactivar();
private: private:
// Miembros específicos de Bullet (heredados: renderer_, shape_, center_, angle_, brightness_, body_). const BulletConfig* config_{nullptr}; // apunta al BulletRegistry; vàlid post-ctor
// Inicializados en la declaración para que tanto el ctor por defecto como el que toma renderer float collision_radius_{0.0F}; // derivat: shape.bounding_radius × scale × collision_factor
// dejen el objeto en estado coherente (proyectil inactivo, sin owner).
bool is_active_{false}; bool is_active_{false};
uint8_t owner_id_{0}; // 0=P1, 1=P2 uint8_t owner_id_{0}; // 0=P1, 1=P2
Vec2 prev_position_{}; // Posició al final del frame anterior (per a swept collision) Vec2 prev_position_{}; // posició al final del frame anterior (swept CCD)
}; };
+80
View File
@@ -0,0 +1,80 @@
// bullet_config.cpp - Implementació del parser de BulletConfig
// © 2026 JailDesigner
#include "game/entities/bullet_config.hpp"
#include <cstdint>
#include <exception>
#include <iostream>
#include <string>
namespace {
auto parseColor(const fkyaml::node& node, SDL_Color& out) -> bool {
if (!node.is_sequence() || node.size() != 3) {
return false;
}
const auto R = node[0].get_value<uint32_t>();
const auto G = node[1].get_value<uint32_t>();
const auto B = node[2].get_value<uint32_t>();
out = SDL_Color{
.r = static_cast<uint8_t>(R),
.g = static_cast<uint8_t>(G),
.b = static_cast<uint8_t>(B),
.a = 255};
return true;
}
auto parseShape(const fkyaml::node& node, BulletConfig::ShapeCfg& out) -> bool {
if (!node.contains("shape") || !node["shape"].contains("path")) {
std::cerr << "[BulletConfig] Error: falta 'shape.path'\n";
return false;
}
const auto& s = node["shape"];
out.path = s["path"].get_value<std::string>();
out.scale = s.contains("scale") ? s["scale"].get_value<float>() : 1.0F;
out.collision_factor = s.contains("collision_factor")
? s["collision_factor"].get_value<float>()
: 1.0F;
return true;
}
auto parsePhysics(const fkyaml::node& node, BulletConfig::PhysicsCfg& out) -> bool {
if (!node.contains("physics")) {
std::cerr << "[BulletConfig] Error: falta 'physics'\n";
return false;
}
const auto& p = node["physics"];
out.mass = p["mass"].get_value<float>();
out.restitution = p["restitution"].get_value<float>();
out.linear_damping = p["linear_damping"].get_value<float>();
out.angular_damping = p["angular_damping"].get_value<float>();
out.impact_momentum_factor = p["impact_momentum_factor"].get_value<float>();
return true;
}
auto parseColors(const fkyaml::node& node, BulletConfig::ColorsCfg& out) -> bool {
if (!node.contains("colors") || !parseColor(node["colors"]["normal"], out.normal)) {
std::cerr << "[BulletConfig] Error: 'colors.normal' no és [r,g,b]\n";
return false;
}
return true;
}
} // namespace
auto BulletConfig::fromYaml(const fkyaml::node& node) -> std::optional<BulletConfig> {
try {
BulletConfig cfg;
cfg.name = node.contains("name") ? node["name"].get_value<std::string>() : "bullet";
if (!parseShape(node, cfg.shape)) { return std::nullopt; }
if (!parsePhysics(node, cfg.physics)) { return std::nullopt; }
if (!parseColors(node, cfg.colors)) { return std::nullopt; }
return cfg;
} catch (const std::exception& e) {
std::cerr << "[BulletConfig] Excepció parsejant: " << e.what() << '\n';
return std::nullopt;
}
}
+41
View File
@@ -0,0 +1,41 @@
// bullet_config.hpp - Configuració de la bala carregada des de YAML
// © 2026 JailDesigner
//
// Paral·lel a Player/EnemyConfig. Una sola instància a tot el joc (per ara);
// es comparteix entre totes les bales actives via BulletRegistry.
#pragma once
#include <SDL3/SDL.h>
#include <optional>
#include <string>
#include "external/fkyaml_node.hpp"
struct BulletConfig {
struct ShapeCfg {
std::string path;
float scale;
float collision_factor;
};
struct PhysicsCfg {
float mass;
float restitution;
float linear_damping;
float angular_damping;
float impact_momentum_factor; // factor de transferència bullet→enemic
};
struct ColorsCfg {
SDL_Color normal;
};
std::string name;
ShapeCfg shape;
PhysicsCfg physics;
ColorsCfg colors;
static auto fromYaml(const fkyaml::node& node) -> std::optional<BulletConfig>;
};
+37
View File
@@ -0,0 +1,37 @@
// bullet_registry.cpp - Implementació del registre de bala
// © 2026 JailDesigner
#include "game/entities/bullet_registry.hpp"
#include <cstdlib>
#include <iostream>
#include "core/entities/entity_loader.hpp"
BulletConfig BulletRegistry::config;
bool BulletRegistry::loaded = false;
auto BulletRegistry::load() -> bool {
auto yaml = Entities::EntityLoader::load("bullet");
if (!yaml) {
std::cerr << "[BulletRegistry] Error: no s'ha pogut carregar bullet.yaml\n";
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;
std::cout << "[BulletRegistry] Configuració de bala carregada.\n";
return true;
}
auto BulletRegistry::get() -> const BulletConfig& {
if (!loaded) {
std::cerr << "[BulletRegistry] FATAL: get() abans de load()\n";
std::exit(EXIT_FAILURE);
}
return config;
}
+26
View File
@@ -0,0 +1,26 @@
// bullet_registry.hpp - Registre estàtic de la configuració de la bala
// © 2026 JailDesigner
//
// Una única instància per a tota la sessió. Es manté el patró registry
// (paral·lel a EnemyRegistry) tot i ser una sola entitat: si el dia de demà
// hi ha més tipus de bala (laser/plasma/etc.) només cal estendre-ho.
#pragma once
#include "game/entities/bullet_config.hpp"
class BulletRegistry {
public:
BulletRegistry() = delete;
// Carrega data/entities/bullet/bullet.yaml. Retorna false si falla.
static auto load() -> bool;
// Accés a la configuració. Avorta amb log fatal si load() no s'ha cridat
// o ha fallat.
static auto get() -> const BulletConfig&;
private:
static BulletConfig config;
static bool loaded;
};
+84 -181
View File
@@ -14,6 +14,8 @@
#include "core/rendering/shape_renderer.hpp" #include "core/rendering/shape_renderer.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/constants.hpp" #include "game/constants.hpp"
#include "game/entities/enemy_config.hpp"
#include "game/entities/enemy_registry.hpp"
namespace { namespace {
@@ -27,99 +29,72 @@ namespace {
} }
// Recupera el "ángulo equivalente" de un body en movimiento (para zigzag). // Recupera el "ángulo equivalente" de un body en movimiento (para zigzag).
// Si está parado, devuelve 0.
auto velocityToAngle(const Vec2& velocity) -> float { auto velocityToAngle(const Vec2& velocity) -> float {
if (velocity.lengthSquared() < 0.0001F) { if (velocity.lengthSquared() < 0.0001F) {
return 0.0F; return 0.0F;
} }
// El movimiento (vx, vy) corresponde a angle - PI/2; invertimos.
return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F); return std::atan2(velocity.y, velocity.x) + (Constants::PI / 2.0F);
} }
// Random float [0..1).
auto randFloat01() -> float {
return static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
}
// Random float [min..max).
auto randRange(float min, float max) -> float {
return min + (randFloat01() * (max - min));
}
} // namespace } // namespace
Enemy::Enemy(Rendering::Renderer* renderer) Enemy::Enemy(Rendering::Renderer* renderer)
: Entity(renderer), : Entity(renderer) {
tracking_strength_(Defaults::Enemies::Square::TRACKING_STRENGTH) {
brightness_ = Defaults::Brightness::ENEMIC; brightness_ = Defaults::Brightness::ENEMIC;
// Configuración del cuerpo físico — defaults para enemy genérico. // Body queda amb defaults inocus (radius=0 = no col·lisiona) fins
// init() ajusta velocidad y masa según el tipo (Pentagon/Quadrat/Molinillo). // que init() apliqui la configuració del tipus carregada via Registry.
body_.setMass(Defaults::Enemies::Body::DEFAULT_MASS); body_.radius = 0.0F;
body_.radius = 0.0F; // 0 hasta spawn (no colisiona inactivo)
body_.restitution = Defaults::Enemies::Body::RESTITUTION;
body_.linear_damping = Defaults::Enemies::Body::LINEAR_DAMPING;
body_.angular_damping = Defaults::Enemies::Body::ANGULAR_DAMPING;
} }
void Enemy::init(EnemyType type, const Vec2* ship_pos) { void Enemy::init(EnemyType type, const Vec2* ship_pos) {
type_ = type; type_ = type;
config_ = &EnemyRegistry::get(type);
const EnemyConfig& cfg = *config_;
const char* shape_file = nullptr; if (type_ == EnemyType::SQUARE) {
float base_speed = 0.0F;
float rotation_delta_min = 0.0F;
float rotation_delta_max = 0.0F;
float type_mass = Defaults::Enemies::Body::DEFAULT_MASS;
switch (type_) {
case EnemyType::PENTAGON:
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
base_speed = Defaults::Enemies::Pentagon::SPEED;
rotation_delta_min = Defaults::Enemies::Pentagon::ROTATION_DELTA_MIN;
rotation_delta_max = Defaults::Enemies::Pentagon::ROTATION_DELTA_MAX;
type_mass = Defaults::Enemies::Pentagon::MASS;
break;
case EnemyType::SQUARE:
shape_file = Defaults::Enemies::Square::SHAPE_FILE;
base_speed = Defaults::Enemies::Square::SPEED;
rotation_delta_min = Defaults::Enemies::Square::ROTATION_DELTA_MIN;
rotation_delta_max = Defaults::Enemies::Square::ROTATION_DELTA_MAX;
type_mass = Defaults::Enemies::Square::MASS;
tracking_timer_ = 0.0F; tracking_timer_ = 0.0F;
break; tracking_strength_ = cfg.behavior.tracking_strength;
case EnemyType::PINWHEEL:
shape_file = Defaults::Enemies::Pinwheel::SHAPE_FILE;
base_speed = Defaults::Enemies::Pinwheel::SPEED;
rotation_delta_min = Defaults::Enemies::Pinwheel::ROTATION_DELTA_MIN;
rotation_delta_max = Defaults::Enemies::Pinwheel::ROTATION_DELTA_MAX;
type_mass = Defaults::Enemies::Pinwheel::MASS;
break;
default:
std::cerr << "[Enemy] Error: tipo desconocido ("
<< static_cast<int>(type_) << "), usando PENTAGON\n";
shape_file = Defaults::Enemies::Pentagon::SHAPE_FILE;
base_speed = Defaults::Enemies::Pentagon::SPEED;
rotation_delta_min = Defaults::Enemies::Pentagon::ROTATION_DELTA_MIN;
rotation_delta_max = Defaults::Enemies::Pentagon::ROTATION_DELTA_MAX;
break;
} }
body_.setMass(type_mass); shape_ = Graphics::ShapeLoader::load(cfg.shape.path);
body_.radius = Defaults::Entities::ENEMY_RADIUS;
// Cargar shape
shape_ = Graphics::ShapeLoader::load(shape_file);
if (!shape_ || !shape_->isValid()) { if (!shape_ || !shape_->isValid()) {
std::cerr << "[Enemy] Error: no se ha podido cargar " << shape_file << '\n'; std::cerr << "[Enemy] Error: no se ha podido cargar " << cfg.shape.path << '\n';
} }
// Posición aleatoria con comprobación de seguridad // Radi de col·lisió derivat del cercle circumscrit de la shape.
const float BOUNDING = (shape_ != nullptr) ? shape_->getBoundingRadius() : 0.0F;
collision_radius_ = BOUNDING * cfg.shape.scale * cfg.shape.collision_factor;
body_.setMass(cfg.physics.mass);
body_.radius = collision_radius_;
body_.restitution = cfg.physics.restitution;
body_.linear_damping = cfg.physics.linear_damping;
body_.angular_damping = cfg.physics.angular_damping;
// Posició aleatòria amb comprovació de safety_distance.
float min_x; float min_x;
float max_x; float max_x;
float min_y; float min_y;
float max_y; float max_y;
Constants::getSafePlayAreaBounds(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y); Constants::getSafePlayAreaBounds(collision_radius_, min_x, max_x, min_y, max_y);
if (ship_pos != nullptr) { if (ship_pos != nullptr) {
bool found_safe_position = false; bool found_safe_position = false;
for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) { for (int attempt = 0; attempt < Defaults::Enemies::Spawn::MAX_SPAWN_ATTEMPTS; attempt++) {
float candidate_x; float candidate_x;
float candidate_y; float candidate_y;
if (attemptSafeSpawn(*ship_pos, candidate_x, candidate_y)) { if (attemptSafeSpawn(*ship_pos, collision_radius_, cfg.spawn.safety_distance, candidate_x, candidate_y)) {
center_.x = candidate_x; center_.x = candidate_x;
center_.y = candidate_y; center_.y = candidate_y;
found_safe_position = true; found_safe_position = true;
@@ -141,32 +116,26 @@ void Enemy::init(EnemyType type, const Vec2* ship_pos) {
center_.y = static_cast<float>((std::rand() % RANGE_Y) + static_cast<int>(min_y)); center_.y = static_cast<float>((std::rand() % RANGE_Y) + static_cast<int>(min_y));
} }
// Dirección inicial aleatoria, velocidad escalar según tipo const float ANGLE_INICIAL = static_cast<float>(std::rand() % 360) * Constants::PI / 180.0F;
const float ANGLE_INICIAL = (std::rand() % 360) * Constants::PI / 180.0F; setVelocityFromAngle(ANGLE_INICIAL, cfg.physics.speed);
setVelocityFromAngle(ANGLE_INICIAL, base_speed);
// Sincronizar body_ con posición inicial
body_.position = center_; body_.position = center_;
body_.angle = 0.0F; body_.angle = 0.0F;
body_.angular_velocity = 0.0F; body_.angular_velocity = 0.0F;
body_.clearAccumulators(); body_.clearAccumulators();
// Rotación visual aleatoria (independiente del body) // Rotació visual aleatòria dins del rang del tipus
const float ROTATION_DELTA_RANGE = rotation_delta_max - rotation_delta_min; rotation_delta_ = randRange(cfg.physics.rotation_delta_min, cfg.physics.rotation_delta_max);
rotation_delta_ = rotation_delta_min + ((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * ROTATION_DELTA_RANGE);
rotation_ = 0.0F; rotation_ = 0.0F;
// Estado de animación
animation_ = EnemyAnimation(); animation_ = EnemyAnimation();
animation_.rotation_delta_base = rotation_delta_; animation_.rotation_delta_base = rotation_delta_;
animation_.rotation_delta_target = rotation_delta_; animation_.rotation_delta_target = rotation_delta_;
animation_.rotation_delta_t = 1.0F; animation_.rotation_delta_t = 1.0F;
// Invulnerabilidad post-spawn invulnerability_timer_ = cfg.spawn.invulnerability_duration;
invulnerability_timer_ = Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; brightness_ = cfg.spawn.invulnerability_brightness_start;
brightness_ = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START;
// Timer para próximo cambio de dirección (Pentagon)
direction_change_timer_ = 0.0F; direction_change_timer_ = 0.0F;
is_active_ = true; is_active_ = true;
@@ -177,8 +146,6 @@ void Enemy::update(float delta_time) {
return; return;
} }
// Decremento de timer "herido"; al cruzar 0 marca expiración para que el
// system layer dispare la explosión diferida.
wound_expired_this_frame_ = false; wound_expired_this_frame_ = false;
if (wounded_timer_ > 0.0F) { if (wounded_timer_ > 0.0F) {
wounded_timer_ -= delta_time; wounded_timer_ -= delta_time;
@@ -188,25 +155,24 @@ void Enemy::update(float delta_time) {
} }
} }
// Decremento de invulnerabilidad + LERP de brightness
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);
const float T_INV = invulnerability_timer_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; const float T_INV = invulnerability_timer_ / config_->spawn.invulnerability_duration;
const float T = 1.0F - T_INV; const float T = 1.0F - T_INV;
const float SMOOTH_T = T * T * (3.0F - (2.0F * T)); const float SMOOTH_T = T * T * (3.0F - (2.0F * T));
constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_START; const float START = config_->spawn.invulnerability_brightness_start;
constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_BRIGHTNESS_END; const float END = config_->spawn.invulnerability_brightness_end;
brightness_ = START + ((END - START) * SMOOTH_T); brightness_ = START + ((END - START) * SMOOTH_T);
} }
// Comportamiento por tipo (ajusta body_.velocity, NO mueve posición).
// Skip cuando está herido: el enemy és un "cos mort" inert, només
// respon a la inèrcia del impulse rebut i a les col·lisions físiques.
if (!isWounded()) { if (!isWounded()) {
switch (type_) { switch (type_) {
case EnemyType::PENTAGON: case EnemyType::PENTAGON:
case EnemyType::STAR:
// STAR reusa el zigzag esquivador de Pentagon. Si en el futur
// vol comportament propi, separa-li el cas.
behaviorPentagon(delta_time); behaviorPentagon(delta_time);
break; break;
case EnemyType::SQUARE: case EnemyType::SQUARE:
@@ -218,15 +184,12 @@ void Enemy::update(float delta_time) {
} }
} }
// Animaciones (palpitación + rotación acelerada)
updateAnimation(delta_time); updateAnimation(delta_time);
// Rotación visual (decoración, no afecta movimiento)
rotation_ += rotation_delta_ * delta_time; rotation_ += rotation_delta_ * delta_time;
} }
void Enemy::postUpdate(float /*delta_time*/) { void Enemy::postUpdate(float /*delta_time*/) {
// Sincronizar mirror tras la integración del world.
if (is_active_) { if (is_active_) {
center_ = body_.position; center_ = body_.position;
} }
@@ -236,27 +199,14 @@ void Enemy::draw() const {
if (!is_active_ || !shape_) { if (!is_active_ || !shape_) {
return; return;
} }
const float SCALE = computeCurrentScale(); const float SCALE = config_->shape.scale * computeCurrentScale();
SDL_Color color{}; SDL_Color color = config_->colors.normal;
switch (type_) {
case EnemyType::PENTAGON:
color = Defaults::Palette::PENTAGON;
break;
case EnemyType::SQUARE:
color = Defaults::Palette::SQUARE;
break;
case EnemyType::PINWHEEL:
color = Defaults::Palette::PINWHEEL;
break;
}
// Parpadeo dorado mientras está herido: alterna color de tipo ↔ dorado
// a Wounded::BLINK_HZ usando el timer (fmod sobre el periodo).
if (wounded_timer_ > 0.0F) { if (wounded_timer_ > 0.0F) {
const float CYCLE = 1.0F / Defaults::Enemies::Wounded::BLINK_HZ; const float CYCLE = 1.0F / config_->wounded.blink_hz;
const float T = std::fmod(wounded_timer_, CYCLE); const float T = std::fmod(wounded_timer_, CYCLE);
if (T < (CYCLE / 2.0F)) { if (T < (CYCLE / 2.0F)) {
color = Defaults::Palette::WOUNDED; color = config_->colors.wounded;
} }
} }
@@ -267,17 +217,15 @@ void Enemy::destroy() {
is_active_ = false; is_active_ = false;
body_.velocity = Vec2{}; body_.velocity = Vec2{};
body_.angular_velocity = 0.0F; body_.angular_velocity = 0.0F;
body_.radius = 0.0F; // No colisiona mientras está inactivo body_.radius = 0.0F;
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;
} }
void Enemy::hurt(uint8_t shooter_id) { void Enemy::hurt(uint8_t shooter_id) {
wounded_timer_ = Defaults::Enemies::Wounded::DURATION; wounded_timer_ = config_->wounded.duration;
last_hit_by_ = shooter_id; last_hit_by_ = shooter_id;
// El so HIT ara el reprodueix la bala quan es trenca en debris
// (Systems::Collision::breakBullet), no l'enemic en entrar a HURT.
} }
void Enemy::applyImpulse(const Vec2& impulse) { void Enemy::applyImpulse(const Vec2& impulse) {
@@ -285,13 +233,11 @@ void Enemy::applyImpulse(const Vec2& impulse) {
} }
void Enemy::setVelocity(float speed) { void Enemy::setVelocity(float speed) {
// Mantener la dirección actual del body, cambiar solo la magnitud.
const float CURRENT_SPEED = body_.velocity.length(); const float CURRENT_SPEED = body_.velocity.length();
if (CURRENT_SPEED > 0.0F) { if (CURRENT_SPEED > 0.0F) {
body_.velocity = body_.velocity * (speed / CURRENT_SPEED); body_.velocity = body_.velocity * (speed / CURRENT_SPEED);
} else { } else {
// Sin dirección actual: usar ángulo aleatorio const float A = static_cast<float>(std::rand() % 360) * Constants::PI / 180.0F;
const float A = (std::rand() % 360) * Constants::PI / 180.0F;
setVelocityFromAngle(A, speed); setVelocityFromAngle(A, speed);
} }
} }
@@ -300,19 +246,14 @@ void Enemy::setVelocityFromAngle(float angle_movement, float speed) {
body_.velocity = angleToDirection(angle_movement) * speed; body_.velocity = angleToDirection(angle_movement) * speed;
} }
// PENTAGON: zigzag esquivador. Cambios de dirección periódicos (probabilísticos) // PENTAGON: zigzag esquivador. Canvis de direcció periòdics (probabilístics)
// en lugar de detectar paredes; el rebote contra muros lo hace PhysicsWorld // en lloc de detectar parets; el rebot contra murs el fa PhysicsWorld.
// con restitution=1.0.
void Enemy::behaviorPentagon(float delta_time) { void Enemy::behaviorPentagon(float delta_time) {
direction_change_timer_ += delta_time; direction_change_timer_ += delta_time;
// Probabilidad de zigzag por segundo (calibrada para sensación equivalente if (randFloat01() < config_->behavior.zigzag_prob_per_second * delta_time) {
// a la versión vieja que disparaba en cada toque de pared).
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
if (RAND_VAL < Defaults::Enemies::Pentagon::ZIGZAG_PROB_PER_SECOND * delta_time) {
const float CURRENT_ANGLE = velocityToAngle(body_.velocity); const float CURRENT_ANGLE = velocityToAngle(body_.velocity);
const float DELTA = (static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * const float DELTA = randFloat01() * config_->behavior.angle_change_max;
Defaults::Enemies::Pentagon::ANGLE_CHANGE_MAX;
const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA); const float NEW_ANGLE = CURRENT_ANGLE + ((std::rand() % 2 == 0) ? DELTA : -DELTA);
const float SPEED = body_.velocity.length(); const float SPEED = body_.velocity.length();
setVelocityFromAngle(NEW_ANGLE, SPEED); setVelocityFromAngle(NEW_ANGLE, SPEED);
@@ -320,12 +261,11 @@ void Enemy::behaviorPentagon(float delta_time) {
} }
} }
// SQUARE: tracking discreto cada TRACKING_INTERVAL. Ajusta dirección // SQUARE: tracking discret cap a la nau cada N segons.
// hacia el ship mezclando con tracking_strength_.
void Enemy::behaviorSquare(float delta_time) { void Enemy::behaviorSquare(float delta_time) {
tracking_timer_ += delta_time; tracking_timer_ += delta_time;
if (tracking_timer_ >= Defaults::Enemies::Square::TRACKING_INTERVAL && ship_position_ != nullptr) { if (tracking_timer_ >= config_->behavior.tracking_interval && ship_position_ != nullptr) {
tracking_timer_ = 0.0F; tracking_timer_ = 0.0F;
const Vec2 TO_SHIP = *ship_position_ - center_; const Vec2 TO_SHIP = *ship_position_ - center_;
@@ -335,11 +275,9 @@ void Enemy::behaviorSquare(float delta_time) {
const float SPEED = body_.velocity.length(); const float SPEED = body_.velocity.length();
const Vec2 DESIRED_VEL = DESIRED_DIR * SPEED; const Vec2 DESIRED_VEL = DESIRED_DIR * SPEED;
// Mezcla LERP: velocidad actual con la deseada según tracking_strength_.
body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) + body_.velocity = (body_.velocity * (1.0F - tracking_strength_)) +
(DESIRED_VEL * tracking_strength_); (DESIRED_VEL * tracking_strength_);
// Renormalizar a la velocidad escalar original
const float NEW_SPEED = body_.velocity.length(); const float NEW_SPEED = body_.velocity.length();
if (NEW_SPEED > 0.0F) { if (NEW_SPEED > 0.0F) {
body_.velocity = body_.velocity * (SPEED / NEW_SPEED); body_.velocity = body_.velocity * (SPEED / NEW_SPEED);
@@ -348,21 +286,17 @@ void Enemy::behaviorSquare(float delta_time) {
} }
} }
// PINWHEEL: movimiento recto + boost de rotación visual cerca del ship. // PINWHEEL: movement rectilini + boost de rotació visual prop del ship.
// Sin tracking — solo cambios de dirección raros (igual que Pentagon pero
// con probabilidad mucho menor).
void Enemy::behaviorPinwheel(float /*delta_time*/) { void Enemy::behaviorPinwheel(float /*delta_time*/) {
// Boost de rotación visual por proximidad al ship
if (ship_position_ != nullptr) { if (ship_position_ != nullptr) {
const Vec2 TO_SHIP = *ship_position_ - center_; const Vec2 TO_SHIP = *ship_position_ - center_;
const float DIST = TO_SHIP.length(); const float DIST = TO_SHIP.length();
if (DIST < Defaults::Enemies::Pinwheel::PROXIMITY_DISTANCE) { if (DIST < config_->behavior.proximity_distance) {
rotation_delta_ = animation_.rotation_delta_base * Defaults::Enemies::Pinwheel::ROTATION_DELTA_PROXIMITY_MULTIPLIER; rotation_delta_ = animation_.rotation_delta_base * config_->behavior.rotation_proximity_multiplier;
} else { } else {
rotation_delta_ = animation_.rotation_delta_base; rotation_delta_ = animation_.rotation_delta_base;
} }
} }
// Movimiento lineal puro: el world se encarga de integrar y rebotar.
} }
void Enemy::updateAnimation(float delta_time) { void Enemy::updateAnimation(float delta_time) {
@@ -371,38 +305,26 @@ void Enemy::updateAnimation(float delta_time) {
} }
void Enemy::updatePulse(float delta_time) { void Enemy::updatePulse(float delta_time) {
const auto& cfg = config_->animation.pulse;
if (animation_.pulse_active) { if (animation_.pulse_active) {
animation_.pulse_phase += 2.0F * Constants::PI * animation_.pulse_frequency * delta_time; animation_.pulse_phase += 2.0F * Constants::PI * animation_.pulse_frequency * delta_time;
animation_.pulse_time_remaining -= delta_time; animation_.pulse_time_remaining -= delta_time;
if (animation_.pulse_time_remaining <= 0.0F) { if (animation_.pulse_time_remaining <= 0.0F) {
animation_.pulse_active = false; animation_.pulse_active = false;
} }
} else { return;
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX); }
const float TRIGGER_PROB = Defaults::Enemies::Animation::PULSE_TRIGGER_PROB * delta_time; if (randFloat01() < cfg.trigger_prob_per_second * delta_time) {
if (RAND_VAL < TRIGGER_PROB) {
animation_.pulse_active = true; animation_.pulse_active = true;
animation_.pulse_phase = 0.0F; animation_.pulse_phase = 0.0F;
animation_.pulse_frequency = randRange(cfg.frequency_min, cfg.frequency_max);
const float FREQ_RANGE = Defaults::Enemies::Animation::PULSE_FREQ_MAX - animation_.pulse_amplitude = randRange(cfg.amplitude_min, cfg.amplitude_max);
Defaults::Enemies::Animation::PULSE_FREQ_MIN; animation_.pulse_time_remaining = randRange(cfg.duration_min, cfg.duration_max);
animation_.pulse_frequency = Defaults::Enemies::Animation::PULSE_FREQ_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * FREQ_RANGE);
const float AMP_RANGE = Defaults::Enemies::Animation::PULSE_AMPLITUD_MAX -
Defaults::Enemies::Animation::PULSE_AMPLITUD_MIN;
animation_.pulse_amplitude = Defaults::Enemies::Animation::PULSE_AMPLITUD_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * AMP_RANGE);
const float DUR_RANGE = Defaults::Enemies::Animation::PULSE_DURATION_MAX -
Defaults::Enemies::Animation::PULSE_DURATION_MIN;
animation_.pulse_time_remaining = Defaults::Enemies::Animation::PULSE_DURATION_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE);
}
} }
} }
void Enemy::updateRotationAcceleration(float delta_time) { void Enemy::updateRotationAcceleration(float delta_time) {
const auto& cfg = config_->animation.rotation_accel;
if (animation_.rotation_delta_t < 1.0F) { if (animation_.rotation_delta_t < 1.0F) {
animation_.rotation_delta_t += delta_time / animation_.rotation_delta_duration; animation_.rotation_delta_t += delta_time / animation_.rotation_delta_duration;
if (animation_.rotation_delta_t >= 1.0F) { if (animation_.rotation_delta_t >= 1.0F) {
@@ -416,34 +338,24 @@ void Enemy::updateRotationAcceleration(float delta_time) {
const float TARGET = animation_.rotation_delta_target; const float TARGET = animation_.rotation_delta_target;
rotation_delta_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T); rotation_delta_ = INITIAL + ((TARGET - INITIAL) * SMOOTH_T);
} }
} else { return;
const float RAND_VAL = static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX);
const float TRIGGER_PROB = Defaults::Enemies::Animation::ROTATION_ACCEL_TRIGGER_PROB * delta_time;
if (RAND_VAL < TRIGGER_PROB) {
animation_.rotation_delta_t = 0.0F;
const float MULT_RANGE = Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MAX -
Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MIN;
const float MULTIPLIER = Defaults::Enemies::Animation::ROTATION_ACCEL_MULTIPLIER_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * MULT_RANGE);
animation_.rotation_delta_target = animation_.rotation_delta_base * MULTIPLIER;
const float DUR_RANGE = Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MAX -
Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MIN;
animation_.rotation_delta_duration = Defaults::Enemies::Animation::ROTATION_ACCEL_DURATION_MIN +
((static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX)) * DUR_RANGE);
} }
if (randFloat01() < cfg.trigger_prob_per_second * delta_time) {
animation_.rotation_delta_t = 0.0F;
const float MULTIPLIER = randRange(cfg.multiplier_min, cfg.multiplier_max);
animation_.rotation_delta_target = animation_.rotation_delta_base * MULTIPLIER;
animation_.rotation_delta_duration = randRange(cfg.duration_min, cfg.duration_max);
} }
} }
auto Enemy::computeCurrentScale() const -> float { auto Enemy::computeCurrentScale() const -> float {
float scale = 1.0F; float scale = 1.0F;
if (invulnerability_timer_ > 0.0F) { if (invulnerability_timer_ > 0.0F) {
const float T_INV = invulnerability_timer_ / Defaults::Enemies::Spawn::INVULNERABILITY_DURATION; const float T_INV = invulnerability_timer_ / config_->spawn.invulnerability_duration;
const float T = 1.0F - T_INV; const float T = 1.0F - T_INV;
const float SMOOTH_T = T * T * (3.0F - (2.0F * T)); const float SMOOTH_T = T * T * (3.0F - (2.0F * T));
constexpr float START = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_START; const float START = config_->spawn.invulnerability_scale_start;
constexpr float END = Defaults::Enemies::Spawn::INVULNERABILITY_SCALE_END; const float END = config_->spawn.invulnerability_scale_end;
scale = START + ((END - START) * SMOOTH_T); scale = START + ((END - START) * SMOOTH_T);
} else if (animation_.pulse_active) { } else if (animation_.pulse_active) {
scale += animation_.pulse_amplitude * std::sin(animation_.pulse_phase); scale += animation_.pulse_amplitude * std::sin(animation_.pulse_phase);
@@ -452,16 +364,7 @@ auto Enemy::computeCurrentScale() const -> float {
} }
auto Enemy::getBaseVelocity() const -> float { auto Enemy::getBaseVelocity() const -> float {
switch (type_) { return EnemyRegistry::get(type_).physics.speed;
case EnemyType::PENTAGON:
return Defaults::Enemies::Pentagon::SPEED;
case EnemyType::SQUARE:
return Defaults::Enemies::Square::SPEED;
case EnemyType::PINWHEEL:
return Defaults::Enemies::Pinwheel::SPEED;
default:
return Defaults::Enemies::Pentagon::SPEED;
}
} }
auto Enemy::getBaseRotation() const -> float { auto Enemy::getBaseRotation() const -> float {
@@ -474,12 +377,12 @@ void Enemy::setTrackingStrength(float strength) {
} }
} }
auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -> bool { auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float collision_radius, float safety_distance, float& out_x, float& out_y) -> bool {
float min_x; float min_x;
float max_x; float max_x;
float min_y; float min_y;
float max_y; float max_y;
Constants::getSafePlayAreaBounds(Defaults::Entities::ENEMY_RADIUS, min_x, max_x, min_y, max_y); Constants::getSafePlayAreaBounds(collision_radius, min_x, max_x, min_y, max_y);
const int RANGE_X = static_cast<int>(max_x - min_x); const int RANGE_X = static_cast<int>(max_x - min_x);
const int RANGE_Y = static_cast<int>(max_y - min_y); const int RANGE_Y = static_cast<int>(max_y - min_y);
@@ -489,5 +392,5 @@ auto Enemy::attemptSafeSpawn(const Vec2& ship_pos, float& out_x, float& out_y) -
const float DX = out_x - ship_pos.x; const float DX = out_x - ship_pos.x;
const float DY = out_y - ship_pos.y; const float DY = out_y - ship_pos.y;
const float DISTANCE = std::sqrt((DX * DX) + (DY * DY)); const float DISTANCE = std::sqrt((DX * DX) + (DY * DY));
return DISTANCE >= Defaults::Enemies::Spawn::SAFETY_DISTANCE; return DISTANCE >= safety_distance;
} }
+28 -19
View File
@@ -6,7 +6,6 @@
#include <cstdint> #include <cstdint>
#include "core/defaults.hpp"
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/types.hpp" #include "core/types.hpp"
@@ -14,9 +13,13 @@
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)
}; };
// Forward declaration — EnemyConfig viu a enemy_config.hpp i s'inclou només a enemy.cpp.
struct EnemyConfig;
// Estado de animación (palpitación + rotación acelerada) // Estado de animación (palpitación + rotación acelerada)
struct EnemyAnimation { struct EnemyAnimation {
// Palpitación (efecto respiración) // Palpitación (efecto respiración)
@@ -48,10 +51,8 @@ class Enemy : public Entities::Entity {
// Override: Interfaz de Entity // Override: Interfaz de Entity
[[nodiscard]] auto isActive() const -> bool override { return is_active_; } [[nodiscard]] auto isActive() const -> bool override { return is_active_; }
// Override: Interfaz de colisión // Override: Interfaz de colisión. El radi ve del config carregat per tipus.
[[nodiscard]] auto getCollisionRadius() const -> float override { [[nodiscard]] auto getCollisionRadius() const -> float override { return collision_radius_; }
return Defaults::Entities::ENEMY_RADIUS;
}
// Mentre fa spawn (invulnerable) segueix col·lisionant: les bales el // Mentre fa spawn (invulnerable) segueix col·lisionant: les bales el
// poden abatre i el cos físic rebota amb la nau. El damage a la nau // poden abatre i el cos físic rebota amb la nau. El damage a la nau
// segueix filtrat per `isInvulnerable()` al detectShipEnemy. // segueix filtrat per `isInvulnerable()` al detectShipEnemy.
@@ -65,6 +66,9 @@ class Enemy : public Entities::Entity {
// Getters // Getters
[[nodiscard]] auto getRotationDelta() const -> float { return rotation_delta_; } [[nodiscard]] auto getRotationDelta() const -> float { return rotation_delta_; }
[[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; } [[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; }
// Configuració activa (carregada al darrer init()). Vàlida mentre l'enemic
// ha estat inicialitzat almenys un cop; abans és nullptr.
[[nodiscard]] auto getConfig() const -> const EnemyConfig& { return *config_; }
// Set ship position reference for tracking behavior // Set ship position reference for tracking behavior
void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; } void setShipPosition(const Vec2* ship_pos) { ship_position_ = ship_pos; }
@@ -101,29 +105,34 @@ class Enemy : public Entities::Entity {
void applyImpulse(const Vec2& impulse); void applyImpulse(const Vec2& impulse);
private: private:
// Miembros específicos (heredados: renderer_, shape_, center_, angle_, brightness_, body_). // Configuració carregada per tipus (apunta a una entrada de EnemyRegistry).
// Inicializados en la declaración: el ctor por defecto deja al enemy en estado "inactivo // nullptr abans del primer init(); per això getConfig() només és vàlid post-init.
// como pentágono", coherente con lo que harán init() o el ctor con renderer al activarlo. const EnemyConfig* config_{nullptr};
float rotation_delta_{0.0F}; // Velocidad angular visual (rad/s) — solo decoración, separada de body_.angular_velocity
float rotation_{0.0F}; // Rotación visual acumulada (no afecta movimiento) // Cache local del radi (per evitar dereferenciar config_ a getCollisionRadius);
// s'actualitza a init() segons el tipus.
float collision_radius_{0.0F};
float rotation_delta_{0.0F}; // Velocidad angular visual (rad/s)
float rotation_{0.0F}; // Rotación visual acumulada
bool is_active_{false}; bool is_active_{false};
EnemyType type_{EnemyType::PENTAGON}; EnemyType type_{EnemyType::PENTAGON};
EnemyAnimation animation_; EnemyAnimation animation_;
// Comportamiento type-specific // Comportamiento type-specific
float tracking_timer_{0.0F}; // Quadrat: tiempo desde último update de dirección float tracking_timer_{0.0F};
const Vec2* ship_position_{nullptr}; // Puntero a posición de la nave (para tracking) const Vec2* ship_position_{nullptr};
float tracking_strength_{0.0F}; // Quadrat: intensidad de tracking (0.0-1.5), default 0.5 float tracking_strength_{0.0F};
float direction_change_timer_{0.0F}; // Pentagon: tiempo para próximo cambio de dirección float direction_change_timer_{0.0F};
// Invulnerabilidad post-spawn // Invulnerabilidad post-spawn
float invulnerability_timer_{0.0F}; float invulnerability_timer_{0.0F};
// Estado "herido": timer cuenta atrás; al cruzar 0 se marca expiración. // Estado "herido"
float wounded_timer_{0.0F}; float wounded_timer_{0.0F};
bool wound_expired_this_frame_{false}; bool wound_expired_this_frame_{false};
uint8_t last_hit_by_{0xFF}; // 0xFF = sin atribución uint8_t last_hit_by_{0xFF};
// Métodos privados // Métodos privados
void updateAnimation(float delta_time); void updateAnimation(float delta_time);
@@ -133,8 +142,8 @@ class Enemy : public Entities::Entity {
void behaviorSquare(float delta_time); void behaviorSquare(float delta_time);
void behaviorPinwheel(float delta_time); void behaviorPinwheel(float delta_time);
[[nodiscard]] auto computeCurrentScale() const -> float; [[nodiscard]] auto computeCurrentScale() const -> float;
// Estático: solo opera sobre ship_pos pasado; no consulta estado del enemy. // Static: passa els paràmetres com a args per no acoblar a *this.
static auto attemptSafeSpawn(const Vec2& ship_pos, 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. // Helper: setear body_.velocity desde un ángulo y magnitud.
// angle_movement=0 apunta hacia arriba (eje Y negativo SDL). // angle_movement=0 apunta hacia arriba (eje Y negativo SDL).
+277
View File
@@ -0,0 +1,277 @@
// enemy_config.cpp - Implementació del parser de EnemyConfig
// © 2026 JailDesigner
#include "game/entities/enemy_config.hpp"
#include <cstdint>
#include <exception>
#include <iostream>
#include <string>
namespace {
auto parseColor(const fkyaml::node& node, SDL_Color& out) -> bool {
if (!node.is_sequence() || node.size() != 3) {
return false;
}
const auto R = node[0].get_value<uint32_t>();
const auto G = node[1].get_value<uint32_t>();
const auto B = node[2].get_value<uint32_t>();
out = SDL_Color{
.r = static_cast<uint8_t>(R),
.g = static_cast<uint8_t>(G),
.b = static_cast<uint8_t>(B),
.a = 255};
return true;
}
auto aiTypeFromString(const std::string& s) -> std::optional<EnemyType> {
if (s == "pentagon") { return EnemyType::PENTAGON; }
if (s == "square") { return EnemyType::SQUARE; }
if (s == "pinwheel") { return EnemyType::PINWHEEL; }
if (s == "star") { return EnemyType::STAR; }
return std::nullopt;
}
// Cada parseXxx valida + omple la sub-struct corresponent. Retornen false
// amb log si falta un camp requerit. Separar-los baixa la complexitat
// cognitiva del fromYaml() principal.
auto parseAiType(const fkyaml::node& node, EnemyType expected, const std::string& name, EnemyType& out) -> bool {
if (!node.contains("ai_type")) {
std::cerr << "[EnemyConfig] Error: falta 'ai_type' a " << name << '\n';
return false;
}
const auto AI_STR = node["ai_type"].get_value<std::string>();
const auto PARSED = aiTypeFromString(AI_STR);
if (!PARSED) {
std::cerr << "[EnemyConfig] Error: ai_type desconegut '" << AI_STR << "' a " << name << '\n';
return false;
}
if (*PARSED != expected) {
std::cerr << "[EnemyConfig] Error: ai_type '" << AI_STR
<< "' no coincideix amb el tipus esperat (per directori) a " << name << '\n';
return false;
}
out = *PARSED;
return true;
}
auto parseShape(const fkyaml::node& node, const std::string& name, EnemyConfig::ShapeCfg& out) -> bool {
if (!node.contains("shape") || !node["shape"].contains("path")) {
std::cerr << "[EnemyConfig] Error: falta 'shape.path' a " << name << '\n';
return false;
}
const auto& shape = node["shape"];
out.path = shape["path"].get_value<std::string>();
out.scale = shape.contains("scale") ? shape["scale"].get_value<float>() : 1.0F;
out.collision_factor = shape.contains("collision_factor")
? shape["collision_factor"].get_value<float>()
: 1.0F;
return true;
}
auto parsePhysics(const fkyaml::node& node, const std::string& name, EnemyConfig::PhysicsCfg& out) -> bool {
if (!node.contains("physics")) {
std::cerr << "[EnemyConfig] Error: falta 'physics' a " << name << '\n';
return false;
}
const auto& p = node["physics"];
out.mass = p["mass"].get_value<float>();
out.speed = p["speed"].get_value<float>();
out.rotation_delta_min = p["rotation_delta_min"].get_value<float>();
out.rotation_delta_max = p["rotation_delta_max"].get_value<float>();
out.restitution = p["restitution"].get_value<float>();
out.linear_damping = p["linear_damping"].get_value<float>();
out.angular_damping = p["angular_damping"].get_value<float>();
return true;
}
auto parseAnimation(const fkyaml::node& node, const std::string& name, EnemyConfig::AnimationCfg& out) -> bool {
if (!node.contains("animation") ||
!node["animation"].contains("pulse") ||
!node["animation"].contains("rotation_accel")) {
std::cerr << "[EnemyConfig] Error: falta 'animation.pulse' o 'animation.rotation_accel' a " << name << '\n';
return false;
}
const auto& p = node["animation"]["pulse"];
out.pulse.trigger_prob_per_second = p["trigger_prob_per_second"].get_value<float>();
out.pulse.duration_min = p["duration_min"].get_value<float>();
out.pulse.duration_max = p["duration_max"].get_value<float>();
out.pulse.amplitude_min = p["amplitude_min"].get_value<float>();
out.pulse.amplitude_max = p["amplitude_max"].get_value<float>();
out.pulse.frequency_min = p["frequency_min"].get_value<float>();
out.pulse.frequency_max = p["frequency_max"].get_value<float>();
const auto& r = node["animation"]["rotation_accel"];
out.rotation_accel.trigger_prob_per_second = r["trigger_prob_per_second"].get_value<float>();
out.rotation_accel.duration_min = r["duration_min"].get_value<float>();
out.rotation_accel.duration_max = r["duration_max"].get_value<float>();
out.rotation_accel.multiplier_min = r["multiplier_min"].get_value<float>();
out.rotation_accel.multiplier_max = r["multiplier_max"].get_value<float>();
return true;
}
auto parseWounded(const fkyaml::node& node, const std::string& name, EnemyConfig::WoundedCfg& out) -> bool {
if (!node.contains("wounded")) {
std::cerr << "[EnemyConfig] Error: falta 'wounded' a " << name << '\n';
return false;
}
const auto& w = node["wounded"];
out.duration = w["duration"].get_value<float>();
out.blink_hz = w["blink_hz"].get_value<float>();
return true;
}
auto parseSpawn(const fkyaml::node& node, const std::string& name, EnemyConfig::SpawnCfg& out) -> bool {
if (!node.contains("spawn")) {
std::cerr << "[EnemyConfig] Error: falta 'spawn' a " << name << '\n';
return false;
}
const auto& s = node["spawn"];
out.invulnerability_duration = s["invulnerability_duration"].get_value<float>();
out.invulnerability_brightness_start = s["invulnerability_brightness_start"].get_value<float>();
out.invulnerability_brightness_end = s["invulnerability_brightness_end"].get_value<float>();
out.invulnerability_scale_start = s["invulnerability_scale_start"].get_value<float>();
out.invulnerability_scale_end = s["invulnerability_scale_end"].get_value<float>();
out.safety_distance = s["safety_distance"].get_value<float>();
return true;
}
// Tots els camps de behavior són opcionals; només l'AI corresponent els consumeix.
void parseBehavior(const fkyaml::node& node, EnemyConfig::BehaviorCfg& out) {
if (!node.contains("behavior")) {
return;
}
const auto& b = node["behavior"];
const auto READ_OPT = [&b](const char* key, float& dst) {
if (b.contains(key)) {
dst = b[key].get_value<float>();
}
};
READ_OPT("zigzag_prob_per_second", out.zigzag_prob_per_second);
READ_OPT("angle_change_max", out.angle_change_max);
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);
}
auto parseColors(const fkyaml::node& node, const std::string& name, EnemyConfig::ColorsCfg& out) -> bool {
if (!node.contains("colors") ||
!parseColor(node["colors"]["normal"], out.normal) ||
!parseColor(node["colors"]["wounded"], out.wounded)) {
std::cerr << "[EnemyConfig] Error: 'colors.normal' / 'colors.wounded' no són [r,g,b] a "
<< name << '\n';
return false;
}
return true;
}
auto parseScore(const fkyaml::node& node, const std::string& name, int& out) -> bool {
if (!node.contains("score")) {
std::cerr << "[EnemyConfig] Error: falta 'score' a " << name << '\n';
return false;
}
out = node["score"].get_value<int>();
return true;
}
auto actionTypeFromString(const std::string& s) -> std::optional<EnemyActionType> {
if (s == "set_hurt") { return EnemyActionType::SET_HURT; }
if (s == "destroy") { return EnemyActionType::DESTROY; }
if (s == "add_score") { return EnemyActionType::ADD_SCORE; }
if (s == "create_debris") { return EnemyActionType::CREATE_DEBRIS; }
if (s == "create_fireworks") { return EnemyActionType::CREATE_FIREWORKS; }
if (s == "apply_impulse") { return EnemyActionType::APPLY_IMPULSE; }
return std::nullopt;
}
auto parseActionList(const fkyaml::node& list_node, const std::string& enemy_name, const char* event_name, std::vector<EnemyAction>& out) -> bool {
if (!list_node.is_sequence()) {
std::cerr << "[EnemyConfig] Error: '" << event_name << "' 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 " << event_name
<< " (" << enemy_name << ")\n";
return false;
}
const auto STR = item["action"].get_value<std::string>();
const auto PARSED = actionTypeFromString(STR);
if (!PARSED) {
std::cerr << "[EnemyConfig] Error: acció desconeguda '" << STR << "' a "
<< event_name << " (" << enemy_name << ")\n";
return false;
}
out.push_back({*PARSED});
}
return true;
}
// Defaults: replica el flux hardcoded actual (set_hurt → destroy → score+debris+fireworks).
void fillLegacyDefaults(EnemyEventConfig& events) {
events.on_hit = {{EnemyActionType::SET_HURT}};
events.on_hurt_end = {{EnemyActionType::DESTROY}};
events.on_destroy = {
{EnemyActionType::ADD_SCORE},
{EnemyActionType::CREATE_DEBRIS},
{EnemyActionType::CREATE_FIREWORKS},
};
}
auto parseEvents(const fkyaml::node& node, const std::string& name, EnemyEventConfig& out) -> bool {
if (!node.contains("events")) {
fillLegacyDefaults(out);
return true;
}
const auto& e = node["events"];
if (e.contains("on_hit") && !parseActionList(e["on_hit"], name, "on_hit", out.on_hit)) {
return false;
}
if (e.contains("on_hurt_end") &&
!parseActionList(e["on_hurt_end"], name, "on_hurt_end", out.on_hurt_end)) {
return false;
}
if (e.contains("on_destroy") &&
!parseActionList(e["on_destroy"], name, "on_destroy", out.on_destroy)) {
return false;
}
// Validació: destroy no pot aparèixer dins on_destroy (recursió infinita).
for (const auto& a : out.on_destroy) {
if (a.type == EnemyActionType::DESTROY) {
std::cerr << "[EnemyConfig] Error: 'destroy' no pot aparèixer dins 'on_destroy' a "
<< name << " (recursió infinita)\n";
return false;
}
}
return true;
}
} // namespace
auto EnemyConfig::fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
-> std::optional<EnemyConfig> {
try {
EnemyConfig cfg;
cfg.name = node.contains("name") ? node["name"].get_value<std::string>() : "enemy";
if (!parseAiType(node, expected_ai_type, cfg.name, cfg.ai_type)) { return std::nullopt; }
if (!parseShape(node, cfg.name, cfg.shape)) { return std::nullopt; }
if (!parsePhysics(node, cfg.name, cfg.physics)) { return std::nullopt; }
parseBehavior(node, cfg.behavior);
if (!parseAnimation(node, cfg.name, cfg.animation)) { return std::nullopt; }
if (!parseWounded(node, cfg.name, cfg.wounded)) { return std::nullopt; }
if (!parseSpawn(node, cfg.name, cfg.spawn)) { return std::nullopt; }
if (!parseColors(node, cfg.name, cfg.colors)) { return std::nullopt; }
if (!parseScore(node, cfg.name, cfg.score)) { return std::nullopt; }
if (!parseEvents(node, cfg.name, cfg.events)) { return std::nullopt; }
return cfg;
} catch (const std::exception& e) {
std::cerr << "[EnemyConfig] Excepció parsejant: " << e.what() << '\n';
return std::nullopt;
}
}
+108
View File
@@ -0,0 +1,108 @@
// enemy_config.hpp - Configuració d'un tipus d'enemic carregada des de YAML
// © 2026 JailDesigner
//
// Una instància per tipus (Pentagon/Square/Pinwheel), carregada un cop al
// startup per EnemyRegistry i compartida entre totes les instàncies d'aquell
// tipus. Estructura paral·lela a PlayerConfig.
#pragma once
#include <SDL3/SDL.h>
#include <optional>
#include <string>
#include "external/fkyaml_node.hpp"
#include "game/entities/enemy.hpp" // EnemyType
#include "game/entities/enemy_event.hpp"
struct EnemyConfig {
struct ShapeCfg {
std::string path;
float scale; // multiplicador visual + hitbox sobre la mida nativa del .shp
float collision_factor; // ajust opcional del hitbox respecte el cercle circumscrit (default 1.0)
};
struct PhysicsCfg {
float mass;
float speed;
float rotation_delta_min;
float rotation_delta_max;
float restitution; // rebot contra parets (1.0 = perfectament elàstic)
float linear_damping; // fricció lineal (s^-1)
float angular_damping;
};
// Camps específics de cada AI. Els no aplicables a un tipus queden a 0.0F
// i no s'usen — el dispatch viu a Enemy::behaviorXxx.
struct BehaviorCfg {
// Pentagon
float zigzag_prob_per_second{0.0F};
float angle_change_max{0.0F};
// Square
float tracking_strength{0.0F};
float tracking_interval{0.0F};
// Pinwheel
float rotation_proximity_multiplier{0.0F};
float proximity_distance{0.0F};
};
// Animacions decoratives. Compartides estructuralment entre tots els tipus
// però amb valors propis per personalitzar la "personalitat" visual de cada un.
struct AnimationCfg {
struct PulseCfg {
float trigger_prob_per_second; // probabilitat per segon d'iniciar un pulse
float duration_min;
float duration_max;
float amplitude_min; // amplitud d'escala (±)
float amplitude_max;
float frequency_min; // Hz
float frequency_max;
};
struct RotationAccelCfg {
float trigger_prob_per_second;
float duration_min; // segons de transició al nou speed
float duration_max;
float multiplier_min; // multiplicador sobre rotation_delta_base
float multiplier_max;
};
PulseCfg pulse;
RotationAccelCfg rotation_accel;
};
struct WoundedCfg {
float duration; // segons en estat ferit abans d'explotar
float blink_hz; // freqüència del parpelleig color normal ↔ wounded
};
struct SpawnCfg {
float invulnerability_duration; // segons d'invulnerabilitat post-spawn
float invulnerability_brightness_start; // brightness inicial (corba LERP)
float invulnerability_brightness_end; // brightness final
float invulnerability_scale_start; // escala inicial (corba LERP, 0 = invisible)
float invulnerability_scale_end; // escala final (1 = mida nativa)
float safety_distance; // px mínim respecte al player al spawn
};
struct ColorsCfg {
SDL_Color normal;
SDL_Color wounded;
};
std::string name;
EnemyType ai_type;
ShapeCfg shape;
PhysicsCfg physics;
BehaviorCfg behavior;
AnimationCfg animation;
WoundedCfg wounded;
SpawnCfg spawn;
ColorsCfg colors;
int score;
EnemyEventConfig events;
// 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).
static auto fromYaml(const fkyaml::node& node, EnemyType expected_ai_type)
-> std::optional<EnemyConfig>;
};
+48
View File
@@ -0,0 +1,48 @@
// enemy_event.hpp - Sistema declaratiu d'events i accions per a enemics
// © 2026 JailDesigner
//
// Cada enemic descriu al seu YAML què passa quan rep un event (on_hit,
// on_hurt_end, on_destroy) com a llista d'accions. El motor només dispatcha;
// el comportament viu a les dades.
#pragma once
#include <cstdint>
#include <vector>
enum class EnemyEventType : uint8_t {
ON_HIT, // Impactat per una bala
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 {
SET_HURT, // Entra estat wounded (o destrueix si ja era wounded)
DESTROY, // Dispara on_destroy + desactiva físicament
ADD_SCORE, // Suma config.score al shooter + floating score
CREATE_DEBRIS, // Explosió de debris amb herència de velocitat
CREATE_FIREWORKS, // Burst radial de firework
APPLY_IMPULSE, // Aplica l'impuls de la bala impactant
};
struct EnemyAction {
EnemyActionType type;
};
struct EnemyEventConfig {
std::vector<EnemyAction> on_hit;
std::vector<EnemyAction> on_hurt_end;
std::vector<EnemyAction> on_destroy;
[[nodiscard]] auto getActions(EnemyEventType event) const -> const std::vector<EnemyAction>& {
switch (event) {
case EnemyEventType::ON_HIT:
return on_hit;
case EnemyEventType::ON_HURT_END:
return on_hurt_end;
case EnemyEventType::ON_DESTROY:
return on_destroy;
}
return on_hit; // unreachable
}
};
+66
View File
@@ -0,0 +1,66 @@
// enemy_registry.cpp - Implementació del registre estàtic d'enemics
// © 2026 JailDesigner
#include "game/entities/enemy_registry.hpp"
#include <cstdlib>
#include <iostream>
#include <string>
#include "core/entities/entity_loader.hpp"
EnemyConfig EnemyRegistry::pentagon_config;
EnemyConfig EnemyRegistry::square_config;
EnemyConfig EnemyRegistry::pinwheel_config;
EnemyConfig EnemyRegistry::star_config;
bool EnemyRegistry::loaded = false;
namespace {
auto loadOne(const std::string& name, EnemyType expected_type, EnemyConfig& out) -> bool {
auto yaml = Entities::EntityLoader::load(name);
if (!yaml) {
std::cerr << "[EnemyRegistry] Error: no s'ha pogut carregar " << name << ".yaml\n";
return false;
}
auto cfg = EnemyConfig::fromYaml(*yaml, expected_type);
if (!cfg) {
std::cerr << "[EnemyRegistry] Error: format invàlid a " << name << ".yaml\n";
return false;
}
out = *cfg;
return true;
}
} // namespace
auto EnemyRegistry::loadAll() -> bool {
const bool OK = loadOne("pentagon", EnemyType::PENTAGON, pentagon_config) &&
loadOne("square", EnemyType::SQUARE, square_config) &&
loadOne("pinwheel", EnemyType::PINWHEEL, pinwheel_config) &&
loadOne("star", EnemyType::STAR, star_config);
loaded = OK;
if (OK) {
std::cout << "[EnemyRegistry] 4 configuracions d'enemic carregades.\n";
}
return OK;
}
auto EnemyRegistry::get(EnemyType type) -> const EnemyConfig& {
if (!loaded) {
std::cerr << "[EnemyRegistry] FATAL: get() abans de loadAll()\n";
std::exit(EXIT_FAILURE);
}
switch (type) {
case EnemyType::PENTAGON:
return pentagon_config;
case EnemyType::SQUARE:
return square_config;
case EnemyType::PINWHEEL:
return pinwheel_config;
case EnemyType::STAR:
return star_config;
}
std::cerr << "[EnemyRegistry] FATAL: tipus desconegut\n";
std::exit(EXIT_FAILURE);
}
+31
View File
@@ -0,0 +1,31 @@
// enemy_registry.hpp - Registre estàtic de configuracions d'enemics per tipus
// © 2026 JailDesigner
//
// Carrega els 3 fitxers YAML (pentagon, square, pinwheel) un cop al startup
// i exposa el lookup per EnemyType. Pensat per a ser invocat des de
// GameScene; si la càrrega falla, el caller decideix avortar.
#pragma once
#include "game/entities/enemy.hpp"
#include "game/entities/enemy_config.hpp"
class EnemyRegistry {
public:
EnemyRegistry() = delete; // tot estàtic
// Carrega els 3 descriptors. Retorna true si tots tres s'han carregat
// i parsejat correctament. Cridar abans del primer get().
static auto loadAll() -> bool;
// Lookup. Cal haver cridat loadAll() abans. Si el tipus no s'ha carregat
// (loadAll fallida o no cridada), avorta amb log fatal.
static auto get(EnemyType type) -> const EnemyConfig&;
private:
static EnemyConfig pentagon_config;
static EnemyConfig square_config;
static EnemyConfig pinwheel_config;
static EnemyConfig star_config;
static bool loaded;
};
+111
View File
@@ -0,0 +1,111 @@
// player_config.cpp - Implementació del parser de PlayerConfig
// © 2026 JailDesigner
#include "game/entities/player_config.hpp"
#include <cstdint>
#include <exception>
#include <iostream>
#include <string>
namespace {
// Helper: extreu un SDL_Color d'una seqüència de 3 enters [r, g, b] del YAML.
// Retorna true si el format és vàlid.
auto parseColor(const fkyaml::node& node, SDL_Color& out) -> bool {
if (!node.is_sequence() || node.size() != 3) {
return false;
}
const auto R = node[0].get_value<uint32_t>();
const auto G = node[1].get_value<uint32_t>();
const auto B = node[2].get_value<uint32_t>();
out = SDL_Color{
.r = static_cast<uint8_t>(R),
.g = static_cast<uint8_t>(G),
.b = static_cast<uint8_t>(B),
.a = 255};
return true;
}
} // namespace
auto PlayerConfig::fromYaml(const fkyaml::node& node) -> std::optional<PlayerConfig> {
try {
PlayerConfig cfg;
cfg.name = node.contains("name") ? node["name"].get_value<std::string>() : "player";
// shape
if (!node.contains("shape") || !node["shape"].contains("path")) {
std::cerr << "[PlayerConfig] Error: falta 'shape.path'" << '\n';
return std::nullopt;
}
const auto& shape = node["shape"];
cfg.shape.path = shape["path"].get_value<std::string>();
cfg.shape.scale = shape.contains("scale") ? shape["scale"].get_value<float>() : 1.0F;
cfg.shape.collision_factor = shape.contains("collision_factor")
? shape["collision_factor"].get_value<float>()
: 1.0F;
// physics
if (!node.contains("physics")) {
std::cerr << "[PlayerConfig] Error: falta 'physics'" << '\n';
return std::nullopt;
}
const auto& physics = node["physics"];
cfg.physics.mass = physics["mass"].get_value<float>();
cfg.physics.restitution = physics["restitution"].get_value<float>();
cfg.physics.linear_damping = physics["linear_damping"].get_value<float>();
cfg.physics.angular_damping = physics["angular_damping"].get_value<float>();
cfg.physics.rotation_speed = physics["rotation_speed"].get_value<float>();
cfg.physics.acceleration = physics["acceleration"].get_value<float>();
cfg.physics.max_velocity = physics["max_velocity"].get_value<float>();
cfg.physics.death_impact_factor = physics["death_impact_factor"].get_value<float>();
// invulnerability
if (!node.contains("invulnerability")) {
std::cerr << "[PlayerConfig] Error: falta 'invulnerability'" << '\n';
return std::nullopt;
}
const auto& invul = node["invulnerability"];
cfg.invulnerability.duration = invul["duration"].get_value<float>();
cfg.invulnerability.blink_visible = invul["blink_visible"].get_value<float>();
cfg.invulnerability.blink_invisible = invul["blink_invisible"].get_value<float>();
// hurt
if (!node.contains("hurt")) {
std::cerr << "[PlayerConfig] Error: falta 'hurt'" << '\n';
return std::nullopt;
}
cfg.hurt.duration = node["hurt"]["duration"].get_value<float>();
cfg.hurt.blink_hz = node["hurt"]["blink_hz"].get_value<float>();
// visual_thrust
if (!node.contains("visual_thrust")) {
std::cerr << "[PlayerConfig] Error: falta 'visual_thrust'" << '\n';
return std::nullopt;
}
cfg.visual_thrust.push_divisor = node["visual_thrust"]["push_divisor"].get_value<float>();
cfg.visual_thrust.scale_divisor = node["visual_thrust"]["scale_divisor"].get_value<float>();
// colors
if (!node.contains("colors") ||
!parseColor(node["colors"]["normal"], cfg.colors.normal) ||
!parseColor(node["colors"]["hurt"], cfg.colors.hurt)) {
std::cerr << "[PlayerConfig] Error: 'colors.normal' / 'colors.hurt' no són seqüències [r,g,b]" << '\n';
return std::nullopt;
}
// weapon
if (!node.contains("weapon")) {
std::cerr << "[PlayerConfig] Error: falta 'weapon'" << '\n';
return std::nullopt;
}
cfg.weapon.bullet_speed = node["weapon"]["bullet_speed"].get_value<float>();
return cfg;
} catch (const std::exception& e) {
std::cerr << "[PlayerConfig] Excepció parsejant: " << e.what() << '\n';
return std::nullopt;
}
}
+73
View File
@@ -0,0 +1,73 @@
// player_config.hpp - Configuració de la nau del player carregada des de YAML
// © 2026 JailDesigner
//
// POD struct amb sub-structs per organitzar els paràmetres del jugador
// (física, invulnerabilitat, hurt, empenta visual, colors, weapon). Es
// construeix a partir d'un node fkyaml carregat per EntityLoader.
#pragma once
#include <SDL3/SDL.h>
#include <optional>
#include <string>
#include "external/fkyaml_node.hpp"
struct PlayerConfig {
struct ShapeCfg {
std::string path;
float scale; // multiplicador visual + hitbox sobre la mida nativa del .shp
float collision_factor; // ajust opcional del hitbox respecte el cercle circumscrit (default 1.0)
};
struct PhysicsCfg {
float mass;
float restitution;
float linear_damping;
float angular_damping;
float rotation_speed; // rad/s
float acceleration; // px/s^2 multiplicat per la massa
float max_velocity; // px/s (clamp post-integració)
float death_impact_factor; // [0..1] moment transferit a l'enemic al morir
};
struct InvulnerabilityCfg {
float duration;
float blink_visible;
float blink_invisible;
};
struct HurtCfg {
float duration;
float blink_hz;
};
struct VisualThrustCfg {
float push_divisor;
float scale_divisor;
};
struct ColorsCfg {
SDL_Color normal;
SDL_Color hurt;
};
struct WeaponCfg {
float bullet_speed;
};
std::string name;
ShapeCfg shape;
PhysicsCfg physics;
InvulnerabilityCfg invulnerability;
HurtCfg hurt;
VisualThrustCfg visual_thrust;
ColorsCfg colors;
WeaponCfg weapon;
// Construeix un PlayerConfig a partir del node YAML. Retorna std::nullopt
// si falten camps requerits o el format no és vàlid (el caller decideix
// si abortar).
static auto fromYaml(const fkyaml::node& node) -> std::optional<PlayerConfig>;
};
+35 -48
View File
@@ -9,6 +9,7 @@
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <iostream> #include <iostream>
#include <utility>
#include "core/audio/audio.hpp" #include "core/audio/audio.hpp"
#include "core/defaults.hpp" #include "core/defaults.hpp"
@@ -20,27 +21,30 @@
#include "core/types.hpp" #include "core/types.hpp"
#include "game/constants.hpp" #include "game/constants.hpp"
Ship::Ship(Rendering::Renderer* renderer, const char* shape_file) Ship::Ship(Rendering::Renderer* renderer, PlayerConfig config, const char* shape_override)
: Entity(renderer) { : Entity(renderer),
// Brightness específico para naves config_(std::move(config)) {
brightness_ = Defaults::Brightness::NAU; brightness_ = Defaults::Brightness::NAU;
// Configuración del cuerpo físico // El shape pot venir del YAML o ser overridden (ex: P2 amb "ship2.shp").
body_.setMass(Defaults::Ship::MASS); const std::string SHAPE_PATH = (shape_override != nullptr) ? shape_override : config_.shape.path;
body_.radius = Defaults::Entities::SHIP_RADIUS; shape_ = Graphics::ShapeLoader::load(SHAPE_PATH);
body_.restitution = Defaults::Ship::RESTITUTION;
body_.linear_damping = Defaults::Ship::LINEAR_DAMPING;
body_.angular_damping = Defaults::Ship::ANGULAR_DAMPING;
// Cargar shape compartida desde archivo
shape_ = Graphics::ShapeLoader::load(shape_file);
if (!shape_ || !shape_->isValid()) { if (!shape_ || !shape_->isValid()) {
std::cerr << "[Ship] Error: no se ha podido cargar " << shape_file << '\n'; std::cerr << "[Ship] Error: no se ha podido cargar " << SHAPE_PATH << '\n';
} }
// Radi de col·lisió derivat del cercle circumscrit de la shape * scale * collision_factor.
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_.radius = collision_radius_;
body_.restitution = config_.physics.restitution;
body_.linear_damping = config_.physics.linear_damping;
body_.angular_damping = config_.physics.angular_damping;
} }
void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) { void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) {
// Posición inicial
if (spawn_point != nullptr) { if (spawn_point != nullptr) {
center_ = *spawn_point; center_ = *spawn_point;
} else { } else {
@@ -50,34 +54,27 @@ void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) {
center_ = {.x = center_x, .y = center_y}; center_ = {.x = center_x, .y = center_y};
} }
// Reset orientación
angle_ = 0.0F; angle_ = 0.0F;
// Sincronizar cuerpo físico con la posición/orientación inicial
body_.position = center_; body_.position = center_;
body_.angle = angle_; body_.angle = angle_;
body_.velocity = Vec2{}; body_.velocity = Vec2{};
body_.angular_velocity = 0.0F; body_.angular_velocity = 0.0F;
body_.clearAccumulators(); body_.clearAccumulators();
// Activar invulnerabilidad solo si es respawn invulnerable_timer_ = activar_invulnerabilitat ? config_.invulnerability.duration : 0.0F;
invulnerable_timer_ = activar_invulnerabilitat ? Defaults::Ship::INVULNERABILITY_DURATION : 0.0F;
is_hit_ = false; is_hit_ = false;
hurt_timer_ = 0.0F; hurt_timer_ = 0.0F;
touching_enemy_prev_frame_ = false; touching_enemy_prev_frame_ = false;
} }
void Ship::processInput(float delta_time, uint8_t player_id) { void Ship::processInput(float delta_time, uint8_t player_id) {
// Solo procesa input si la nave está viva
if (is_hit_) { if (is_hit_) {
return; return;
} }
auto* input = Input::get(); auto* input = Input::get();
// Rotación: control directo del ángulo (no física, no inercial).
// Se actualiza también body_.angle para que el dibujado tras
// postUpdate refleje el cambio inmediatamente.
const bool ROTATE_RIGHT = (player_id == 0) const bool ROTATE_RIGHT = (player_id == 0)
? input->checkActionPlayer1(InputAction::RIGHT, Input::ALLOW_REPEAT) ? input->checkActionPlayer1(InputAction::RIGHT, Input::ALLOW_REPEAT)
: input->checkActionPlayer2(InputAction::RIGHT, Input::ALLOW_REPEAT); : input->checkActionPlayer2(InputAction::RIGHT, Input::ALLOW_REPEAT);
@@ -89,10 +86,10 @@ void Ship::processInput(float delta_time, uint8_t player_id) {
: input->checkActionPlayer2(InputAction::THRUST, Input::ALLOW_REPEAT); : input->checkActionPlayer2(InputAction::THRUST, Input::ALLOW_REPEAT);
if (ROTATE_RIGHT) { if (ROTATE_RIGHT) {
body_.angle += Defaults::Physics::ROTATION_SPEED * delta_time; body_.angle += config_.physics.rotation_speed * delta_time;
} }
if (ROTATE_LEFT) { if (ROTATE_LEFT) {
body_.angle -= Defaults::Physics::ROTATION_SPEED * delta_time; body_.angle -= config_.physics.rotation_speed * delta_time;
} }
// Thrust: fuerza vectorial en la dirección de la nariz. // Thrust: fuerza vectorial en la dirección de la nariz.
@@ -100,44 +97,36 @@ void Ship::processInput(float delta_time, uint8_t player_id) {
if (THRUST) { if (THRUST) {
const float DIR_X = std::cos(body_.angle - (Constants::PI / 2.0F)); const float DIR_X = std::cos(body_.angle - (Constants::PI / 2.0F));
const float DIR_Y = std::sin(body_.angle - (Constants::PI / 2.0F)); const float DIR_Y = std::sin(body_.angle - (Constants::PI / 2.0F));
// Fuerza = masa * aceleración: 10 kg * 400 px/s² = 4000 (unidades arcade) const float MAGNITUDE = body_.mass * config_.physics.acceleration;
const float MAGNITUDE = body_.mass * Defaults::Physics::ACCELERATION;
body_.applyForce(Vec2{.x = DIR_X * MAGNITUDE, .y = DIR_Y * MAGNITUDE}); body_.applyForce(Vec2{.x = DIR_X * MAGNITUDE, .y = DIR_Y * MAGNITUDE});
} }
} }
void Ship::update(float delta_time) { void Ship::update(float delta_time) {
// Solo update si la nave está viva
if (is_hit_) { if (is_hit_) {
return; return;
} }
// Decrementar timer de invulnerabilidad
if (invulnerable_timer_ > 0.0F) { if (invulnerable_timer_ > 0.0F) {
invulnerable_timer_ -= delta_time; invulnerable_timer_ -= delta_time;
invulnerable_timer_ = std::max(invulnerable_timer_, 0.0F); invulnerable_timer_ = std::max(invulnerable_timer_, 0.0F);
} }
// Decrementar timer d'estat HURT (a 0 → torna a normal sense efecte extern)
if (hurt_timer_ > 0.0F) { if (hurt_timer_ > 0.0F) {
hurt_timer_ -= delta_time; hurt_timer_ -= delta_time;
hurt_timer_ = std::max(hurt_timer_, 0.0F); hurt_timer_ = std::max(hurt_timer_, 0.0F);
} }
// El movimiento real lo hace PhysicsWorld::update().
// Aquí solo lógica de estado.
// Cap de velocidad: el thrust acumula fuerza sin límite; limitamos // Cap de velocidad: el thrust acumula fuerza sin límite; limitamos
// la magnitud de body_.velocity tras aplicar fuerzas para preservar // la magnitud de body_.velocity tras aplicar fuerzas para preservar
// el feel arcade del MAX_VELOCITY original. // el feel arcade del MAX_VELOCITY original.
const float CURRENT_SPEED = body_.velocity.length(); const float CURRENT_SPEED = body_.velocity.length();
if (CURRENT_SPEED > Defaults::Physics::MAX_VELOCITY) { if (CURRENT_SPEED > config_.physics.max_velocity) {
body_.velocity = body_.velocity * (Defaults::Physics::MAX_VELOCITY / CURRENT_SPEED); body_.velocity = body_.velocity * (config_.physics.max_velocity / CURRENT_SPEED);
} }
} }
void Ship::postUpdate(float /*delta_time*/) { void Ship::postUpdate(float /*delta_time*/) {
// Sincronizar mirror desde body_ tras la integración del world.
center_ = body_.position; center_ = body_.position;
angle_ = body_.angle; angle_ = body_.angle;
} }
@@ -147,11 +136,10 @@ void Ship::draw() const {
return; return;
} }
// Parpadeo si invulnerable
if (isInvulnerable()) { if (isInvulnerable()) {
const float BLINK_CYCLE = Defaults::Ship::BLINK_VISIBLE_TIME + Defaults::Ship::BLINK_INVISIBLE_TIME; const float BLINK_CYCLE = config_.invulnerability.blink_visible + config_.invulnerability.blink_invisible;
const float TIME_IN_CYCLE = std::fmod(invulnerable_timer_, BLINK_CYCLE); const float TIME_IN_CYCLE = std::fmod(invulnerable_timer_, BLINK_CYCLE);
if (TIME_IN_CYCLE < Defaults::Ship::BLINK_INVISIBLE_TIME) { if (TIME_IN_CYCLE < config_.invulnerability.blink_invisible) {
return; return;
} }
} }
@@ -160,20 +148,19 @@ void Ship::draw() const {
return; return;
} }
// Efecto visual de empuje: escala proporcional a la velocidad. // Efecte visual d'empenta (modulador sobre l'escala base del YAML).
// 0..200 px/s → escala 1.0..1.5 (manteniendo la sensación del Pascal original).
const float SPEED = getSpeed(); const float SPEED = getSpeed();
const float VISUAL_PUSH = SPEED / Defaults::Ship::VISUAL_PUSH_DIVISOR; const float VISUAL_PUSH = SPEED / config_.visual_thrust.push_divisor;
const float SCALE = 1.0F + (VISUAL_PUSH / Defaults::Ship::VISUAL_SCALE_DIVISOR); const float THRUST_MODULATOR = 1.0F + (VISUAL_PUSH / config_.visual_thrust.scale_divisor);
const float SCALE = config_.shape.scale * THRUST_MODULATOR;
// Parpelleig daurat mentre està ferida: alterna color normal ↔ color hurt // Parpelleig daurat mentre està ferida: alterna color normal ↔ color hurt.
// a Hurt::BLINK_HZ (mateixa estètica que el wounded dels enemics). SDL_Color color = config_.colors.normal;
SDL_Color color = color_normal_;
if (hurt_timer_ > 0.0F) { if (hurt_timer_ > 0.0F) {
const float CYCLE = 1.0F / Defaults::Ship::Hurt::BLINK_HZ; const float CYCLE = 1.0F / config_.hurt.blink_hz;
const float T = std::fmod(hurt_timer_, CYCLE); const float T = std::fmod(hurt_timer_, CYCLE);
if (T < (CYCLE / 2.0F)) { if (T < (CYCLE / 2.0F)) {
color = color_hurt_; color = config_.colors.hurt;
} }
} }
@@ -181,6 +168,6 @@ void Ship::draw() const {
} }
void Ship::hurt() { void Ship::hurt() {
hurt_timer_ = Defaults::Ship::Hurt::DURATION; hurt_timer_ = config_.hurt.duration;
Audio::get()->playSound(Defaults::Sound::HURT, Audio::Group::GAME); Audio::get()->playSound(Defaults::Sound::HURT, Audio::Group::GAME);
} }
+18 -16
View File
@@ -6,15 +6,18 @@
#include <cstdint> #include <cstdint>
#include "core/defaults.hpp"
#include "core/entities/entity.hpp" #include "core/entities/entity.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/entities/player_config.hpp"
class Ship : public Entities::Entity { class Ship : public Entities::Entity {
public: public:
Ship() Ship()
: Entity(nullptr) {} : Entity(nullptr) {}
explicit Ship(Rendering::Renderer* renderer, const char* shape_file = "ship.shp"); // shape_override: si no és nullptr, substitueix config.shape.path
// (utilitzat per donar al P2 un model visual diferent compartint la
// mateixa configuració del player).
explicit Ship(Rendering::Renderer* renderer, PlayerConfig config, const char* shape_override = nullptr);
void init() override { init(nullptr, false); } void init() override { init(nullptr, false); }
void init(const Vec2* spawn_point, bool activar_invulnerabilitat = false); void init(const Vec2* spawn_point, bool activar_invulnerabilitat = false);
@@ -26,20 +29,18 @@ class Ship : public Entities::Entity {
// Override: Interfaz de Entity // Override: Interfaz de Entity
[[nodiscard]] auto isActive() const -> bool override { return !is_hit_; } [[nodiscard]] auto isActive() const -> bool override { return !is_hit_; }
// Override: Interfaz de colisión // Override: Interfaz de colisión. Derivat al ctor del bounding_radius del
[[nodiscard]] auto getCollisionRadius() const -> float override { // shape carregat × scale × collision_factor.
return Defaults::Entities::SHIP_RADIUS; [[nodiscard]] auto getCollisionRadius() const -> float override { return collision_radius_; }
}
[[nodiscard]] auto isCollidable() const -> bool override { [[nodiscard]] auto isCollidable() const -> bool override {
return !is_hit_ && invulnerable_timer_ <= 0.0F; return !is_hit_ && invulnerable_timer_ <= 0.0F;
} }
// Getters // Getters
[[nodiscard]] auto isInvulnerable() const -> bool { return invulnerable_timer_ > 0.0F; } [[nodiscard]] auto isInvulnerable() const -> bool { return invulnerable_timer_ > 0.0F; }
// Velocidad como vector cartesiano (ahora viene directa del body_).
[[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; } [[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; }
// Velocidad escalar (utilidad para draw y debugging).
[[nodiscard]] auto getSpeed() const -> float { return body_.velocity.length(); } [[nodiscard]] auto getSpeed() const -> float { return body_.velocity.length(); }
[[nodiscard]] auto getConfig() const -> const PlayerConfig& { return config_; }
// Setters // Setters
void setCenter(const Vec2& nou_centre) { void setCenter(const Vec2& nou_centre) {
@@ -65,17 +66,18 @@ class Ship : public Entities::Entity {
void setTouchingEnemyPrevFrame(bool touching) { touching_enemy_prev_frame_ = touching; } void setTouchingEnemyPrevFrame(bool touching) { touching_enemy_prev_frame_ = touching; }
private: private:
// Miembros específicos de Ship (heredados: renderer_, shape_, center_, angle_, brightness_, body_). // Configuració carregada des de YAML. Default-init zero permet el ctor
// Inicializados en la declaración: el ctor por defecto deja la nave "viva y sin invulnerabilidad", // per defecte (necessari per a `std::array<Ship, N>`); s'omple via
// que es el estado coherente al que llevan tanto init() como el ctor con renderer. // copy/move-assignment quan GameScene crea la nau real.
PlayerConfig config_{};
// Radi de col·lisió derivat: shape.bounding_radius × shape.scale × shape.collision_factor.
float collision_radius_{0.0F};
bool is_hit_{false}; bool is_hit_{false};
float invulnerable_timer_{0.0F}; // 0.0f = vulnerable, >0.0f = invulnerable float invulnerable_timer_{0.0F}; // 0.0f = vulnerable, >0.0f = invulnerable
// Colors de la nau (propietats, prep per migració a YAML). // >0 → estat HURT (parpelleig color normal ↔ color hurt).
SDL_Color color_normal_{Defaults::Palette::SHIP};
SDL_Color color_hurt_{Defaults::Palette::WOUNDED};
// >0 → estat HURT (parpelleig color_normal_ ↔ color_hurt_).
float hurt_timer_{0.0F}; float hurt_timer_{0.0F};
// Edge-trigger: true si el frame anterior la nau ja estava en contacte amb un enemic. // Edge-trigger: true si el frame anterior la nau ja estava en contacte amb un enemic.
+36 -5
View File
@@ -10,10 +10,14 @@
#include <iostream> #include <iostream>
#include "core/audio/audio.hpp" #include "core/audio/audio.hpp"
#include "core/entities/entity_loader.hpp"
#include "core/input/input.hpp" #include "core/input/input.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/system/scene_context.hpp" #include "core/system/scene_context.hpp"
#include "core/system/service_menu.hpp" #include "core/system/service_menu.hpp"
#include "game/entities/bullet_registry.hpp"
#include "game/entities/enemy_registry.hpp"
#include "game/entities/player_config.hpp"
#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"
@@ -49,9 +53,36 @@ GameScene::GameScene(SDLManager& sdl, SceneContext& context)
auto option = context_.consumeOption(); auto option = context_.consumeOption();
(void)option; // Suprimir warning de variable no usada (void)option; // Suprimir warning de variable no usada
// Inicialitzar naves con renderer (P1=ship.shp, P2=ship2.shp) // Carregar la configuració del player des de YAML. Sense fallback: si
ships_[0] = Ship(sdl.getRenderer(), "ship.shp"); // Jugador 1: nave estàndar // falla, abortem (la nau no és construïble sense paràmetres).
ships_[1] = Ship(sdl.getRenderer(), "ship2.shp"); // Jugador 2: interceptor con ales auto player_yaml = Entities::EntityLoader::load("player");
if (!player_yaml) {
std::cerr << "[GameScene] FATAL: no s'ha pogut carregar data/entities/player/player.yaml\n";
std::exit(EXIT_FAILURE);
}
auto player_config = PlayerConfig::fromYaml(*player_yaml);
if (!player_config) {
std::cerr << "[GameScene] FATAL: player.yaml mal format\n";
std::exit(EXIT_FAILURE);
}
// Carregar les configuracions dels 3 enemics. Sense fallback: si falla,
// abortem (els enemics no es poden construir sense els seus paràmetres).
if (!EnemyRegistry::loadAll()) {
std::cerr << "[GameScene] FATAL: no s'han pogut carregar els enemics YAML\n";
std::exit(EXIT_FAILURE);
}
// Carregar la configuració de la bala. Cal abans de construir el pool de
// bullets, ja que cada Bullet llegeix el registry al seu ctor.
if (!BulletRegistry::load()) {
std::cerr << "[GameScene] FATAL: no s'ha pogut carregar bullet.yaml\n";
std::exit(EXIT_FAILURE);
}
// 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_[1] = Ship(sdl.getRenderer(), *player_config, "ship2.shp"); // Jugador 2: interceptor amb ales
// Inicialitzar balas con renderer // Inicialitzar balas con renderer
std::ranges::fill(bullets_, Bullet(sdl.getRenderer())); std::ranges::fill(bullets_, Bullet(sdl.getRenderer()));
@@ -758,7 +789,7 @@ void GameScene::tocado(uint8_t player_id) {
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::EXPLOSION2,
Defaults::Palette::SHIP, 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);
@@ -944,7 +975,7 @@ void GameScene::fireBullet(uint8_t player_id) {
const int START_IDX = player_id * SLOTS_PER_PLAYER; const int START_IDX = player_id * SLOTS_PER_PLAYER;
for (int i = START_IDX; i < START_IDX + SLOTS_PER_PLAYER; i++) { for (int i = START_IDX; i < START_IDX + SLOTS_PER_PLAYER; i++) {
if (!bullets_[i].isActive()) { if (!bullets_[i].isActive()) {
bullets_[i].fire(fire_position, ship_angle, player_id); bullets_[i].fire(fire_position, ship_angle, player_id, ships_[player_id].getConfig().weapon.bullet_speed);
break; break;
} }
} }
+14
View File
@@ -13,11 +13,13 @@
#include "core/audio/audio.hpp" #include "core/audio/audio.hpp"
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/graphics/shape_loader.hpp" #include "core/graphics/shape_loader.hpp"
#include "core/input/define_inputs.hpp"
#include "core/input/input.hpp" #include "core/input/input.hpp"
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/math/easing.hpp" #include "core/math/easing.hpp"
#include "core/rendering/shape_renderer.hpp" #include "core/rendering/shape_renderer.hpp"
#include "core/system/scene_context.hpp" #include "core/system/scene_context.hpp"
#include "core/system/service_menu.hpp"
#include "project.h" #include "project.h"
using SceneManager::SceneContext; using SceneManager::SceneContext;
@@ -324,8 +326,20 @@ void TitleScene::update(float delta_time) {
break; break;
} }
// Les animacions segueixen pero els inputs es bloquegen mentre el menu
// de servei o l'overlay de redefinicio estiguin actius: en cas contrari,
// SDL_GetKeyboardState i SDL_GetGamepadButton segueixen veient les tecles
// pulsades i podrien disparar handleSkipInput/handleStartInput sense
// intencio. Mateixa logica: per a GameScene tota la pausa es global,
// pero a TitleScene nomes guardem els polls d'input.
const auto* menu = System::ServiceMenu::get();
const auto* di = System::DefineInputs::get();
const bool INPUT_BLOCKED = (menu != nullptr && menu->isOpen()) ||
(di != nullptr && di->isActive());
if (!INPUT_BLOCKED) {
handleSkipInput(); handleSkipInput();
handleStartInput(); handleStartInput();
}
} }
void TitleScene::updateStarfieldFadeInState(float delta_time) { void TitleScene::updateStarfieldFadeInState(float delta_time) {
@@ -136,8 +136,11 @@ namespace StageSystem {
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado) { if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado) {
return EnemyType::SQUARE; return EnemyType::SQUARE;
} }
if (rand_val < config_->distribucio.pentagon + config_->distribucio.cuadrado + config_->distribucio.molinillo) {
return EnemyType::PINWHEEL; return EnemyType::PINWHEEL;
} }
return EnemyType::STAR;
}
void SpawnController::spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos) { void SpawnController::spawnEnemy(Enemy& enemy, EnemyType type, const Vec2* ship_pos) {
// Initialize enemy (with safe spawn if ship_pos provided) // Initialize enemy (with safe spawn if ship_pos provided)
+2 -1
View File
@@ -28,6 +28,7 @@ namespace StageSystem {
uint8_t pentagon; // 0-100 uint8_t pentagon; // 0-100
uint8_t cuadrado; // 0-100 uint8_t cuadrado; // 0-100
uint8_t molinillo; // 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 // Suma ha de ser 100, validat en StageLoader
}; };
@@ -59,7 +60,7 @@ namespace StageSystem {
// el tipo; basta con confirmar que no es 0 (sentinela "sin asignar"). // el tipo; basta con confirmar que no es 0 (sentinela "sin asignar").
return stage_id >= 1 && return stage_id >= 1 &&
total_enemies > 0 && total_enemies <= 200 && total_enemies > 0 && total_enemies <= 200 &&
distribucio.pentagon + distribucio.cuadrado + distribucio.molinillo == 100; distribucio.pentagon + distribucio.cuadrado + distribucio.molinillo + distribucio.star == 100;
} }
}; };
+19 -17
View File
@@ -19,7 +19,7 @@
namespace StageSystem { namespace StageSystem {
auto StageLoader::load(const std::string& path) -> std::unique_ptr<StageSystemConfig> { auto StageLoader::load(const std::string& path) -> std::unique_ptr<StageSystemConfig> {
try { try {
// Normalize path: "data/stages/stages.yaml" → "stages/stages.yaml" // Normalize path: "data/stages/stages.yaml" → "stages/stages.yaml"
std::string normalized = path; std::string normalized = path;
@@ -83,9 +83,9 @@ auto StageLoader::load(const std::string& path) -> std::unique_ptr<StageSystemCo
std::cerr << "[StageLoader] Excepció: " << e.what() << '\n'; std::cerr << "[StageLoader] Excepció: " << e.what() << '\n';
return nullptr; return nullptr;
} }
} }
auto StageLoader::parseMetadata(const fkyaml::node& yaml, MetadataStages& meta) -> bool { auto StageLoader::parseMetadata(const fkyaml::node& yaml, MetadataStages& meta) -> bool {
try { try {
if (!yaml.contains("version") || !yaml.contains("total_stages")) { if (!yaml.contains("version") || !yaml.contains("total_stages")) {
std::cerr << "[StageLoader] Error: metadata incompleta" << '\n'; std::cerr << "[StageLoader] Error: metadata incompleta" << '\n';
@@ -103,9 +103,9 @@ auto StageLoader::parseMetadata(const fkyaml::node& yaml, MetadataStages& meta)
std::cerr << "[StageLoader] Error parsing metadata: " << e.what() << '\n'; std::cerr << "[StageLoader] Error parsing metadata: " << e.what() << '\n';
return false; return false;
} }
} }
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("total_enemies") ||
!yaml.contains("spawn_config") || !yaml.contains("enemy_distribution") || !yaml.contains("spawn_config") || !yaml.contains("enemy_distribution") ||
@@ -138,9 +138,9 @@ auto StageLoader::parseStage(const fkyaml::node& yaml, StageConfig& stage) -> bo
std::cerr << "[StageLoader] Error parsing stage: " << e.what() << '\n'; std::cerr << "[StageLoader] Error parsing stage: " << e.what() << '\n';
return false; return false;
} }
} }
auto StageLoader::parseSpawnConfig(const fkyaml::node& yaml, ConfigSpawn& config) -> bool { auto StageLoader::parseSpawnConfig(const fkyaml::node& yaml, ConfigSpawn& config) -> bool {
try { try {
if (!yaml.contains("mode") || !yaml.contains("initial_delay") || if (!yaml.contains("mode") || !yaml.contains("initial_delay") ||
!yaml.contains("spawn_interval")) { !yaml.contains("spawn_interval")) {
@@ -158,9 +158,9 @@ auto StageLoader::parseSpawnConfig(const fkyaml::node& yaml, ConfigSpawn& config
std::cerr << "[StageLoader] Error parsing spawn_config: " << e.what() << '\n'; std::cerr << "[StageLoader] Error parsing spawn_config: " << e.what() << '\n';
return false; return false;
} }
} }
auto StageLoader::parseDistribution(const fkyaml::node& yaml, DistribucioEnemics& dist) -> bool { auto StageLoader::parseDistribution(const fkyaml::node& yaml, DistribucioEnemics& dist) -> bool {
try { try {
if (!yaml.contains("pentagon") || !yaml.contains("cuadrado") || if (!yaml.contains("pentagon") || !yaml.contains("cuadrado") ||
!yaml.contains("molinillo")) { !yaml.contains("molinillo")) {
@@ -171,9 +171,11 @@ auto StageLoader::parseDistribution(const fkyaml::node& yaml, DistribucioEnemics
dist.pentagon = yaml["pentagon"].get_value<uint8_t>(); dist.pentagon = yaml["pentagon"].get_value<uint8_t>();
dist.cuadrado = yaml["cuadrado"].get_value<uint8_t>(); dist.cuadrado = yaml["cuadrado"].get_value<uint8_t>();
dist.molinillo = yaml["molinillo"].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 // Validar que suma 100
int sum = dist.pentagon + dist.cuadrado + dist.molinillo; int sum = dist.pentagon + dist.cuadrado + dist.molinillo + dist.star;
if (sum != 100) { if (sum != 100) {
std::cerr << "[StageLoader] Error: distribució no suma 100 (suma=" << sum << ")" << '\n'; std::cerr << "[StageLoader] Error: distribució no suma 100 (suma=" << sum << ")" << '\n';
return false; return false;
@@ -184,9 +186,9 @@ auto StageLoader::parseDistribution(const fkyaml::node& yaml, DistribucioEnemics
std::cerr << "[StageLoader] Error parsing distribution: " << e.what() << '\n'; std::cerr << "[StageLoader] Error parsing distribution: " << e.what() << '\n';
return false; 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("speed_multiplier") || !yaml.contains("rotation_multiplier") ||
!yaml.contains("tracking_strength")) { !yaml.contains("tracking_strength")) {
@@ -214,9 +216,9 @@ auto StageLoader::parseMultipliers(const fkyaml::node& yaml, MultiplicadorsDific
std::cerr << "[StageLoader] Error parsing multipliers: " << e.what() << '\n'; std::cerr << "[StageLoader] Error parsing multipliers: " << e.what() << '\n';
return false; return false;
} }
} }
auto StageLoader::parseSpawnMode(const std::string& mode_str) -> ModeSpawn { auto StageLoader::parseSpawnMode(const std::string& mode_str) -> ModeSpawn {
if (mode_str == "progressive") { if (mode_str == "progressive") {
return ModeSpawn::PROGRESSIVE; return ModeSpawn::PROGRESSIVE;
} }
@@ -229,9 +231,9 @@ auto StageLoader::parseSpawnMode(const std::string& mode_str) -> ModeSpawn {
std::cerr << "[StageLoader] Warning: mode de spawn desconegut '" << mode_str std::cerr << "[StageLoader] Warning: mode de spawn desconegut '" << mode_str
<< "', usant PROGRESSIVE" << '\n'; << "', usant PROGRESSIVE" << '\n';
return ModeSpawn::PROGRESSIVE; 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: sin stage carregat" << '\n';
return false; return false;
@@ -254,6 +256,6 @@ auto StageLoader::validateConfig(const StageSystemConfig& config) -> bool {
} }
return true; return true;
} }
} // namespace StageSystem } // namespace StageSystem
+14 -102
View File
@@ -8,86 +8,12 @@
#include "core/physics/collision.hpp" #include "core/physics/collision.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/constants.hpp" #include "game/constants.hpp"
#include "game/entities/bullet_config.hpp"
#include "game/systems/enemy_event_dispatcher.hpp"
namespace Systems::Collision { namespace Systems::Collision {
namespace { namespace {
constexpr uint8_t NO_SHOOTER = 0xFF;
// Lookup tabla puntos / color por tipo de enemy (mantiene la lógica
// anterior pero centralizada para reutilizar entre paths).
auto scoreForType(EnemyType type) -> int {
switch (type) {
case EnemyType::PENTAGON:
return Defaults::Enemies::Scoring::PENTAGON_SCORE;
case EnemyType::SQUARE:
return Defaults::Enemies::Scoring::SQUARE_SCORE;
case EnemyType::PINWHEEL:
return Defaults::Enemies::Scoring::PINWHEEL_SCORE;
}
return 0;
}
auto colorForType(EnemyType type) -> SDL_Color {
switch (type) {
case EnemyType::PENTAGON:
return Defaults::Palette::PENTAGON;
case EnemyType::SQUARE:
return Defaults::Palette::SQUARE;
case EnemyType::PINWHEEL:
return Defaults::Palette::PINWHEEL;
}
return SDL_Color{};
}
// Mata al enemy con explosión: floating score, debris con velocity heredada,
// sonido. Si shooter_id ≠ NO_SHOOTER, suma puntos a ese jugador.
// CRUCIAL: leer velocity/datos ANTES de destruir() (que zera la velocity).
void explodeNow(Context& ctx, Enemy& enemy, uint8_t shooter_id) {
const Vec2 ENEMY_POS = enemy.getCenter();
const Vec2 ENEMY_VEL = enemy.getVelocityVector();
const float BRIGHTNESS = enemy.getBrightness();
const auto SHAPE = enemy.getShape();
const EnemyType TYPE = enemy.getType();
const int POINTS = scoreForType(TYPE);
const SDL_Color COLOR = colorForType(TYPE);
if (shooter_id != NO_SHOOTER) {
ctx.score_per_player[shooter_id] += POINTS;
}
ctx.floating_score_manager.crear(POINTS, ENEMY_POS);
enemy.destroy();
constexpr float SPEED_EXPLOSIO = 80.0F; // px/s (explosión suave)
const Vec2 INHERITED_VEL = ENEMY_VEL * Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE;
ctx.debris_manager.explode(
SHAPE,
ENEMY_POS,
0.0F, // angle (rotación interna del enemy)
1.0F, // escala
SPEED_EXPLOSIO,
BRIGHTNESS,
INHERITED_VEL,
0.0F, // sense herència angular: evita que els 5 trossos curvin en bloc
0.0F, // sin herencia visual
Defaults::Sound::EXPLOSION,
COLOR,
Defaults::Physics::Debris::ENEMY_LIFETIME,
Defaults::Physics::Debris::ENEMY_FRICTION,
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER);
// Firework burst radial des del centro de l'enemic (efecte adicional al debris).
// Línia blanca + halo daurat (WOUNDED) per a feel d'espurnes.
ctx.firework_manager.spawn(ENEMY_POS,
Defaults::FX::Firework::DEFAULT_COLOR,
Defaults::FX::Firework::SPEED,
Defaults::FX::Firework::N_POINTS,
Defaults::FX::Firework::INITIAL_BRIGHTNESS,
/*glow=*/true,
Defaults::Palette::WOUNDED);
}
// 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 HIT + 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,
@@ -105,7 +31,7 @@ namespace Systems::Collision {
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::HIT,
Defaults::Palette::BULLET, bullet.getConfig().colors.normal,
Defaults::Physics::Debris::TEMPS_VIDA, Defaults::Physics::Debris::TEMPS_VIDA,
Defaults::Physics::Debris::ACCELERACIO, Defaults::Physics::Debris::ACCELERACIO,
1); // sense duplicat de segments 1); // sense duplicat de segments
@@ -121,31 +47,16 @@ namespace Systems::Collision {
continue; continue;
} }
for (auto& enemy : ctx.enemies) { for (auto& enemy : ctx.enemies) {
if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), Defaults::Entities::BULLET_RADIUS, enemy, AMPLIFIER)) { if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), bullet.getCollisionRadius(), enemy, AMPLIFIER)) {
continue; continue;
} }
// *** COLISIÓN bullet → enemy *** // *** COLISIÓN bullet → enemy ***
// Empuje físico cuasi-realista: el impulse és el moment de la bala // La cadena d'efectes (impulse, hurt, destroy, debris, score...) viu
// (m·v) multiplicat pel factor de transferència. Direcció = vector // al YAML de l'enemic via la secció `events:`. Aquí només dispatchem.
// velocity de la bala (cap a on viatjava). Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HIT, bullet.getOwnerId(), &bullet);
const Vec2 IMPULSE = bullet.getBody().velocity *
(bullet.getBody().mass * Defaults::Physics::Bullet::IMPACT_MOMENTUM_FACTOR);
enemy.applyImpulse(IMPULSE);
const uint8_t SHOOTER = bullet.getOwnerId();
if (enemy.isWounded()) {
// Segundo impacto sobre enemy ya herido → muerte instantánea,
// puntos al nuevo shooter.
explodeNow(ctx, enemy, SHOOTER);
} else {
// Primer impacto → entra en estado herido (explosión diferida).
enemy.hurt(SHOOTER);
}
breakBullet(ctx.debris_manager, bullet); breakBullet(ctx.debris_manager, bullet);
break; // Una bala impacta a un enemy y muere break;
} }
} }
} }
@@ -156,7 +67,7 @@ namespace Systems::Collision {
continue; continue;
} }
enemy.consumeWoundExpired(); enemy.consumeWoundExpired();
explodeNow(ctx, enemy, enemy.getLastHitBy()); Systems::EnemyEvents::dispatchEvent(ctx, enemy, EnemyEventType::ON_HURT_END, enemy.getLastHitBy());
} }
} }
@@ -224,7 +135,8 @@ namespace Systems::Collision {
// Segon impacte durant HURT → mort. Aplica un impuls afegit // Segon impacte durant HURT → mort. Aplica un impuls afegit
// perquè l'enemic surti disparat (feedback visible). // perquè l'enemic surti disparat (feedback visible).
const Vec2 SHIP_VEL = ctx.ships[i].getVelocityVector(); const Vec2 SHIP_VEL = ctx.ships[i].getVelocityVector();
const Vec2 IMPULSE = SHIP_VEL * (Defaults::Ship::MASS * Defaults::Physics::Ship::DEATH_IMPACT_MOMENTUM_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);
touched_enemy->applyImpulse(IMPULSE); touched_enemy->applyImpulse(IMPULSE);
ctx.on_player_hit(i); ctx.on_player_hit(i);
} else { } else {
@@ -268,7 +180,7 @@ namespace Systems::Collision {
continue; continue;
} }
if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), Defaults::Entities::BULLET_RADIUS, ctx.ships[player_id], AMPLIFIER)) { if (!Physics::checkCollisionSwept(bullet.getPrevPosition(), bullet.getCenter(), bullet.getCollisionRadius(), ctx.ships[player_id], AMPLIFIER)) {
continue; continue;
} }
@@ -277,7 +189,7 @@ namespace Systems::Collision {
// de la bala a la nau ABANS de on_player_hit perquè tocado() // de la bala a la nau ABANS de on_player_hit perquè tocado()
// captura la velocitat per als debris (si no, queden quiets). // captura la velocitat per als debris (si no, queden quiets).
const Vec2 BULLET_IMPULSE = bullet.getBody().velocity * const Vec2 BULLET_IMPULSE = bullet.getBody().velocity *
(bullet.getBody().mass * Defaults::Physics::Bullet::IMPACT_MOMENTUM_FACTOR); (bullet.getBody().mass * bullet.getConfig().physics.impact_momentum_factor);
ctx.ships[player_id].getBody().applyImpulse(BULLET_IMPULSE); ctx.ships[player_id].getBody().applyImpulse(BULLET_IMPULSE);
ctx.on_player_hit(player_id); ctx.on_player_hit(player_id);
ctx.lives_per_player[BULLET_OWNER]++; ctx.lives_per_player[BULLET_OWNER]++;
@@ -304,12 +216,12 @@ namespace Systems::Collision {
float min_y; float min_y;
float max_y; float max_y;
Constants::getPlayAreaBounds(min_x, max_x, min_y, max_y); Constants::getPlayAreaBounds(min_x, max_x, min_y, max_y);
constexpr float R = Defaults::Entities::BULLET_RADIUS;
for (auto& bullet : bullets) { for (auto& bullet : bullets) {
if (!bullet.isActive()) { if (!bullet.isActive()) {
continue; continue;
} }
const float R = bullet.getCollisionRadius();
const Vec2& pos = bullet.getCenter(); const Vec2& pos = bullet.getCenter();
if (pos.x < min_x + R || pos.x > max_x - R || if (pos.x < min_x + R || pos.x > max_x - R ||
pos.y < min_y + R || pos.y > max_y - R) { pos.y < min_y + R || pos.y > max_y - R) {
@@ -0,0 +1,101 @@
// enemy_event_dispatcher.cpp - Implementació del dispatcher d'events d'enemic
// © 2026 JailDesigner
#include "game/systems/enemy_event_dispatcher.hpp"
#include <cstdint>
#include "core/defaults.hpp"
#include "core/types.hpp"
#include "game/entities/bullet.hpp"
#include "game/entities/bullet_config.hpp"
#include "game/entities/enemy_config.hpp"
namespace Systems::EnemyEvents {
namespace {
constexpr uint8_t NO_SHOOTER = 0xFF;
void doAddScore(Systems::Collision::Context& ctx, const Enemy& enemy, uint8_t shooter) {
const int POINTS = enemy.getConfig().score;
if (shooter != NO_SHOOTER) {
ctx.score_per_player[shooter] += POINTS;
}
ctx.floating_score_manager.crear(POINTS, enemy.getCenter());
}
void doCreateDebris(Systems::Collision::Context& ctx, const Enemy& enemy) {
constexpr float SPEED_EXPLOSIO = 80.0F;
const Vec2 INHERITED_VEL = enemy.getVelocityVector() *
Defaults::Physics::Debris::ENEMY_VELOCITY_INHERITANCE;
ctx.debris_manager.explode(
enemy.getShape(),
enemy.getCenter(),
0.0F,
1.0F,
SPEED_EXPLOSIO,
enemy.getBrightness(),
INHERITED_VEL,
0.0F,
0.0F,
Defaults::Sound::EXPLOSION,
enemy.getConfig().colors.normal,
Defaults::Physics::Debris::ENEMY_LIFETIME,
Defaults::Physics::Debris::ENEMY_FRICTION,
Defaults::Physics::Debris::ENEMY_SEGMENT_MULTIPLIER);
}
void doCreateFireworks(Systems::Collision::Context& ctx, const Enemy& enemy) {
ctx.firework_manager.spawn(enemy.getCenter(),
Defaults::FX::Firework::DEFAULT_COLOR,
Defaults::FX::Firework::SPEED,
Defaults::FX::Firework::N_POINTS,
Defaults::FX::Firework::INITIAL_BRIGHTNESS,
/*glow=*/true,
enemy.getConfig().colors.wounded);
}
void doApplyImpulse(Enemy& enemy, const Bullet* bullet) {
if (bullet == nullptr) {
return;
}
const Vec2 IMPULSE = bullet->getBody().velocity *
(bullet->getBody().mass * bullet->getConfig().physics.impact_momentum_factor);
enemy.applyImpulse(IMPULSE);
}
} // namespace
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);
for (const auto& action : actions) {
switch (action.type) {
case EnemyActionType::SET_HURT:
if (enemy.isWounded()) {
// Segon hit sobre wounded → mort immediata (regla 2-hits).
dispatchEvent(ctx, enemy, EnemyEventType::ON_DESTROY, shooter_id, bullet);
enemy.destroy();
} else {
enemy.hurt(shooter_id);
}
break;
case EnemyActionType::DESTROY:
dispatchEvent(ctx, enemy, EnemyEventType::ON_DESTROY, shooter_id, bullet);
enemy.destroy();
break;
case EnemyActionType::ADD_SCORE:
doAddScore(ctx, enemy, shooter_id);
break;
case EnemyActionType::CREATE_DEBRIS:
doCreateDebris(ctx, enemy);
break;
case EnemyActionType::CREATE_FIREWORKS:
doCreateFireworks(ctx, enemy);
break;
case EnemyActionType::APPLY_IMPULSE:
doApplyImpulse(enemy, bullet);
break;
}
}
}
} // namespace Systems::EnemyEvents
@@ -0,0 +1,23 @@
// enemy_event_dispatcher.hpp - Executa les accions YAML d'un event d'enemic
// © 2026 JailDesigner
//
// Mira la llista d'EnemyAction associada a l'event al config de l'enemic i les
// executa una per una. L'acció DESTROY dispara recursivament ON_DESTROY abans
// de desactivar físicament l'enemic (el parser garanteix que ON_DESTROY no
// conté DESTROY, evitant recursió infinita).
#pragma once
#include <cstdint>
#include "game/entities/enemy_event.hpp"
#include "game/systems/collision_system.hpp"
namespace Systems::EnemyEvents {
// shooter_id: id del jugador que ha disparat (0xFF = sense atribució).
// bullet: punter opcional a la bala que ha causat l'event (usat per APPLY_IMPULSE);
// nullptr per a events no derivats d'una bala (on_hurt_end).
void dispatchEvent(Systems::Collision::Context& ctx, Enemy& enemy, EnemyEventType event, uint8_t shooter_id, const Bullet* bullet = nullptr);
} // namespace Systems::EnemyEvents
+73 -66
View File
@@ -1,92 +1,99 @@
// pack_resources.cpp - Utilitat per crear paquets de recursos // pack_resources.cpp - Utilitat per crear paquets de recursos
// © 2026 JailDesigner // © 2026 JailDesigner
#include "../../source/core/resources/resource_pack.hpp"
#include <filesystem> #include <filesystem>
#include <iostream> #include <iostream>
#include <string>
void print_usage(const char* program_name) { #include "core/resources/resource_pack.hpp"
std::cout << "Ús: " << program_name << " [opcions] [directori_entrada] [fitxer_sortida]\n"; #include "project.h"
std::cout << "\nOpcions:\n";
std::cout << " --list <fitxer> Llistar contingut d'un paquet\n"; namespace {
std::cout << "\nExemples:\n";
std::cout << " " << program_name << " data resources.pack\n"; void showHelp() {
std::cout << " " << program_name << " --list resources.pack\n"; std::cout << Project::LONG_NAME << " - Resource Packer\n";
std::cout << "\nSi no s'especifiquen arguments, empaqueta 'data/' a 'resources.pack'\n"; std::cout << "==============================================\n";
} std::cout << "Usage: pack_resources [options] [input_dir] [output_file]\n\n";
std::cout << "Options:\n";
std::cout << " --help Show this help message\n";
std::cout << " --list List contents of an existing pack file\n\n";
std::cout << "Arguments:\n";
std::cout << " input_dir Directory to pack (default: data)\n";
std::cout << " output_file Pack file name (default: resources.pack)\n";
}
void listPackContents(const std::string& pack_file) {
Resource::Pack pack;
if (!pack.loadPack(pack_file)) {
std::cerr << "Error: cannot open pack file: " << pack_file << '\n';
return;
}
auto resources = pack.getResourceList();
std::cout << "Pack file: " << pack_file << '\n';
std::cout << "Resources: " << resources.size() << '\n';
for (const auto& r : resources) { std::cout << " " << r << '\n'; }
}
} // namespace
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
std::string input_dir = "data"; std::string data_dir = "data";
std::string output_file = "resources.pack"; std::string output_file = "resources.pack";
bool list_mode = false;
bool data_dir_set = false;
// Processar arguments for (int i = 1; i < argc; ++i) {
if (argc == 2 && std::string(argv[1]) == "--help") { std::string arg = argv[i];
print_usage(argv[0]); if (arg == "--help" || arg == "-h") {
showHelp();
return 0;
}
if (arg == "--list") {
list_mode = true;
if (i + 1 < argc) { output_file = argv[++i]; }
continue;
}
if (!arg.empty() && arg[0] != '-') {
if (!data_dir_set) {
data_dir = arg;
data_dir_set = true;
} else {
output_file = arg;
}
}
}
if (list_mode) {
listPackContents(output_file);
return 0; return 0;
} }
// Mode --list std::cout << Project::LONG_NAME << " - Resource Packer\n";
if (argc == 3 && std::string(argv[1]) == "--list") { std::cout << "==============================================\n";
Resource::Pack pack; std::cout << "Input directory: " << data_dir << '\n';
if (!pack.loadPack(argv[2])) { std::cout << "Output file: " << output_file << '\n';
std::cerr << "ERROR: No es pot carregar " << argv[2] << "\n";
if (!std::filesystem::exists(data_dir)) {
std::cerr << "Error: input directory does not exist: " << data_dir << '\n';
return 1; return 1;
} }
std::cout << "Contingut de " << argv[2] << ":\n";
auto resources = pack.getResourceList();
std::cout << "Total: " << resources.size() << " recursos\n\n";
for (const auto& name : resources) {
std::cout << " " << name << "\n";
}
return 0;
}
// Mode empaquetar
if (argc >= 3) {
input_dir = argv[1];
output_file = argv[2];
}
// Verificar que existeix el directori
if (!std::filesystem::exists(input_dir)) {
std::cerr << "ERROR: Directori no trobat: " << input_dir << "\n";
return 1;
}
if (!std::filesystem::is_directory(input_dir)) {
std::cerr << "ERROR: " << input_dir << " no és un directori\n";
return 1;
}
// Crear paquet
std::cout << "Creant paquet de recursos...\n";
std::cout << " Entrada: " << input_dir << "\n";
std::cout << " Sortida: " << output_file << "\n\n";
Resource::Pack pack; Resource::Pack pack;
if (!pack.addDirectory(input_dir)) { std::cout << "Scanning and packing resources...\n";
std::cerr << "ERROR: No s'ha pogut afegir el directori\n"; if (!pack.addDirectory(data_dir)) {
std::cerr << "Error: failed to add directory to pack\n";
return 1; return 1;
} }
std::cout << "Found " << pack.getResourceList().size() << " resources\n";
std::cout << "Saving pack file...\n";
if (!pack.savePack(output_file)) { if (!pack.savePack(output_file)) {
std::cerr << "ERROR: No s'ha pogut guardar el paquet\n"; std::cerr << "Error: failed to save pack file\n";
return 1; return 1;
} }
// Resum auto file_size = std::filesystem::file_size(std::filesystem::path(output_file));
auto resources = pack.getResourceList(); std::cout << "Pack file created: " << output_file << " ("
std::cout << "\n"; << (static_cast<double>(file_size) / 1024.0 / 1024.0) << " MB)\n";
std::cout << "✓ Paquet creat amb èxit!\n";
std::cout << " Recursos: " << resources.size() << "\n";
// Mostrar mida del fitxer
auto file_size = std::filesystem::file_size(output_file);
std::cout << " Mida: " << (file_size / 1024) << " KB\n";
return 0; return 0;
} }